forked from mirrors/homebox
style updates
This commit is contained in:
parent
f4f7123073
commit
6bbe62823d
19 changed files with 337 additions and 107 deletions
|
@ -64,48 +64,52 @@
|
||||||
<LabelCreateModal v-model="modals.label" />
|
<LabelCreateModal v-model="modals.label" />
|
||||||
<LocationCreateModal v-model="modals.location" />
|
<LocationCreateModal v-model="modals.location" />
|
||||||
|
|
||||||
<BaseContainer is="header" class="py-6">
|
<div class="bg-neutral absolute shadow-xl top-0 h-[50vh] max-h-96 sm:h-[28vh] -z-10 w-full"></div>
|
||||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-base-content sm:text-5xl lg:text-6xl flex">
|
|
||||||
HomeB
|
<BaseContainer is="header" class="py-6 max-w-none">
|
||||||
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
|
<BaseContainer>
|
||||||
x
|
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
|
||||||
</h2>
|
HomeB
|
||||||
<div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
|
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
|
||||||
<template v-for="link in links">
|
x
|
||||||
<NuxtLink
|
</h2>
|
||||||
v-if="!link.action"
|
<div class="ml-1 mt-2 text-lg text-neutral-content/75 space-x-2">
|
||||||
class="hover:text-base-content transition-color duration-200 italic"
|
<template v-for="link in links">
|
||||||
:to="link.href"
|
<NuxtLink
|
||||||
>
|
v-if="!link.action"
|
||||||
{{ link.name }}
|
class="hover:text-base-content transition-color duration-200 italic"
|
||||||
</NuxtLink>
|
:to="link.href"
|
||||||
<button
|
>
|
||||||
for="location-form-modal"
|
{{ link.name }}
|
||||||
v-else
|
</NuxtLink>
|
||||||
@click="link.action"
|
<button
|
||||||
class="hover:text-base-content transition-color duration-200 italic"
|
for="location-form-modal"
|
||||||
>
|
v-else
|
||||||
{{ link.name }}
|
@click="link.action"
|
||||||
</button>
|
class="hover:text-base-content transition-color duration-200 italic"
|
||||||
<span v-if="!link.last"> / </span>
|
>
|
||||||
</template>
|
{{ link.name }}
|
||||||
</div>
|
</button>
|
||||||
<div class="flex mt-6">
|
<span v-if="!link.last"> / </span>
|
||||||
<div class="dropdown">
|
</template>
|
||||||
<label tabindex="0" class="btn btn-sm">
|
|
||||||
<span>
|
|
||||||
<Icon name="mdi-plus" class="mr-1 -ml-1" />
|
|
||||||
</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>
|
||||||
</div>
|
<div class="flex mt-6">
|
||||||
|
<div class="dropdown">
|
||||||
|
<label tabindex="0" class="btn btn-primary btn-sm">
|
||||||
|
<span>
|
||||||
|
<Icon name="mdi-plus" class="mr-1 -ml-1" />
|
||||||
|
</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>
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div class="divider">
|
||||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
<div class="btn-group min-w-[180px] flex-nowrap">
|
||||||
<div class="w-full border-t border-primary" />
|
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
|
||||||
</div>
|
<Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||||
<div class="relative flex justify-center">
|
<span> Edit </span>
|
||||||
<span class="isolate inline-flex -space-x-px rounded-md shadow-sm">
|
</button>
|
||||||
<div class="btn-group">
|
<button @click="$emit('delete')" name="options" class="btn btn-sm btn-primary">
|
||||||
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
|
<Icon name="heroicons-trash" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||||
<Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
|
<span> Delete </span>
|
||||||
<span> Edit </span>
|
</button>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineEmits(['edit', 'delete']);
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||||
<div class="px-4 py-5 sm:px-6 bg-neutral">
|
<div class="px-4 py-5 sm:px-6">
|
||||||
<h3 class="text-lg font-medium leading-6 text-neutral-content">
|
<h3 class="text-lg font-medium leading-6">
|
||||||
<slot name="title"></slot>
|
<slot name="title"></slot>
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="border-b border-base-200 pb-3">
|
<div class="pb-3">
|
||||||
<h3 class="text-xl font-medium leading-4 text-base-content">
|
<h3
|
||||||
|
class="text-3xl font-bold tracking-tight"
|
||||||
|
:class="{
|
||||||
|
'text-neutral-content': dark,
|
||||||
|
'text-content': !dark,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</h3>
|
</h3>
|
||||||
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
||||||
|
@ -8,3 +14,12 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
dark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
</div>
|
</div>
|
||||||
<ul
|
<ul
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-96 overflow-y-scroll scroll-bar"
|
class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll scroll-bar"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="(obj, idx) in items"
|
v-for="(obj, idx) in items"
|
||||||
|
|
30
frontend/components/Item/Card.vue
Normal file
30
frontend/components/Item/Card.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
class="group card bg-neutral text-neutral-content hover:bg-primary transition-colors duration-300"
|
||||||
|
:to="`/item/${item.id}`"
|
||||||
|
>
|
||||||
|
<div class="card-body py-4 px-6">
|
||||||
|
<h2 class="card-title">
|
||||||
|
<Icon name="mdi-package-variant" />
|
||||||
|
{{ item.name }}
|
||||||
|
</h2>
|
||||||
|
<p>{{ item.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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Item } from '~~/lib/api/classes/items';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
item: {
|
||||||
|
type: Object as () => Item,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -26,6 +26,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { type Location } from '~~/lib/api/classes/locations';
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -39,7 +40,7 @@
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const focused = ref(false);
|
const focused = ref(false);
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
location: {},
|
location: {} as Location,
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
color: '', // Future!
|
color: '', // Future!
|
||||||
|
@ -75,7 +76,18 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
const { data, error } = await api.labels.create(form);
|
if (!form.location) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out = {
|
||||||
|
name: form.name,
|
||||||
|
description: form.description,
|
||||||
|
locationId: form.location.id as string,
|
||||||
|
labelIds: form.labels.map(l => l.id) as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, error } = await api.items.create(out);
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error("Couldn't create label");
|
toast.error("Couldn't create label");
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
export type sizes = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
import { Label } from '~~/lib/api/classes/labels';
|
import { Label } from '~~/lib/api/classes/labels';
|
||||||
defineProps({
|
defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: Object as () => Label,
|
type: Object as () => Label,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String as () => sizes,
|
||||||
|
default: 'md',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const badge = ref(null);
|
const badge = ref(null);
|
||||||
|
@ -15,13 +21,19 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink ref="badge" :to="`/label/${label.id}`">
|
<NuxtLink
|
||||||
<span class="badge badge-lg p-4">
|
class="badge"
|
||||||
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
:class="{
|
||||||
<Icon name="heroicons-arrow-right" class="mr-2 swap-on"></Icon>
|
'p-3': size !== 'sm',
|
||||||
<Icon name="heroicons-tag" class="mr-2 swap-off"></Icon>
|
'p-2 badge-sm': size === 'sm',
|
||||||
</label>
|
}"
|
||||||
{{ label.name }}
|
ref="badge"
|
||||||
</span>
|
:to="`/label/${label.id}`"
|
||||||
|
>
|
||||||
|
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
||||||
|
<Icon name="heroicons-arrow-right" class="mr-2 swap-on"></Icon>
|
||||||
|
<Icon name="heroicons-tag" class="mr-2 swap-off"></Icon>
|
||||||
|
</label>
|
||||||
|
{{ label.name }}
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
37
frontend/components/Location/Card.vue
Normal file
37
frontend/components/Location/Card.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLink
|
||||||
|
ref="card"
|
||||||
|
:to="`/location/${location.id}`"
|
||||||
|
class="card bg-primary text-primary-content transition duration-300"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h2 class="flex items-center gap-2">
|
||||||
|
<label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
|
||||||
|
<Icon name="heroicons-arrow-right" class="swap-on" />
|
||||||
|
<Icon name="heroicons-map-pin" class="swap-off" />
|
||||||
|
</label>
|
||||||
|
{{ location.name }}
|
||||||
|
<span class="badge badge-secondary badge-lg ml-auto text-secondary-content">
|
||||||
|
{{ location.itemCount }}</span
|
||||||
|
>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</NuxtLink>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Location } from '~~/lib/api/classes/locations';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
location: {
|
||||||
|
type: Object as () => Location,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = ref(null);
|
||||||
|
const isHover = useElementHover(card);
|
||||||
|
const { focused } = useFocus(card);
|
||||||
|
|
||||||
|
const isActive = computed(() => isHover.value || focused.value);
|
||||||
|
</script>
|
54
frontend/lib/api/classes/items.ts
Normal file
54
frontend/lib/api/classes/items.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { BaseAPI, UrlBuilder } from '../base';
|
||||||
|
import { Label } from './labels';
|
||||||
|
import { Location } from './locations';
|
||||||
|
import { Results } from './types';
|
||||||
|
|
||||||
|
export interface ItemCreate {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
locationId: string;
|
||||||
|
labelIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
createdAt: string;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
labels: Label[];
|
||||||
|
location: Location;
|
||||||
|
manufacturer: string;
|
||||||
|
modelNumber: string;
|
||||||
|
name: string;
|
||||||
|
notes: string;
|
||||||
|
purchaseFrom: string;
|
||||||
|
purchasePrice: number;
|
||||||
|
purchaseTime: string;
|
||||||
|
serialNumber: string;
|
||||||
|
soldNotes: string;
|
||||||
|
soldPrice: number;
|
||||||
|
soldTime: string;
|
||||||
|
soldTo: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemsApi extends BaseAPI {
|
||||||
|
async getAll() {
|
||||||
|
return this.http.get<Results<Item>>(UrlBuilder('/items'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(item: ItemCreate) {
|
||||||
|
return this.http.post<ItemCreate, Item>(UrlBuilder('/items'), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string) {
|
||||||
|
return this.http.get<Item>(UrlBuilder(`/items/${id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string) {
|
||||||
|
return this.http.delete<void>(UrlBuilder(`/items/${id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, item: ItemCreate) {
|
||||||
|
return this.http.put<ItemCreate, Item>(UrlBuilder(`/items/${id}`), item);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { BaseAPI, UrlBuilder } from '../base';
|
import { BaseAPI, UrlBuilder } from '../base';
|
||||||
|
import { Item } from './items';
|
||||||
import { Details, OutType, Results } from './types';
|
import { Details, OutType, Results } from './types';
|
||||||
|
|
||||||
export type LocationCreate = Details;
|
export type LocationCreate = Details;
|
||||||
|
@ -6,6 +7,8 @@ export type LocationCreate = Details;
|
||||||
export type Location = LocationCreate &
|
export type Location = LocationCreate &
|
||||||
OutType & {
|
OutType & {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
items: Item[];
|
||||||
|
itemCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LocationUpdate = LocationCreate;
|
export type LocationUpdate = LocationCreate;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Requests } from '~~/lib/requests';
|
import { Requests } from '~~/lib/requests';
|
||||||
import { BaseAPI, UrlBuilder } from './base';
|
import { BaseAPI, UrlBuilder } from './base';
|
||||||
|
import { ItemsApi } from './classes/items';
|
||||||
import { LabelsApi } from './classes/labels';
|
import { LabelsApi } from './classes/labels';
|
||||||
import { LocationsApi } from './classes/locations';
|
import { LocationsApi } from './classes/locations';
|
||||||
|
|
||||||
|
@ -17,11 +18,13 @@ export type User = {
|
||||||
export class UserApi extends BaseAPI {
|
export class UserApi extends BaseAPI {
|
||||||
locations: LocationsApi;
|
locations: LocationsApi;
|
||||||
labels: LabelsApi;
|
labels: LabelsApi;
|
||||||
|
items: ItemsApi;
|
||||||
constructor(requests: Requests) {
|
constructor(requests: Requests) {
|
||||||
super(requests);
|
super(requests);
|
||||||
|
|
||||||
this.locations = new LocationsApi(requests);
|
this.locations = new LocationsApi(requests);
|
||||||
this.labels = new LabelsApi(requests);
|
this.labels = new LabelsApi(requests);
|
||||||
|
this.items = new ItemsApi(requests);
|
||||||
|
|
||||||
Object.freeze(this);
|
Object.freeze(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,10 @@ import { defineNuxtConfig } from 'nuxt';
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
ssr: false,
|
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' }],
|
||||||
|
},
|
||||||
vite: {
|
vite: {
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|
|
@ -17,33 +17,85 @@
|
||||||
const { data } = await api.labels.getAll();
|
const { data } = await api.labels.getAll();
|
||||||
return data.items;
|
return data.items;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: items } = useAsyncData('items', async () => {
|
||||||
|
const { data } = await api.items.getAll();
|
||||||
|
return data.items;
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalItems = computed(() => items.value?.length || 0);
|
||||||
|
const totalLocations = computed(() => locations.value?.length || 0);
|
||||||
|
const totalLabels = computed(() => labels.value?.length || 0);
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
label: 'Locations',
|
||||||
|
value: totalLocations,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Items',
|
||||||
|
value: totalItems,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Labels',
|
||||||
|
value: totalLabels,
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BaseContainer class="space-y-16">
|
<BaseContainer class="space-y-16">
|
||||||
|
<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>
|
||||||
|
<div class="bg-white p-6">
|
||||||
|
<div class="sm:flex sm:items-center sm:justify-between">
|
||||||
|
<div class="sm:flex sm:space-x-5">
|
||||||
|
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Welcome back,</p>
|
||||||
|
<p class="text-xl font-bold text-gray-900 sm:text-2xl">Hayden Kotelman</p>
|
||||||
|
<p class="text-sm font-medium text-gray-600">User</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 flex justify-center sm:mt-0">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
class="flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50"
|
||||||
|
>View profile</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 divide-y divide-gray-200 border-t border-gray-200 bg-gray-50 sm:grid-cols-3 sm:divide-y-0 sm:divide-x"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
|
||||||
<NuxtLink
|
<LocationCard v-for="location in locations" :location="location" />
|
||||||
:to="`/location/${l.id}`"
|
</div>
|
||||||
class="card bg-primary text-primary-content hover:-translate-y-1 focus:-translate-y-1 transition duration-300"
|
</section>
|
||||||
v-for="l in locations"
|
|
||||||
>
|
<section>
|
||||||
<div class="card-body p-4">
|
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||||
<h2 class="flex items-center gap-2">
|
<div class="grid sm:grid-cols-2 gap-4">
|
||||||
<Icon name="heroicons-map-pin" class="h-5 w-5 text-white" height="25" />
|
<ItemCard v-for="item in items" :item="item" />
|
||||||
{{ l.name }}
|
|
||||||
<!-- <span class="badge badge-accent badge-lg ml-auto text-accent-content text-lg">0</span> -->
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<LabelChip v-for="label in labels" :label="label" />
|
<LabelChip v-for="label in labels" size="lg" :label="label" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
|
|
|
@ -11,6 +11,11 @@
|
||||||
layout: 'empty',
|
layout: 'empty',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
if (!authStore.isTokenExpired) {
|
||||||
|
navigateTo('/home');
|
||||||
|
}
|
||||||
|
|
||||||
const registerFields = [
|
const registerFields = [
|
||||||
{
|
{
|
||||||
label: "What's your name?",
|
label: "What's your name?",
|
||||||
|
@ -72,8 +77,6 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
const toast = useNotifier();
|
const toast = useNotifier();
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
<section>
|
<section>
|
||||||
<BaseSectionHeader class="mb-5">
|
<BaseSectionHeader class="mb-5" dark>
|
||||||
{{ label ? label.name : '' }}
|
{{ label ? label.name : '' }}
|
||||||
</BaseSectionHeader>
|
</BaseSectionHeader>
|
||||||
<BaseDetails class="mb-2" :details="details">
|
<BaseDetails class="mb-2" :details="details">
|
||||||
|
|
|
@ -112,7 +112,7 @@
|
||||||
</form>
|
</form>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
<section>
|
<section>
|
||||||
<BaseSectionHeader class="mb-5">
|
<BaseSectionHeader class="mb-5" dark>
|
||||||
{{ location ? location.name : '' }}
|
{{ location ? location.name : '' }}
|
||||||
</BaseSectionHeader>
|
</BaseSectionHeader>
|
||||||
<BaseDetails class="mb-2" :details="details">
|
<BaseDetails class="mb-2" :details="details">
|
||||||
|
@ -127,8 +127,11 @@
|
||||||
<ActionsDivider @delete="confirmDelete" @edit="openUpdate" />
|
<ActionsDivider @delete="confirmDelete" @edit="openUpdate" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- <section>
|
<section v-if="location">
|
||||||
<BaseSectionHeader> Items </BaseSectionHeader>
|
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||||
</section> -->
|
<div class="grid gap-2 grid-cols-2">
|
||||||
|
<ItemCard v-for="item in location.items" :item="item" :key="item.id" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</BaseContainer>
|
</BaseContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
1
frontend/public/favicon.svg
Normal file
1
frontend/public/favicon.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" /></svg>
|
After Width: | Height: | Size: 707 B |
|
@ -1,19 +1,19 @@
|
||||||
import { UserApi } from "~~/lib/api/user";
|
import { UserApi } from '~~/lib/api/user';
|
||||||
import { defineStore } from "pinia";
|
import { defineStore } from 'pinia';
|
||||||
import { useLocalStorage } from "@vueuse/core";
|
import { useLocalStorage } from '@vueuse/core';
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
token: useLocalStorage("pinia/auth/token", ""),
|
token: useLocalStorage('pinia/auth/token', ''),
|
||||||
expires: useLocalStorage("pinia/auth/expires", ""),
|
expires: useLocalStorage('pinia/auth/expires', ''),
|
||||||
}),
|
}),
|
||||||
getters: {
|
getters: {
|
||||||
isTokenExpired: (state) => {
|
isTokenExpired: state => {
|
||||||
if (!state.expires) {
|
if (!state.expires) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof state.expires === "string") {
|
if (typeof state.expires === 'string') {
|
||||||
return new Date(state.expires) < new Date();
|
return new Date(state.expires) < new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ export const useAuthStore = defineStore("auth", {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.token = "";
|
this.token = '';
|
||||||
this.expires = "";
|
this.expires = '';
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue