style updates

This commit is contained in:
Hayden 2022-09-03 01:17:57 -08:00
parent f4f7123073
commit 6bbe62823d
19 changed files with 337 additions and 107 deletions

View file

@ -64,13 +64,16 @@
<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">
<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 HomeB
<AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" /> <AppLogo class="w-12 -mb-4" style="padding-left: 3px; padding-right: 2px" />
x x
</h2> </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"> <template v-for="link in links">
<NuxtLink <NuxtLink
v-if="!link.action" v-if="!link.action"
@ -92,7 +95,7 @@
</div> </div>
<div class="flex mt-6"> <div class="flex mt-6">
<div class="dropdown"> <div class="dropdown">
<label tabindex="0" class="btn btn-sm"> <label tabindex="0" class="btn btn-primary btn-sm">
<span> <span>
<Icon name="mdi-plus" class="mr-1 -ml-1" /> <Icon name="mdi-plus" class="mr-1 -ml-1" />
</span> </span>
@ -108,4 +111,5 @@
</div> </div>
</div> </div>
</BaseContainer> </BaseContainer>
</BaseContainer>
</template> </template>

View file

@ -1,11 +1,6 @@
<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" />
</div>
<div class="relative flex justify-center">
<span class="isolate inline-flex -space-x-px rounded-md shadow-sm">
<div class="btn-group">
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary"> <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" /> <Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
<span> Edit </span> <span> Edit </span>
@ -15,7 +10,9 @@
<span> Delete </span> <span> Delete </span>
</button> </button>
</div> </div>
</span>
</div>
</div> </div>
</template> </template>
<script lang="ts" setup>
defineEmits(['edit', 'delete']);
</script>

View file

@ -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">

View file

@ -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>

View file

@ -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"

View 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>

View file

@ -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;

View file

@ -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"
: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' : ''"> <label class="swap swap-rotate" :class="isActive ? 'swap-active' : ''">
<Icon name="heroicons-arrow-right" class="mr-2 swap-on"></Icon> <Icon name="heroicons-arrow-right" class="mr-2 swap-on"></Icon>
<Icon name="heroicons-tag" class="mr-2 swap-off"></Icon> <Icon name="heroicons-tag" class="mr-2 swap-off"></Icon>
</label> </label>
{{ label.name }} {{ label.name }}
</span>
</NuxtLink> </NuxtLink>
</template> </template>

View 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>

View 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);
}
}

View file

@ -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;

View file

@ -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);
} }

View file

@ -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: {

View file

@ -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}`"
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> </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> </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>

View file

@ -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);

View file

@ -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">

View file

@ -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>

View 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

View file

@ -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;
}, },