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,13 +64,16 @@
|
|||
<LabelCreateModal v-model="modals.label" />
|
||||
<LocationCreateModal v-model="modals.location" />
|
||||
|
||||
<BaseContainer is="header" class="py-6">
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-base-content sm:text-5xl lg:text-6xl flex">
|
||||
<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>
|
||||
<h2 class="mt-1 text-4xl font-bold tracking-tight text-neutral-content sm:text-5xl lg:text-6xl flex">
|
||||
HomeB
|
||||
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
|
||||
x
|
||||
</h2>
|
||||
<div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
|
||||
<div class="ml-1 mt-2 text-lg text-neutral-content/75 space-x-2">
|
||||
<template v-for="link in links">
|
||||
<NuxtLink
|
||||
v-if="!link.action"
|
||||
|
@ -92,7 +95,7 @@
|
|||
</div>
|
||||
<div class="flex mt-6">
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-sm">
|
||||
<label tabindex="0" class="btn btn-primary btn-sm">
|
||||
<span>
|
||||
<Icon name="mdi-plus" class="mr-1 -ml-1" />
|
||||
</span>
|
||||
|
@ -108,4 +111,5 @@
|
|||
</div>
|
||||
</div>
|
||||
</BaseContainer>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div class="w-full border-t border-primary" />
|
||||
</div>
|
||||
<div class="relative flex justify-center">
|
||||
<span class="isolate inline-flex -space-x-px rounded-md shadow-sm">
|
||||
<div class="btn-group">
|
||||
<div class="divider">
|
||||
<div class="btn-group min-w-[180px] flex-nowrap">
|
||||
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
|
||||
<Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||
<span> Edit </span>
|
||||
|
@ -15,7 +10,9 @@
|
|||
<span> Delete </span>
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineEmits(['edit', 'delete']);
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 bg-neutral">
|
||||
<h3 class="text-lg font-medium leading-6 text-neutral-content">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<h3 class="text-lg font-medium leading-6">
|
||||
<slot name="title"></slot>
|
||||
</h3>
|
||||
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<template>
|
||||
<div class="border-b border-base-200 pb-3">
|
||||
<h3 class="text-xl font-medium leading-4 text-base-content">
|
||||
<div class="pb-3">
|
||||
<h3
|
||||
class="text-3xl font-bold tracking-tight"
|
||||
:class="{
|
||||
'text-neutral-content': dark,
|
||||
'text-content': !dark,
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</h3>
|
||||
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
|
@ -8,3 +14,12 @@
|
|||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
dark: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<ul
|
||||
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
|
||||
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { type Location } from '~~/lib/api/classes/locations';
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
|
@ -39,7 +40,7 @@
|
|||
const loading = ref(false);
|
||||
const focused = ref(false);
|
||||
const form = reactive({
|
||||
location: {},
|
||||
location: {} as Location,
|
||||
name: '',
|
||||
description: '',
|
||||
color: '', // Future!
|
||||
|
@ -75,7 +76,18 @@
|
|||
});
|
||||
|
||||
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) {
|
||||
toast.error("Couldn't create label");
|
||||
return;
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
<script setup lang="ts">
|
||||
export type sizes = 'sm' | 'md' | 'lg';
|
||||
|
||||
import { Label } from '~~/lib/api/classes/labels';
|
||||
defineProps({
|
||||
label: {
|
||||
type: Object as () => Label,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: String as () => sizes,
|
||||
default: 'md',
|
||||
},
|
||||
});
|
||||
|
||||
const badge = ref(null);
|
||||
|
@ -15,13 +21,19 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink ref="badge" :to="`/label/${label.id}`">
|
||||
<span class="badge badge-lg p-4">
|
||||
<NuxtLink
|
||||
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' : ''">
|
||||
<Icon name="heroicons-arrow-right" class="mr-2 swap-on"></Icon>
|
||||
<Icon name="heroicons-tag" class="mr-2 swap-off"></Icon>
|
||||
</label>
|
||||
{{ label.name }}
|
||||
</span>
|
||||
</NuxtLink>
|
||||
</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 { Item } from './items';
|
||||
import { Details, OutType, Results } from './types';
|
||||
|
||||
export type LocationCreate = Details;
|
||||
|
@ -6,6 +7,8 @@ export type LocationCreate = Details;
|
|||
export type Location = LocationCreate &
|
||||
OutType & {
|
||||
groupId: string;
|
||||
items: Item[];
|
||||
itemCount: number;
|
||||
};
|
||||
|
||||
export type LocationUpdate = LocationCreate;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Requests } from '~~/lib/requests';
|
||||
import { BaseAPI, UrlBuilder } from './base';
|
||||
import { ItemsApi } from './classes/items';
|
||||
import { LabelsApi } from './classes/labels';
|
||||
import { LocationsApi } from './classes/locations';
|
||||
|
||||
|
@ -17,11 +18,13 @@ export type User = {
|
|||
export class UserApi extends BaseAPI {
|
||||
locations: LocationsApi;
|
||||
labels: LabelsApi;
|
||||
items: ItemsApi;
|
||||
constructor(requests: Requests) {
|
||||
super(requests);
|
||||
|
||||
this.locations = new LocationsApi(requests);
|
||||
this.labels = new LabelsApi(requests);
|
||||
this.items = new ItemsApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@ import { defineNuxtConfig } from 'nuxt';
|
|||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vueuse/nuxt'],
|
||||
meta: {
|
||||
title: 'Homebox',
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' }],
|
||||
},
|
||||
vite: {
|
||||
server: {
|
||||
proxy: {
|
||||
|
|
|
@ -17,33 +17,85 @@
|
|||
const { data } = await api.labels.getAll();
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
<BaseSectionHeader class="mb-5"> Storage Locations </BaseSectionHeader>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<NuxtLink
|
||||
:to="`/location/${l.id}`"
|
||||
class="card bg-primary text-primary-content hover:-translate-y-1 focus:-translate-y-1 transition duration-300"
|
||||
v-for="l in locations"
|
||||
>
|
||||
<div class="card-body p-4">
|
||||
<h2 class="flex items-center gap-2">
|
||||
<Icon name="heroicons-map-pin" class="h-5 w-5 text-white" height="25" />
|
||||
{{ l.name }}
|
||||
<!-- <span class="badge badge-accent badge-lg ml-auto text-accent-content text-lg">0</span> -->
|
||||
</h2>
|
||||
<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" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Items </BaseSectionHeader>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<ItemCard v-for="item in items" :item="item" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2">
|
||||
<LabelChip v-for="label in labels" :label="label" />
|
||||
<LabelChip v-for="label in labels" size="lg" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
|
|
|
@ -11,6 +11,11 @@
|
|||
layout: 'empty',
|
||||
});
|
||||
|
||||
const authStore = useAuthStore();
|
||||
if (!authStore.isTokenExpired) {
|
||||
navigateTo('/home');
|
||||
}
|
||||
|
||||
const registerFields = [
|
||||
{
|
||||
label: "What's your name?",
|
||||
|
@ -72,8 +77,6 @@
|
|||
},
|
||||
];
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const toast = useNotifier();
|
||||
const loading = ref(false);
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
</form>
|
||||
</BaseModal>
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5">
|
||||
<BaseSectionHeader class="mb-5" dark>
|
||||
{{ label ? label.name : '' }}
|
||||
</BaseSectionHeader>
|
||||
<BaseDetails class="mb-2" :details="details">
|
||||
|
|
|
@ -112,7 +112,7 @@
|
|||
</form>
|
||||
</BaseModal>
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5">
|
||||
<BaseSectionHeader class="mb-5" dark>
|
||||
{{ location ? location.name : '' }}
|
||||
</BaseSectionHeader>
|
||||
<BaseDetails class="mb-2" :details="details">
|
||||
|
@ -127,8 +127,11 @@
|
|||
<ActionsDivider @delete="confirmDelete" @edit="openUpdate" />
|
||||
</section>
|
||||
|
||||
<!-- <section>
|
||||
<BaseSectionHeader> Items </BaseSectionHeader>
|
||||
</section> -->
|
||||
<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" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</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 { defineStore } from "pinia";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { UserApi } from '~~/lib/api/user';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useLocalStorage } from '@vueuse/core';
|
||||
|
||||
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) => {
|
||||
isTokenExpired: state => {
|
||||
if (!state.expires) {
|
||||
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;
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue