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,48 +64,52 @@
<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">
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">
<template v-for="link in links">
<NuxtLink
v-if="!link.action"
class="hover:text-base-content transition-color duration-200 italic"
:to="link.href"
>
{{ link.name }}
</NuxtLink>
<button
for="location-form-modal"
v-else
@click="link.action"
class="hover:text-base-content transition-color duration-200 italic"
>
{{ link.name }}
</button>
<span v-if="!link.last"> / </span>
</template>
</div>
<div class="flex mt-6">
<div class="dropdown">
<label tabindex="0" class="btn btn-sm">
<span>
<Icon name="mdi-plus" class="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 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-neutral-content/75 space-x-2">
<template v-for="link in links">
<NuxtLink
v-if="!link.action"
class="hover:text-base-content transition-color duration-200 italic"
:to="link.href"
>
{{ link.name }}
</NuxtLink>
<button
for="location-form-modal"
v-else
@click="link.action"
class="hover:text-base-content transition-color duration-200 italic"
>
{{ link.name }}
</button>
<span v-if="!link.last"> / </span>
</template>
</div>
</div>
<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>
</template>

View file

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

View file

@ -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="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
<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">

View file

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

View file

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

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

View file

@ -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">
<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
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 }}
</NuxtLink>
</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 { 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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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