wip: frontend redesign

This commit is contained in:
Hayden 2022-12-10 16:55:23 -09:00
parent ca4ccc6053
commit b66bc407fc
No known key found for this signature in database
GPG key ID: 17CF79474E257545
30 changed files with 1096 additions and 292 deletions

View file

@ -20,5 +20,5 @@
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"eslint.format.enable": true,
}

View file

@ -1665,6 +1665,10 @@ const docTemplate = `{
"name": {
"type": "string"
},
"purchasePrice": {
"type": "string",
"example": "0"
},
"quantity": {
"type": "integer"
},

View file

@ -1657,6 +1657,10 @@
"name": {
"type": "string"
},
"purchasePrice": {
"type": "string",
"example": "0"
},
"quantity": {
"type": "integer"
},

View file

@ -202,6 +202,9 @@ definitions:
x-omitempty: true
name:
type: string
purchasePrice:
example: "0"
type: string
quantity:
type: integer
updatedAt:

View file

@ -97,18 +97,18 @@ func newCsvRow(row []string) csvRow {
LabelStr: row[2],
Item: repo.ItemOut{
ItemSummary: repo.ItemSummary{
ImportRef: row[0],
Quantity: parseInt(row[3]),
Name: row[4],
Description: row[5],
Insured: parseBool(row[6]),
ImportRef: row[0],
Quantity: parseInt(row[3]),
Name: row[4],
Description: row[5],
Insured: parseBool(row[6]),
PurchasePrice: parseFloat(row[12]),
},
SerialNumber: row[7],
ModelNumber: row[8],
Manufacturer: row[9],
Notes: row[10],
PurchaseFrom: row[11],
PurchasePrice: parseFloat(row[12]),
PurchaseTime: parseDate(row[13]),
LifetimeWarranty: parseBool(row[14]),
WarrantyExpires: parseDate(row[15]),

View file

@ -101,6 +101,8 @@ type (
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
PurchasePrice float64 `json:"purchasePrice,string"`
// Edges
Location *LocationSummary `json:"location,omitempty" extensions:"x-nullable,x-omitempty"`
Labels []LabelSummary `json:"labels"`
@ -121,9 +123,8 @@ type (
WarrantyDetails string `json:"warrantyDetails"`
// Purchase
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
PurchasePrice float64 `json:"purchasePrice,string"`
PurchaseTime time.Time `json:"purchaseTime"`
PurchaseFrom string `json:"purchaseFrom"`
// Sold
SoldTime time.Time `json:"soldTime"`
@ -157,13 +158,14 @@ func mapItemSummary(item *ent.Item) ItemSummary {
}
return ItemSummary{
ID: item.ID,
Name: item.Name,
Description: item.Description,
Quantity: item.Quantity,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Archived: item.Archived,
ID: item.ID,
Name: item.Name,
Description: item.Description,
Quantity: item.Quantity,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Archived: item.Archived,
PurchasePrice: item.PurchasePrice,
// Edges
Location: location,
@ -230,9 +232,8 @@ func mapItemOut(item *ent.Item) ItemOut {
Manufacturer: item.Manufacturer,
// Purchase
PurchaseTime: item.PurchaseTime,
PurchaseFrom: item.PurchaseFrom,
PurchasePrice: item.PurchasePrice,
PurchaseTime: item.PurchaseTime,
PurchaseFrom: item.PurchaseFrom,
// Sold
SoldTime: item.SoldTime,

1
frontend/.nuxtignore Normal file
View file

@ -0,0 +1 @@
pages/**/*.ts

View file

@ -1,6 +1,6 @@
<template>
<NuxtLayout>
<Html lang="en" :data-theme="theme" />
<Html lang="en" :data-theme="theme || 'homebox'" />
<NuxtPage />
</NuxtLayout>
</template>

View file

@ -0,0 +1,3 @@
.text-no-transform {
text-transform: none !important;
}

View file

@ -0,0 +1,337 @@
<template>
<svg viewBox="0 0 1440 237" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)" filter="url(#b)">
<path fill="#fff" d="M0-103h1440v310H0z" />
<path fill="#2C2E27" d="M0-103h1440v310H0z" />
<g clip-path="url(#c)">
<path
d="M1344.93 230.569h-75.81v-45.486h30.32c5.98 0 11.89 1.177 17.41 3.463s10.53 5.636 14.76 9.86c8.53 8.53 13.32 20.099 13.32 32.163Z"
fill="#507B5D"
/>
<path
d="M1297.89 170.07c0-3.395-2.75-6.147-6.14-6.147-3.4 0-6.15 2.752-6.15 6.147 0 3.395 2.75 6.148 6.15 6.148 3.39 0 6.14-2.753 6.14-6.148ZM1328.45 170.07c0-3.395-2.75-6.147-6.15-6.147-3.39 0-6.15 2.752-6.15 6.147 0 3.395 2.76 6.148 6.15 6.148 3.4 0 6.15-2.753 6.15-6.148Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#d)">
<path
d="M1401.78 60c7.5 0 14.83 2.2231 21.06 6.388 6.24 4.165 11.09 10.0848 13.96 17.0109 2.87 6.9261 3.62 14.5474 2.16 21.9001-1.46 7.353-5.07 14.107-10.37 19.408-5.3 5.301-12.06 8.911-19.41 10.373-7.35 1.463-14.98.712-21.9-2.157-6.93-2.869-12.85-7.727-17.01-13.96-4.17-6.234-6.39-13.562-6.39-21.0587V60h37.9Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#e)">
<path d="M1269.12 135.809h75.81V60h-75.81v75.809Z" fill="#FFDA56" />
</g>
<g clip-path="url(#f)">
<path
d="M1250.17 97.9043c0 7.4967-2.22 14.8247-6.39 21.0587-4.16 6.233-10.08 11.091-17.01 13.96-6.93 2.869-14.55 3.62-21.9 2.157-7.35-1.462-14.11-5.072-19.41-10.373-5.3-5.301-8.91-12.055-10.37-19.408-1.46-7.3527-.71-14.974 2.16-21.9001 2.86-6.9261 7.72-12.8459 13.96-17.0109 6.23-4.1649 13.56-6.388 21.05-6.388h37.91v37.9043Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#g)">
<path d="M1079.6 135.809h75.81V60h-75.81v75.809Z" fill="#FFDA56" />
</g>
<g clip-path="url(#h)">
<path
d="M890.08 60h75.808v45.485h-30.323c-5.973 0-11.888-1.176-17.406-3.462-5.519-2.2861-10.533-5.6365-14.757-9.8602C894.872 83.6327 890.08 72.0634 890.08 60Z"
fill="#507B5D"
/>
<path
d="M937.114 120.498c0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.147-2.753 6.147-6.148 0-3.395-2.752-6.147-6.147-6.147-3.395 0-6.147 2.752-6.147 6.147ZM906.56 120.498c0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#i)">
<path d="M795.32 154.76v75.809h75.808V154.76H795.32Z" fill="#FFDA56" />
</g>
<g clip-path="url(#j)">
<path
d="M776.368 154.76v75.809h-45.485v-30.324c0-5.973 1.177-11.888 3.463-17.406 2.286-5.519 5.636-10.533 9.86-14.757 8.53-8.53 20.099-13.322 32.162-13.322Z"
fill="#507B5D"
/>
<path
d="M715.87 201.794c-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147ZM715.87 171.24c-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148 3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#k)">
<path
d="M871.128 97.9043c0 7.4967-2.223 14.8247-6.388 21.0587-4.165 6.233-10.084 11.091-17.01 13.96-6.927 2.869-14.548 3.62-21.901 2.157-7.352-1.462-14.106-5.072-19.407-10.373-5.301-5.301-8.911-12.055-10.374-19.408-1.462-7.3527-.712-14.974 2.157-21.9001 2.869-6.9261 7.727-12.8459 13.961-17.0109C818.399 62.2231 825.727 60 833.224 60h37.904v37.9043Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#l)">
<path
d="M679.334 184.193c-8.907 0-8.907 16.943-17.815 16.943-8.908 0-8.904-16.943-17.807-16.943-8.904 0-8.91 16.943-17.82 16.943s-8.908-16.943-17.818-16.943"
stroke="#F6FAFB"
stroke-width="4.54851"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<g clip-path="url(#m)">
<path
d="M605.8 97.9043c0-7.4968 2.223-14.8252 6.388-21.0585 4.165-6.2333 10.085-11.0916 17.011-13.9605 6.926-2.8689 14.547-3.6195 21.9-2.157 7.353 1.4626 14.107 5.0726 19.408 10.3736 5.301 5.301 8.911 12.0549 10.373 19.4076 1.463 7.3527.712 14.9735-2.157 21.9005-2.869 6.926-7.727 12.846-13.96 17.01-6.234 4.165-13.562 6.389-21.059 6.389H605.8V97.9043Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#n)">
<path
d="M511.04 135.809V60h45.485v30.3234c0 5.9732-1.176 11.8876-3.462 17.4066-2.286 5.518-5.637 10.533-9.86 14.756-8.53 8.53-20.1 13.323-32.163 13.323Z"
fill="#507B5D"
/>
<path
d="M571.538 88.7746c3.395 0 6.147-2.7522 6.147-6.1473 0-3.395-2.752-6.1473-6.147-6.1473-3.395 0-6.147 2.7523-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473ZM571.538 119.328c3.395 0 6.147-2.752 6.147-6.147 0-3.395-2.752-6.147-6.147-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.147 6.147 6.147Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#o)">
<path
d="M489.814 184.193c-8.907 0-8.907 16.943-17.815 16.943-8.908 0-8.904-16.943-17.807-16.943-8.904 0-8.91 16.943-17.82 16.943s-8.908-16.943-17.818-16.943"
stroke="#F6FAFB"
stroke-width="4.54851"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<g clip-path="url(#p)">
<path
d="M454.184 135.809c-7.497 0-14.825-2.224-21.058-6.389-6.234-4.164-11.092-10.084-13.961-17.01-2.869-6.927-3.619-14.5478-2.157-21.9005 1.463-7.3527 5.073-14.1066 10.374-19.4076 5.301-5.301 12.055-8.911 19.407-10.3736 7.353-1.4625 14.974-.7119 21.9 2.157 6.927 2.8689 12.846 7.7272 17.011 13.9605 4.165 6.2333 6.388 13.5617 6.388 21.0585v37.9047h-37.904Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#q)">
<path
d="M132 230.569V154.76h45.485v30.323c0 5.974-1.176 11.888-3.462 17.407-2.286 5.518-5.636 10.533-9.86 14.756-8.53 8.53-20.1 13.323-32.163 13.323Z"
fill="#507B5D"
/>
<path
d="M192.498 183.535c3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148ZM192.498 214.089c3.395 0 6.148-2.753 6.148-6.148 0-3.395-2.753-6.147-6.148-6.147-3.395 0-6.147 2.752-6.147 6.147 0 3.395 2.752 6.148 6.147 6.148Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#r)">
<path
d="M302.569 135.809H226.76V90.3234h30.323c5.974 0 11.888 1.1765 17.407 3.4624 5.518 2.2858 10.533 5.6362 14.756 9.8602 8.53 8.53 13.323 20.099 13.323 32.163Z"
fill="#507B5D"
/>
<path
d="M255.535 75.3103c0-3.3951-2.753-6.1473-6.148-6.1473-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473ZM286.089 75.3103c0-3.3951-2.753-6.1473-6.148-6.1473-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#s)">
<path d="M207.809 60H132v75.809h75.809V60Z" fill="#FFDA56" />
</g>
<g clip-path="url(#t)">
<path d="M502-31v75.8085h75.809V-31H502Z" fill="#FFDA56" />
</g>
<g clip-path="url(#u)">
<path
d="M65.9043-31.24c7.4967 0 14.8251 2.223 21.0584 6.388 6.2334 4.165 11.0916 10.0848 13.9603 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.0724 14.1066-10.3734 19.4076-5.301 5.301-12.0549 8.911-19.4076 10.3736-7.3527 1.4625-14.974.7119-21.9001-2.157-6.9261-2.8689-12.8459-7.7272-17.0109-13.9605C30.2231 21.4894 28 14.161 28 6.66425V-31.24h37.9043Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#v)">
<path
d="M160.664 44.5685c-7.496 0-14.825-2.223-21.058-6.388-6.234-4.165-11.092-10.0848-13.961-17.0109-2.869-6.9261-3.619-14.54737-2.157-21.900078 1.463-7.352702 5.073-14.106622 10.374-19.407622s12.055-8.911 19.408-10.3736c7.352-1.4625 14.974-.7119 21.9 2.157 6.926 2.8689 12.846 7.7272 17.011 13.9605 4.164 6.23332 6.388 13.561722 6.388 21.05848V44.5685h-37.905Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#w)">
<path
d="M388.089 6.66425c0 7.49675-2.224 14.82515-6.388 21.05845-4.165 6.2333-10.085 11.0916-17.011 13.9605-6.926 2.8689-14.548 3.6195-21.9 2.157-7.353-1.4626-14.107-5.0726-19.408-10.3736-5.301-5.301-8.911-12.0549-10.374-19.4076-1.462-7.3527-.712-14.973986 2.157-21.90008 2.869-6.92612 7.728-12.84592 13.961-17.01092 6.233-4.165 13.562-6.388 21.058-6.388h37.905V6.66425Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#x)">
<path
d="M444.944-31.24c7.497 0 14.825 2.223 21.059 6.388 6.233 4.165 11.091 10.0848 13.96 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.072 14.1066-10.373 19.4076-5.301 5.301-12.055 8.911-19.408 10.3736-7.353 1.4625-14.974.7119-21.9-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.904Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#y)">
<path
d="M634.464 44.5685c-7.496 0-14.825-2.223-21.058-6.388-6.234-4.165-11.092-10.0848-13.961-17.0109-2.869-6.9261-3.619-14.54737-2.157-21.900078 1.463-7.352702 5.073-14.106622 10.374-19.407622s12.055-8.911 19.408-10.3736c7.352-1.4625 14.973-.7119 21.9 2.157 6.926 2.8689 12.846 7.7272 17.01 13.9605 4.165 6.23332 6.389 13.561722 6.389 21.05848V44.5685h-37.905Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#z)">
<path
d="M823.984-31.24c7.497 0 14.825 2.223 21.059 6.388 6.233 4.165 11.091 10.0848 13.96 17.01092 2.869 6.926094 3.62 14.54738 2.157 21.90008-1.462 7.3527-5.072 14.1066-10.373 19.4076-5.301 5.301-12.055 8.911-19.408 10.3736-7.353 1.4625-14.974.7119-21.9-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.904Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#A)">
<path
d="M956.648-31.24v75.8085h-45.485V14.2451c0-5.97318 1.177-11.88788 3.463-17.40639 2.286-5.5185 5.636-10.53271 9.86-14.75641 8.53-8.5301 20.099-13.3223 32.162-13.3223Z"
fill="#507B5D"
/>
<path
d="M896.15 15.7939c-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473 0-3.3951-2.753-6.1473-6.148-6.1473Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#B)">
<path
d="M1013.5-31.24c7.5 0 14.83 2.223 21.06 6.388 6.24 4.165 11.09 10.0848 13.96 17.01092 2.87 6.926094 3.62 14.54738 2.16 21.90008-1.46 7.3527-5.07 14.1066-10.37 19.4076-5.3 5.301-12.06 8.911-19.41 10.3736-7.35 1.4625-14.97.7119-21.901-2.157-6.926-2.8689-12.846-7.7272-17.011-13.9605-4.165-6.2333-6.388-13.5617-6.388-21.05845V-31.24h37.9Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#C)">
<path
d="M1070.36 44.5685V-31.24h45.49V-.916588c0 5.973188-1.18 11.887888-3.47 17.406388-2.28 5.5185-5.63 10.5328-9.86 14.7564-8.53 8.5302-20.1 13.3223-32.16 13.3223Z"
fill="#507B5D"
/>
<path
d="M1130.86 28.0885c3.39 0 6.15-2.7522 6.15-6.1473 0-3.3951-2.76-6.1473-6.15-6.1473-3.4 0-6.15 2.7522-6.15 6.1473 0 3.3951 2.75 6.1473 6.15 6.1473Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#D)">
<path
d="M1203.02 44.5685c-7.49 0-14.82-2.223-21.05-6.388-6.24-4.165-11.1-10.0848-13.96-17.0109-2.87-6.9261-3.62-14.54737-2.16-21.900078 1.46-7.352702 5.07-14.106622 10.37-19.407622 5.3-5.301 12.06-8.911 19.41-10.3736 7.35-1.4625 14.97-.7119 21.9 2.157 6.93 2.8689 12.85 7.7272 17.01 13.9605 4.17 6.23332 6.39 13.561722 6.39 21.05848V44.5685h-37.91Z"
fill="#F6FAFB"
/>
</g>
<g clip-path="url(#E)">
<path
d="M1335.69-31.24v75.8085h-45.49V14.2451c0-5.97318 1.18-11.88788 3.47-17.40639 2.28-5.5185 5.63-10.53271 9.86-14.75641 8.53-8.5301 20.1-13.3223 32.16-13.3223Z"
fill="#507B5D"
/>
<path
d="M1275.19 15.7939c-3.39 0-6.15 2.7522-6.15 6.1473 0 3.3951 2.76 6.1473 6.15 6.1473 3.4 0 6.15-2.7522 6.15-6.1473 0-3.3951-2.75-6.1473-6.15-6.1473Z"
fill="#B4D9AE"
/>
</g>
<g clip-path="url(#F)">
<path
d="M292.808-31v75.8085h-45.485V14.4851c0-5.97316 1.177-11.88786 3.463-17.40636 2.286-5.51851 5.636-10.53274 9.86-14.75644C269.176-26.2078 280.745-31 292.808-31Z"
fill="#507B5D"
/>
<path
d="M232.31 16.0339c-3.395 0-6.147 2.7522-6.147 6.1473 0 3.3951 2.752 6.1473 6.147 6.1473 3.395 0 6.148-2.7522 6.148-6.1473 0-3.3951-2.753-6.1473-6.148-6.1473Z"
fill="#B4D9AE"
/>
</g>
<path
d="M937.07 193v-40.557h9.802v16.278h15.982v-16.278h9.782V193h-9.782v-16.298h-15.982V193h-9.802Zm55.914.574c-3.195 0-5.941-.653-8.238-1.96-2.284-1.32-4.046-3.156-5.287-5.506-1.228-2.363-1.842-5.102-1.842-8.218 0-3.129.614-5.868 1.842-8.218 1.241-2.364 3.003-4.199 5.287-5.506 2.297-1.32 5.043-1.98 8.238-1.98 3.195 0 5.935.66 8.216 1.98 2.3 1.307 4.06 3.142 5.29 5.506 1.24 2.35 1.86 5.089 1.86 8.218 0 3.116-.62 5.855-1.86 8.218-1.23 2.35-2.99 4.186-5.29 5.506-2.281 1.307-5.021 1.96-8.216 1.96Zm.06-7.307c1.162 0 2.145-.357 2.95-1.07.806-.712 1.42-1.703 1.842-2.97.436-1.267.654-2.733.654-4.396 0-1.69-.218-3.169-.654-4.436-.422-1.268-1.036-2.258-1.842-2.971-.805-.713-1.788-1.069-2.95-1.069-1.202 0-2.218.356-3.05 1.069-.819.713-1.446 1.703-1.881 2.971-.423 1.267-.634 2.746-.634 4.436 0 1.663.211 3.129.634 4.396.435 1.267 1.062 2.258 1.881 2.97.832.713 1.848 1.07 3.05 1.07ZM1013.31 193v-30.418h9.21v5.585h.34c.63-1.849 1.7-3.308 3.21-4.377 1.5-1.069 3.3-1.604 5.38-1.604 2.11 0 3.92.541 5.43 1.624 1.5 1.082 2.46 2.535 2.87 4.357h.32c.56-1.809 1.67-3.255 3.32-4.337 1.65-1.096 3.6-1.644 5.85-1.644 2.87 0 5.21.924 7.01 2.772 1.79 1.836 2.69 4.357 2.69 7.565V193h-9.68v-18.259c0-1.518-.39-2.673-1.17-3.465-.78-.806-1.79-1.208-3.03-1.208-1.34 0-2.39.435-3.15 1.307-.75.858-1.13 2.013-1.13 3.465V193h-9.31v-18.358c0-1.412-.38-2.528-1.15-3.346-.76-.819-1.77-1.228-3.03-1.228-.84 0-1.59.204-2.23.614-.65.396-1.16.963-1.53 1.703-.36.739-.53 1.61-.53 2.614V193h-9.69Zm65.85.574c-3.18 0-5.93-.627-8.24-1.881-2.3-1.267-4.07-3.07-5.31-5.406-1.22-2.35-1.84-5.143-1.84-8.377 0-3.142.62-5.888 1.86-8.238 1.24-2.364 2.99-4.199 5.25-5.506 2.26-1.32 4.92-1.98 7.98-1.98 2.17 0 4.15.337 5.94 1.01 1.8.673 3.35 1.67 4.66 2.99 1.3 1.32 2.32 2.951 3.05 4.892.72 1.927 1.09 4.139 1.09 6.634v2.416h-26.44v-5.624h17.42c-.01-1.03-.25-1.948-.73-2.753-.47-.805-1.13-1.432-1.96-1.881-.82-.462-1.76-.693-2.83-.693-1.08 0-2.05.244-2.91.732-.86.476-1.54 1.129-2.04 1.961-.5.818-.77 1.749-.79 2.792v5.723c0 1.241.24 2.33.73 3.268.49.924 1.18 1.643 2.08 2.158.9.515 1.97.773 3.21.773.86 0 1.63-.119 2.33-.357.7-.237 1.3-.587 1.81-1.049.5-.462.87-1.03 1.12-1.703l8.9.257c-.37 1.994-1.19 3.73-2.44 5.208-1.24 1.466-2.87 2.608-4.89 3.426-2.02.806-4.36 1.208-7.01 1.208Zm19.38-.574v-40.557h16.91c3.04 0 5.58.429 7.63 1.287 2.06.858 3.6 2.059 4.63 3.604 1.04 1.545 1.57 3.334 1.57 5.367 0 1.545-.33 2.924-.97 4.139-.65 1.201-1.54 2.198-2.68 2.99-1.13.792-2.45 1.347-3.94 1.664v.396c1.64.079 3.15.521 4.54 1.326 1.4.806 2.52 1.928 3.36 3.367.85 1.426 1.27 3.116 1.27 5.07 0 2.178-.55 4.125-1.66 5.842-1.11 1.703-2.72 3.049-4.82 4.04-2.09.977-4.64 1.465-7.64 1.465h-18.2Zm9.8-7.902h6.06c2.13 0 3.69-.402 4.7-1.208 1.01-.805 1.52-1.927 1.52-3.366 0-1.043-.24-1.941-.73-2.693-.49-.766-1.18-1.354-2.08-1.763-.9-.422-1.97-.634-3.23-.634h-6.24v9.664Zm0-15.981h5.43c1.07 0 2.02-.178 2.85-.535.83-.356 1.48-.871 1.94-1.544.48-.674.72-1.485.72-2.436 0-1.36-.49-2.429-1.45-3.208-.96-.779-2.26-1.169-3.9-1.169h-5.59v8.892Zm71.41-6.535 5.02 10.021 5.18-10.021h9.77l-8.5 15.209 8.81 15.209h-9.68l-5.58-10.1-5.45 10.1h-9.82l8.83-15.209-8.4-15.209h9.82Z"
fill="#fff"
/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0-103h1440v310H0z" />
</clipPath>
<clipPath id="c">
<path fill="#fff" d="M1269.12 230.569v-75.8085h75.8085v75.8085z" />
</clipPath>
<clipPath id="d">
<path fill="#fff" d="M1439.69 135.809h-75.8085V60.0005h75.8085z" />
</clipPath>
<clipPath id="e">
<path fill="#fff" d="M1344.93 135.809h-75.8085V60.0005h75.8085z" />
</clipPath>
<clipPath id="f">
<path fill="#fff" d="M1174.36 135.809V60.0005h75.8085v75.8085z" />
</clipPath>
<clipPath id="g">
<path fill="#fff" d="M1155.41 135.809h-75.8085V60.0005h75.8085z" />
</clipPath>
<clipPath id="h">
<path fill="#fff" d="M965.888 60v75.8085h-75.8085V60z" />
</clipPath>
<clipPath id="i">
<path fill="#fff" d="M795.32 230.569v-75.8085h75.8085v75.8085z" />
</clipPath>
<clipPath id="j">
<path fill="#fff" d="M776.368 230.569h-75.8085v-75.8085h75.8085z" />
</clipPath>
<clipPath id="k">
<path fill="#fff" d="M795.32 135.809V60.0005h75.8085v75.8085z" />
</clipPath>
<clipPath id="l">
<path fill="#fff" d="M681.608 154.76v75.8085h-75.8085V154.76z" />
</clipPath>
<clipPath id="m">
<path fill="#fff" d="M681.608 60v75.8085h-75.8085V60z" />
</clipPath>
<clipPath id="n">
<path fill="#fff" d="M511.04 60h75.8085v75.8085H511.04z" />
</clipPath>
<clipPath id="o">
<path fill="#fff" d="M492.088 154.76v75.8085h-75.8085V154.76z" />
</clipPath>
<clipPath id="p">
<path fill="#fff" d="M416.28 60h75.8085v75.8085H416.28z" />
</clipPath>
<clipPath id="q">
<path fill="#fff" d="M132 154.76h75.8085v75.8085H132z" />
</clipPath>
<clipPath id="r">
<path fill="#fff" d="M226.76 135.809V60.0005h75.8085v75.8085z" />
</clipPath>
<clipPath id="s">
<path fill="#fff" d="M132 60h75.8085v75.8085H132z" />
</clipPath>
<clipPath id="t">
<path fill="#fff" d="M502 44.8085V-31h75.8085v75.8085z" />
</clipPath>
<clipPath id="u">
<path fill="#fff" d="M103.809 44.5685H28.0005V-31.24h75.8085z" />
</clipPath>
<clipPath id="v">
<path fill="#fff" d="M122.76-31.24h75.8085v75.8085H122.76z" />
</clipPath>
<clipPath id="w">
<path fill="#fff" d="M312.28 44.5685V-31.24h75.8085v75.8085z" />
</clipPath>
<clipPath id="x">
<path fill="#fff" d="M482.849 44.5685h-75.8085V-31.24h75.8085z" />
</clipPath>
<clipPath id="y">
<path fill="#fff" d="M596.56-31.24h75.8085v75.8085H596.56z" />
</clipPath>
<clipPath id="z">
<path fill="#fff" d="M861.889 44.5685h-75.8085V-31.24h75.8085z" />
</clipPath>
<clipPath id="A">
<path fill="#fff" d="M956.648 44.5685h-75.8085V-31.24h75.8085z" />
</clipPath>
<clipPath id="B">
<path fill="#fff" d="M1051.41 44.5685h-75.8085V-31.24h75.8085z" />
</clipPath>
<clipPath id="C">
<path fill="#fff" d="M1070.36-31.24h75.8085v75.8085H1070.36z" />
</clipPath>
<clipPath id="D">
<path fill="#fff" d="M1165.12-31.24h75.8085v75.8085H1165.12z" />
</clipPath>
<clipPath id="E">
<path fill="#fff" d="M1335.69 44.5685h-75.8085V-31.24h75.8085z" />
</clipPath>
<clipPath id="F">
<path fill="#fff" d="M292.808 44.8085h-75.8085V-31h75.8085z" />
</clipPath>
<filter
id="b"
x="-22"
y="-117"
width="1484"
height="354"
filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB"
>
<feFlood flood-opacity="0" result="BackgroundImageFix" />
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha" />
<feMorphology radius="4" in="SourceAlpha" result="effect1_dropShadow_5_1510" />
<feOffset dy="8" />
<feGaussianBlur stdDeviation="13" />
<feComposite in2="hardAlpha" operator="out" />
<feColorMatrix values="0 0 0 0 0.146667 0 0 0 0 0.183333 0 0 0 0 0.157752 0 0 0 0.25 0" />
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow_5_1510" />
<feBlend in="SourceGraphic" in2="effect1_dropShadow_5_1510" result="shape" />
</filter>
</defs>
</svg>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div class="card bg-base-100 shadow-xl sm:rounded-lg">
<div class="card bg-white shadow-xl sm:rounded-lg">
<div v-if="$slots.title" class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6">
<slot name="title"></slot>

View file

@ -1,7 +1,7 @@
<script setup lang="ts">
import { LabelOut, LabelSummary } from "~~/lib/api/types/data-contracts";
export type sizes = "sm" | "md" | "lg";
export type sizes = "sm" | "md" | "lg" | "xl";
defineProps({
label: {
type: Object as () => LabelOut | LabelSummary,
@ -23,9 +23,10 @@
<template>
<NuxtLink
ref="badge"
class="badge"
class="badge badge-secondary"
:class="{
'p-3': size !== 'sm',
'badge-lg p-4': size === 'lg',
'p-3': size !== 'sm' && size !== 'lg',
'p-2 badge-sm': size === 'sm',
}"
:to="`/label/${label.id}`"

View file

@ -2,7 +2,7 @@
<NuxtLink
ref="card"
:to="`/location/${location.id}`"
class="card bg-primary text-primary-content transition duration-300"
class="card bg-white text-neutral rounded-md transition duration-300 shadow-md"
>
<div
class="card-body"
@ -11,13 +11,15 @@
'py-2 px-3': dense,
}"
>
<h2 class="flex items-center gap-2">
<h2 class="flex items-center justify-between 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" />
<Icon name="heroicons-arrow-right" class="swap-on h-6 w-6" />
<Icon name="heroicons-map-pin" class="swap-off h-6 w-6" />
</label>
{{ location.name }}
<span v-if="hasCount" class="badge badge-secondary badge-lg ml-auto text-secondary-content"> {{ count }}</span>
<span class="mx-auto">
{{ location.name }}
</span>
<span v-if="hasCount" class="badge badge-primary h-6 w-6 badge-lg"> {{ count }}</span>
</h2>
</div>
</NuxtLink>

View file

@ -3,12 +3,11 @@
</template>
<script setup lang="ts">
const props = defineProps({
amount: {
type: String,
required: true,
},
});
type Props = {
amount: string | number;
};
const props = defineProps<Props>();
const fmt = await useFormatCurrency();

View file

@ -0,0 +1,27 @@
<template>
<div class="stats bg-neutral shadow rounded-md">
<div class="stat text-neutral-content text-center space-y-1 p-3">
<div class="stat-title">{{ title }}</div>
<div class="stat-value text-2xl">
<Currency v-if="type === 'currency'" :amount="value" />
<template v-if="type === 'number'">{{ value }}</template>
</div>
<div v-if="subtitle" class="stat-desc">{{ subtitle }}</div>
</div>
</div>
</template>
<script setup lang="ts">
type format = "currency" | "number" | "percent";
type Props = {
title: string;
value: number;
subtitle?: string;
type?: format;
};
withDefaults(defineProps<Props>(), {
type: "number",
});
</script>

View file

@ -0,0 +1,5 @@
<template>
<h3 class="flex gap-2 items-center mb-3 pl-1 text-lg">
<slot />
</h3>
</template>

View file

@ -0,0 +1,8 @@
export type TableHeader = {
text: string;
value: string;
sortable?: boolean;
align?: "left" | "center" | "right";
};
export type TableData = Record<string, any>;

View file

@ -0,0 +1,68 @@
<template>
<div class="overflow-x-auto">
<table class="table w-full">
<thead>
<tr class="bg-primary">
<th
v-for="h in headers"
:key="h.value"
class="text-no-transform text-sm bg-neutral text-neutral-content"
:class="{
'text-center': h.align === 'center',
'text-right': h.align === 'right',
'text-left': h.align === 'left',
}"
>
<template v-if="typeof h === 'string'">{{ h }}</template>
<template v-else>{{ h.text }}</template>
</th>
</tr>
</thead>
<tbody>
<!-- row 1 -->
<tr v-for="(d, i) in data" :key="i">
<td
v-for="h in headers"
:key="`${h.value}-${i}`"
class="bg-white"
:class="{
'text-center': h.align === 'center',
'text-right': h.align === 'right',
'text-left': h.align === 'left',
}"
>
<slot :name="cell(h)" v-bind="{ item: d }">
{{ extractValue(d, h.value) }}
</slot>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { TableData, TableHeader } from "./Table.types";
type Props = {
headers: TableHeader[];
data: TableData[];
};
function extractValue(data: TableData, value: string) {
const parts = value.split(".");
let current = data;
for (const part of parts) {
current = current[part];
}
return current;
}
function cell(h: TableHeader) {
return `cell-${h.value.replace(".", "_")}`;
}
defineProps<Props>();
</script>
<style scoped></style>

View file

@ -32,6 +32,13 @@ export function useConfirm(): Store {
}
async function openDialog(msg: string): Promise<UseConfirmDialogRevealResult<boolean, boolean>> {
if (!store.reveal) {
throw new Error("reveal is not defined");
}
if (!store.text) {
throw new Error("text is not defined");
}
store.text.value = msg;
return await store.reveal();
}

View file

@ -1,5 +1,3 @@
import { ComputedRef } from "vue";
type ColorType = "hsla";
export type VarOptions = {
@ -8,15 +6,107 @@ export type VarOptions = {
apply?: (value: string) => string;
};
export function useCssVar(name: string, options?: VarOptions): ComputedRef<string> {
export type Breakpoints = {
sm: boolean;
md: boolean;
lg: boolean;
xl: boolean;
xxl: boolean;
};
export function useBreakpoints(): Breakpoints {
const breakpoints: Breakpoints = reactive({
sm: false,
md: false,
lg: false,
xl: false,
xxl: false,
});
const updateBreakpoints = () => {
breakpoints.sm = window.innerWidth < 640;
breakpoints.md = window.innerWidth >= 640;
breakpoints.lg = window.innerWidth >= 768;
breakpoints.xl = window.innerWidth >= 1024;
breakpoints.xxl = window.innerWidth >= 1280;
};
onMounted(() => {
updateBreakpoints();
window.addEventListener("resize", updateBreakpoints);
});
onUnmounted(() => {
window.removeEventListener("resize", updateBreakpoints);
});
return breakpoints;
}
class ThemeObserver {
// eslint-disable-next-line no-use-before-define
private static instance?: ThemeObserver;
private readonly observer: MutationObserver;
private fns: (() => void)[] = [];
private constructor() {
this.observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName === "data-theme") {
this.fire();
}
});
});
const html = document.querySelector("html");
if (!html) {
throw new Error("No html element found");
}
this.observer.observe(html, { attributes: true });
}
public static getInstance() {
if (!ThemeObserver.instance) {
ThemeObserver.instance = new ThemeObserver();
}
return ThemeObserver.instance;
}
private fire() {
this.fns.forEach(fn => fn());
}
public add(fn: () => void) {
this.fns.push(fn);
}
public remove(fn: () => void) {
this.fns = this.fns.filter(f => f !== fn);
}
}
export function useCssVar(name: string, options?: VarOptions) {
if (!options) {
options = {
type: "hsla",
transparency: 1,
apply: null,
apply: undefined,
};
}
const cssVal = ref(getComputedStyle(document.documentElement).getPropertyValue(name).trim());
const update = () => {
cssVal.value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
};
ThemeObserver.getInstance().add(update);
onUnmounted(() => {
ThemeObserver.getInstance().remove(update);
});
switch (options.type) {
case "hsla": {
return computed(() => {
@ -24,13 +114,13 @@ export function useCssVar(name: string, options?: VarOptions): ComputedRef<strin
return "";
}
let val = getComputedStyle(document.documentElement).getPropertyValue(name);
val = val.trim().split(" ").join(", ");
if (options.transparency) {
let val = cssVal.value.trim().split(" ").join(", ");
if (options?.transparency) {
val += `, ${options.transparency}`;
}
console.log(`hsla(${val})`);
return `hsla(${val})`;
});
}

View file

@ -1,10 +1,62 @@
<template>
<div>
<AppToast />
<AppHeader />
<main>
<slot></slot>
</main>
<div class="drawer drawer-mobile">
<input id="my-drawer-2" type="checkbox" class="drawer-toggle" />
<div class="drawer-content justify-center">
<AppHeaderDecor class="-mt-10" />
<slot></slot>
<!-- Button -->
<label for="my-drawer-2" class="btn btn-primary drawer-button lg:hidden">Open drawer</label>
</div>
<!-- Sidebar -->
<div class="drawer-side shadow-lg w-60 flex flex-col justify-center py-10" style="background: white">
<label for="my-drawer-2" class="drawer-overlay"></label>
<!-- Top Section -->
<div class="space-y-8">
<div class="flex flex-col items-center gap-4">
<p>Kotelman House</p>
<NuxtLink class="avatar placeholder" to="/home">
<div class="bg-base-100 text-neutral-content rounded-full w-36">
<span class="text-6xl text-base-content">HK</span>
</div>
</NuxtLink>
</div>
<div class="flex flex-col">
<div class="mx-auto w-40 mb-6">
<BaseButton class="btn-block btn-primary text-xl">
<template #icon>
<Icon name="mdi-plus-circle" class="h-6 w-6" />
</template>
Create
</BaseButton>
</div>
<ul class="flex flex-col mx-auto gap-y-8">
<li v-for="n in nav" :key="n.id" class="text-xl">
<NuxtLink v-if="n.to" :to="n.to">
<span class="mr-4">
<Icon :name="n.icon" class="h-5 w-5" />
</span>
{{ n.name }}
</NuxtLink>
<button v-else @click="n.action">
<span class="mr-4">
<Icon :name="n.icon" class="h-5 w-5" />
</span>
{{ n.name }}
</button>
</li>
</ul>
</div>
</div>
<!-- Bottom -->
<button class="mt-auto mb-6">Sign Out</button>
</div>
</div>
</div>
</template>
@ -16,6 +68,33 @@
* Store Provider Initialization
*/
const nav = [
{
icon: "mdi-account",
id: 1,
name: "Profile",
to: "/profile",
},
{
icon: "mdi-document",
id: 3,
name: "Items",
to: "/items",
},
{
icon: "mdi-database",
id: 2,
name: "Import",
action: () => {},
},
{
icon: "mdi-database-export",
id: 5,
name: "Export",
action: () => {},
},
];
const labelStore = useLabelStore();
const reLabel = /\/api\/v1\/labels\/.*/gm;
const rmLabelStoreObserver = defineObserver("labelStore", {

View file

@ -120,6 +120,8 @@ export interface ItemSummary {
/** Edges */
location: LocationSummary | null;
name: string;
/** @example "0" */
purchasePrice: string;
quantity: number;
updatedAt: Date;
}

View file

@ -1,4 +1,5 @@
export type DaisyTheme =
| "homebox"
| "light"
| "dark"
| "cupcake"
@ -35,6 +36,10 @@ export type ThemeOption = {
};
export const themes: ThemeOption[] = [
{
label: "Homebox",
value: "homebox",
},
{
label: "Garden",
value: "garden",

View file

@ -9,8 +9,9 @@ export default defineNuxtConfig({
"/api": {
target: "http://localhost:7745/api",
changeOrigin: true,
}
},
},
},
plugins: [],
css: ["@/assets/css/main.css"],
plugins: [],
});

View file

@ -1,241 +0,0 @@
<script setup lang="ts">
import { useAuthStore } from "~~/stores/auth";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
definePageMeta({
middleware: ["auth"],
});
useHead({
title: "Homebox | Home",
});
const api = useUserApi();
const auth = useAuthStore();
const locationStore = useLocationStore();
const locations = computed(() => locationStore.parentLocations);
const labelsStore = useLabelStore();
const labels = computed(() => labelsStore.labels);
const { data: statistics } = useAsyncData(async () => {
const { data } = await api.stats.group();
return data;
});
const stats = computed(() => {
return [
{
label: "Locations",
value: statistics.value?.totalLocations || 0,
},
{
label: "Items",
value: statistics.value?.totalItems || 0,
},
{
label: "Labels",
value: statistics.value?.totalLabels || 0,
},
];
});
const importDialog = ref(false);
const importCsv = ref(null);
const importLoading = ref(false);
const importRef = ref<HTMLInputElement>();
whenever(
() => !importDialog.value,
() => {
importCsv.value = null;
}
);
function setFile(e: Event & { target: HTMLInputElement }) {
importCsv.value = e.target.files[0];
}
const toast = useNotifier();
function openDialog() {
importDialog.value = true;
}
function uploadCsv() {
importRef.value.click();
}
const eventBus = useEventBus();
async function submitCsvFile() {
importLoading.value = true;
const { error } = await api.items.import(importCsv.value);
if (error) {
toast.error("Import failed. Please try again later.");
}
// Reset
importDialog.value = false;
importLoading.value = false;
importCsv.value = null;
importRef.value.value = null;
eventBus.emit(EventTypes.ClearStores);
}
const { data: timeseries } = useAsyncData(async () => {
const { data } = await api.stats.totalPriceOverTime();
return data;
});
const primary = useCssVar("--p");
const secondary = useCssVar("--s");
const accent = useCssVar("--a");
const neutral = useCssVar("--n");
const base = useCssVar("--b");
const chartData = computed(() => {
let start = timeseries.value?.valueAtStart;
return {
labels: timeseries?.value.entries.map(t => new Date(t.date).toDateString()) || [],
datasets: [
{
label: "Purchase Price",
data:
timeseries.value?.entries.map(t => {
start += t.value;
return start;
}) || [],
backgroundColor: primary.value,
borderColor: primary.value,
},
],
};
});
const { data: donutSeries } = useAsyncData(async () => {
const { data } = await api.stats.locations();
return data;
});
const donutData = computed(() => {
return {
labels: donutSeries.value?.map(l => l.name) || [],
datasets: [
{
label: "Value",
data: donutSeries.value?.map(l => l.total) || [],
backgroundColor: [primary.value, secondary.value, neutral.value],
borderColor: [primary.value, secondary.value, neutral.value],
hoverOffset: 4,
},
],
};
});
const refDonutEl = ref<HTMLDivElement>(null);
const donutElWidth = computed(() => {
return refDonutEl.value?.clientWidth || 0;
});
</script>
<template>
<div>
<BaseModal v-model="importDialog">
<template #title> Import CSV File </template>
<p>
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
required format.
</p>
<form @submit.prevent="submitCsvFile">
<div class="flex flex-col gap-2 py-6">
<input ref="importRef" type="file" class="hidden" accept=".csv,.tsv" @change="setFile" />
<BaseButton type="button" @click="uploadCsv">
<Icon class="h-5 w-5 mr-2" name="mdi-upload" />
Upload
</BaseButton>
<p class="text-center pt-4 -mb-5">
{{ importCsv?.name }}
</p>
</div>
<div class="modal-action">
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
</div>
</form>
</BaseModal>
<BaseContainer class="flex flex-col gap-16 pb-16">
<section>
<BaseCard>
<template #title> Welcome Back, {{ auth.self ? auth.self.name : "Username" }} </template>
<!-- <template #subtitle> {{ auth.self.isSuperuser ? "Admin" : "User" }} </template> -->
<template #title-actions>
<div class="flex justify-end gap-2">
<div class="tooltip" data-tip="Import CSV File">
<button class="btn btn-primary btn-sm" @click="openDialog">
<Icon name="mdi-database" class="mr-2"></Icon>
Import
</button>
</div>
<BaseButton type="button" size="sm" to="/profile">
<Icon class="h-5 w-5 mr-2" name="mdi-person" />
Profile
</BaseButton>
</div>
</template>
<div
class="grid grid-cols-1 divide-y divide-base-300 border-t border-base-300 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-base-900 font-bold">{{ stat.value }}</span>
{{ " " }}
<span class="text-base-600">{{ stat.label }}</span>
</div>
</div>
</BaseCard>
</section>
<section v-if="timeseries" class="grid grid-cols-6 gap-6">
<BaseCard class="col-span-4">
<template #title>Total Asset Value {{ fmtCurrency(timeseries.valueAtEnd) }}</template>
<div class="p-6 pt-0">
<ClientOnly>
<ChartLine chart-id="asd" :height="200" :chart-data="chartData" />
</ClientOnly>
</div>
</BaseCard>
<BaseCard class="col-span-2">
<template #title> Asset By Location {{ fmtCurrency(timeseries.valueAtEnd) }}</template>
<div ref="refDonutEl" class="grid place-content-center h-full">
<ClientOnly>
<ChartDonut chart-id="donut" :width="donutElWidth - 50" :height="300" :chart-data="donutData" />
</ClientOnly>
</div>
</BaseCard>
</section>
<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" :key="location.id" :location="location" />
</div>
</section>
<section>
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
<div class="flex gap-2 flex-wrap">
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" />
</div>
</section>
</BaseContainer>
</div>
</template>

View file

@ -0,0 +1,71 @@
import { TChartData } from "vue-chartjs/dist/types";
import { UserClient } from "~~/lib/api/user";
export function purchasePriceOverTimeChart(api: UserClient) {
const { data: timeseries } = useAsyncData(async () => {
const { data } = await api.stats.totalPriceOverTime();
return data;
});
const primary = useCssVar("--p");
return computed(() => {
if (!timeseries.value) {
return {
labels: ["Purchase Price"],
datasets: [
{
label: "Purchase Price",
data: [],
backgroundColor: primary.value,
borderColor: primary.value,
},
],
} as TChartData<"line", number[], unknown>;
}
let start = timeseries.value?.valueAtStart;
return {
labels: timeseries?.value.entries.map(t => new Date(t.date).toDateString()) || [],
datasets: [
{
label: "Purchase Price",
data:
timeseries.value?.entries.map(t => {
start += t.value;
return start;
}) || [],
backgroundColor: primary.value,
borderColor: primary.value,
},
],
} as TChartData<"line", number[], unknown>;
});
}
export function inventoryByLocationChart(api: UserClient) {
const { data: donutSeries } = useAsyncData(async () => {
const { data } = await api.stats.locations();
return data;
});
const primary = useCssVar("--p");
const secondary = useCssVar("--s");
const neutral = useCssVar("--n");
return computed(() => {
return {
labels: donutSeries.value?.map(l => l.name) || [],
datasets: [
{
label: "Value",
data: donutSeries.value?.map(l => l.total) || [],
backgroundColor: [primary.value, secondary.value, neutral.value],
borderColor: [primary.value, secondary.value, neutral.value],
hoverOffset: 4,
},
],
};
});
}

View file

@ -0,0 +1,198 @@
<script setup lang="ts">
import { statCardData } from "./statistics";
import { itemsTable } from "./table";
import { inventoryByLocationChart, purchasePriceOverTimeChart } from "./charts";
import { useLabelStore } from "~~/stores/labels";
import { useLocationStore } from "~~/stores/locations";
definePageMeta({
middleware: ["auth"],
});
useHead({
title: "Homebox | Home",
});
const api = useUserApi();
const breakpoints = useBreakpoints();
const locationStore = useLocationStore();
const locations = computed(() => locationStore.parentLocations);
const labelsStore = useLabelStore();
const labels = computed(() => labelsStore.labels);
const itemTable = itemsTable(api);
const stats = statCardData(api);
// const importDialog = ref(false);
// const importCsv = ref(null);
// const importLoading = ref(false);
// const importRef = ref<HTMLInputElement>();
// whenever(
// () => !importDialog.value,
// () => {
// importCsv.value = null;
// }
// );
// function setFile(e: Event & { target: HTMLInputElement }) {
// importCsv.value = e.target.files[0];
// }
// function openDialog() {
// importDialog.value = true;
// }
// function uploadCsv() {
// importRef.value.click();
// }
// const eventBus = useEventBus();
// async function submitCsvFile() {
// if (!importCsv.value) {
// toast.error("Please select a file to import.");
// return;
// }
// importLoading.value = true;
// const { error } = await api.items.import(importCsv.value);
// if (error) {
// toast.error("Import failed. Please try again later.");
// }
// // Reset
// importDialog.value = false;
// importLoading.value = false;
// importCsv.value = null;
// if (importRef.value) {
// importRef.value.value = "";
// }
// eventBus.emit(EventTypes.ClearStores);
// }
const purchasePriceOverTime = purchasePriceOverTimeChart(api);
const inventoryByLocation = inventoryByLocationChart(api);
const refDonutEl = ref<HTMLDivElement>();
const donutElWidth = computed(() => {
return refDonutEl.value?.clientWidth || 0;
});
</script>
<template>
<div>
<!-- <BaseModal v-model="importDialog">
<template #title> Import CSV File </template>
<p>
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
required format.
</p>
<form @submit.prevent="submitCsvFile">
<div class="flex flex-col gap-2 py-6">
<input ref="importRef" type="file" class="hidden" accept=".csv,.tsv" @change="setFile" />
<BaseButton type="button" @click="uploadCsv">
<Icon class="h-5 w-5 mr-2" name="mdi-upload" />
Upload
</BaseButton>
<p class="text-center pt-4 -mb-5">
{{ importCsv?.name }}
</p>
</div>
<div class="modal-action">
<BaseButton type="submit" :disabled="!importCsv"> Submit </BaseButton>
</div>
</form>
</BaseModal> -->
<BaseContainer class="flex flex-col gap-12 pb-16">
<section v-if="breakpoints.lg" class="grid grid-cols-6 gap-6">
<article class="col-span-4">
<Subtitle> Inventory Value Over Time </Subtitle>
<BaseCard>
<div class="p-6 pt-0">
<ClientOnly>
<ChartLine chart-id="asd" :height="140" :chart-data="purchasePriceOverTime" />
</ClientOnly>
</div>
</BaseCard>
</article>
<article class="col-span-2 max-h-[100px]">
<Subtitle>
Inventory By
<span class="btn-group">
<button class="btn btn-xs btn-active text-no-transform">Locations</button>
<button class="btn btn-xs text-no-transform">Labels</button>
</span>
</Subtitle>
<BaseCard>
<div ref="refDonutEl" class="grid place-content-center h-full">
<ClientOnly>
<ChartDonut
chart-id="donut"
:width="donutElWidth - 50"
:height="265"
:chart-data="inventoryByLocation"
/>
</ClientOnly>
</div>
</BaseCard>
</article>
</section>
<section>
<Subtitle> Quick Statistics </Subtitle>
<div class="grid grid-cols-2 gap-2 md:grid-cols-4 md:gap-6">
<StatCard v-for="(stat, i) in stats" :key="i" :title="stat.label" :value="stat.value" :type="stat.type" />
</div>
</section>
<section>
<Subtitle> Recently Added </Subtitle>
<BaseCard v-if="breakpoints.lg">
<Table :headers="itemTable.headers" :data="itemTable.items">
<template #cell-warranty="{ item }">
<Icon v-if="item.warranty" name="mdi-check" class="text-green-500 h-5 w-5" />
<Icon v-else name="mdi-close" class="text-red-500 h-5 w-5" />
</template>
<template #cell-purchasePrice="{ item }">
<Currency :amount="item.purchasePrice" />
</template>
<template #cell-location_Name="{ item }">
<NuxtLink class="badge badge-sm badge-primary p-3" :to="`/location/${item.location.id}`">
{{ item.location?.name }}
</NuxtLink>
</template>
</Table>
</BaseCard>
<div v-else class="grid grid-cols-1 md:grid-cols-2 gap-4">
<ItemCard v-for="item in itemTable.items" :key="item.id" :item="item" />
</div>
</section>
<section>
<Subtitle> Storage Locations </Subtitle>
<div class="grid grid-cols-1 sm:grid-cols-2 card md:grid-cols-3 gap-4">
<LocationCard v-for="location in locations" :key="location.id" :location="location" />
</div>
</section>
<section>
<Subtitle> Labels </Subtitle>
<div class="flex gap-4 flex-wrap">
<LabelChip v-for="label in labels" :key="label.id" size="lg" :label="label" class="shadow-md" />
</div>
</section>
</BaseContainer>
</div>
</template>

View file

@ -0,0 +1,39 @@
import { UserClient } from "~~/lib/api/user";
type StatCard = {
label: string;
value: number;
type: "currency" | "number";
};
export function statCardData(api: UserClient) {
const { data: statistics } = useAsyncData(async () => {
const { data } = await api.stats.group();
return data;
});
return computed(() => {
return [
{
label: "Total Value",
value: statistics.value?.totalItemPrice || 0,
type: "currency",
},
{
label: "Total Items",
value: statistics.value?.totalItems || 0,
type: "number",
},
{
label: "Total Locations",
value: statistics.value?.totalLocations || 0,
type: "number",
},
{
label: "Total Labels",
value: statistics.value?.totalLabels || 0,
type: "number",
},
] as StatCard[];
});
}

View file

@ -0,0 +1,42 @@
import { TableHeader } from "~~/components/global/Table.types";
import { UserClient } from "~~/lib/api/user";
export function itemsTable(api: UserClient) {
const { data: items } = useAsyncData(async () => {
const { data } = await api.items.getAll({
page: 1,
pageSize: 5,
});
return data.items;
});
const headers = [
{
text: "Name",
sortable: true,
value: "name",
},
{
text: "Location",
value: "location.name",
},
{
text: "Warranty",
value: "warranty",
align: "center",
},
{
text: "Price",
value: "purchasePrice",
align: "center",
},
] as TableHeader[];
return computed(() => {
return {
headers,
items: items.value || [],
};
});
}

View file

@ -4,8 +4,56 @@ module.exports = {
theme: {
extend: {},
},
daisyui: {
themes: [
{
homebox: {
primary: "#5C7F67",
secondary: "#ECF4E7",
accent: "#FFDA56",
neutral: "#2C2E27",
"base-100": "#F6FAFB",
info: "#3ABFF8",
success: "#36D399",
warning: "#FBBD23",
error: "#F87272",
},
},
"light",
"dark",
"cupcake",
"bumblebee",
"emerald",
"corporate",
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"garden",
"forest",
"aqua",
"lofi",
"pastel",
"fantasy",
"wireframe",
"black",
"luxury",
"dracula",
"cmyk",
"autumn",
"business",
"acid",
"lemonade",
"night",
"coffee",
"winter",
],
},
variants: {
extend: {},
},
plugins: [require("@tailwindcss/aspect-ratio"), require("@tailwindcss/typography"), require("daisyui")],
};
const defaults = [];