labels create and get

This commit is contained in:
Hayden 2022-09-01 17:52:40 -08:00
parent f956ec8eb2
commit 8ece3bd7bf
24 changed files with 850 additions and 132 deletions

View file

@ -1273,13 +1273,70 @@ const docTemplate = `{
} }
}, },
"types.LabelCreate": { "types.LabelCreate": {
"type": "object" "type": "object",
"properties": {
"color": {
"type": "string"
},
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
}, },
"types.LabelOut": { "types.LabelOut": {
"type": "object" "type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"groupId": {
"type": "string"
},
"id": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/types.ItemSummary"
}
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
}, },
"types.LabelSummary": { "types.LabelSummary": {
"type": "object" "type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"groupId": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
}, },
"types.LocationCreate": { "types.LocationCreate": {
"type": "object", "type": "object",

View file

@ -1265,13 +1265,70 @@
} }
}, },
"types.LabelCreate": { "types.LabelCreate": {
"type": "object" "type": "object",
"properties": {
"color": {
"type": "string"
},
"description": {
"type": "string"
},
"name": {
"type": "string"
}
}
}, },
"types.LabelOut": { "types.LabelOut": {
"type": "object" "type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"groupId": {
"type": "string"
},
"id": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/types.ItemSummary"
}
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
}, },
"types.LabelSummary": { "types.LabelSummary": {
"type": "object" "type": "object",
"properties": {
"createdAt": {
"type": "string"
},
"description": {
"type": "string"
},
"groupId": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"updatedAt": {
"type": "string"
}
}
}, },
"types.LocationCreate": { "types.LocationCreate": {
"type": "object", "type": "object",

View file

@ -354,10 +354,47 @@ definitions:
type: string type: string
type: object type: object
types.LabelCreate: types.LabelCreate:
properties:
color:
type: string
description:
type: string
name:
type: string
type: object type: object
types.LabelOut: types.LabelOut:
properties:
createdAt:
type: string
description:
type: string
groupId:
type: string
id:
type: string
items:
items:
$ref: '#/definitions/types.ItemSummary'
type: array
name:
type: string
updatedAt:
type: string
type: object type: object
types.LabelSummary: types.LabelSummary:
properties:
createdAt:
type: string
description:
type: string
groupId:
type: string
id:
type: string
name:
type: string
updatedAt:
type: string
type: object type: object
types.LocationCreate: types.LocationCreate:
properties: properties:

View file

@ -2,6 +2,10 @@ package v1
import ( import (
"net/http" "net/http"
"github.com/hay-kot/content/backend/internal/services"
"github.com/hay-kot/content/backend/internal/types"
"github.com/hay-kot/content/backend/pkgs/server"
) )
// HandleLabelsGetAll godoc // HandleLabelsGetAll godoc
@ -13,6 +17,14 @@ import (
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc { func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
user := services.UseUserCtx(r.Context())
labels, err := ctrl.svc.Labels.GetAll(r.Context(), user.GroupID)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusOK, server.Results{Items: labels})
} }
} }
@ -26,6 +38,23 @@ func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc { func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
createData := types.LabelCreate{}
if err := server.Decode(r, &createData); err != nil {
ctrl.log.Error(err, nil)
server.RespondError(w, http.StatusInternalServerError, err)
return
}
user := services.UseUserCtx(r.Context())
label, err := ctrl.svc.Labels.Create(r.Context(), user.GroupID, createData)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusCreated, label)
} }
} }
@ -39,6 +68,18 @@ func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc { func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
uid, user, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
err = ctrl.svc.Labels.Delete(r.Context(), user.GroupID, uid)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusNoContent, nil)
} }
} }
@ -52,6 +93,18 @@ func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc { func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
uid, user, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
labels, err := ctrl.svc.Labels.Get(r.Context(), user.GroupID, uid)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusOK, labels)
} }
} }
@ -65,5 +118,24 @@ func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
// @Security Bearer // @Security Bearer
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc { func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
body := types.LabelUpdate{}
if err := server.Decode(r, &body); err != nil {
ctrl.log.Error(err, nil)
server.RespondError(w, http.StatusInternalServerError, err)
return
}
uid, user, err := ctrl.partialParseIdAndUser(w, r)
if err != nil {
return
}
body.ID = uid
result, err := ctrl.svc.Labels.Update(r.Context(), user.GroupID, body)
if err != nil {
ctrl.log.Error(err, nil)
server.RespondServerError(w)
return
}
server.Respond(w, http.StatusOK, result)
} }
} }

View file

@ -0,0 +1,60 @@
package repo
import (
"context"
"github.com/google/uuid"
"github.com/hay-kot/content/backend/ent"
"github.com/hay-kot/content/backend/ent/group"
"github.com/hay-kot/content/backend/ent/label"
"github.com/hay-kot/content/backend/internal/types"
)
type EntLabelRepository struct {
db *ent.Client
}
func (r *EntLabelRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Label, error) {
return r.db.Label.Query().
Where(label.ID(ID)).
WithGroup().
WithItems().
Only(ctx)
}
func (r *EntLabelRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Label, error) {
return r.db.Label.Query().
Where(label.HasGroupWith(group.ID(groupId))).
WithGroup().
All(ctx)
}
func (r *EntLabelRepository) Create(ctx context.Context, groupdId uuid.UUID, data types.LabelCreate) (*ent.Label, error) {
label, err := r.db.Label.Create().
SetName(data.Name).
SetDescription(data.Description).
SetColor(data.Color).
SetGroupID(groupdId).
Save(ctx)
label.Edges.Group = &ent.Group{ID: groupdId} // bootstrap group ID
return label, err
}
func (r *EntLabelRepository) Update(ctx context.Context, data types.LabelUpdate) (*ent.Label, error) {
_, err := r.db.Label.UpdateOneID(data.ID).
SetName(data.Name).
SetDescription(data.Description).
SetColor(data.Color).
Save(ctx)
if err != nil {
return nil, err
}
return r.Get(ctx, data.ID)
}
func (r *EntLabelRepository) Delete(ctx context.Context, id uuid.UUID) error {
return r.db.Label.DeleteOneID(id).Exec(ctx)
}

View file

@ -8,6 +8,7 @@ type AllRepos struct {
AuthTokens *EntTokenRepository AuthTokens *EntTokenRepository
Groups *EntGroupRepository Groups *EntGroupRepository
Locations *EntLocationRepository Locations *EntLocationRepository
Labels *EntLabelRepository
} }
func EntAllRepos(db *ent.Client) *AllRepos { func EntAllRepos(db *ent.Client) *AllRepos {
@ -16,5 +17,6 @@ func EntAllRepos(db *ent.Client) *AllRepos {
AuthTokens: &EntTokenRepository{db}, AuthTokens: &EntTokenRepository{db},
Groups: &EntGroupRepository{db}, Groups: &EntGroupRepository{db},
Locations: &EntLocationRepository{db}, Locations: &EntLocationRepository{db},
Labels: &EntLabelRepository{db},
} }
} }

View file

@ -6,6 +6,7 @@ type AllServices struct {
User *UserService User *UserService
Admin *AdminService Admin *AdminService
Location *LocationService Location *LocationService
Labels *LabelService
} }
func NewServices(repos *repo.AllRepos) *AllServices { func NewServices(repos *repo.AllRepos) *AllServices {
@ -13,5 +14,6 @@ func NewServices(repos *repo.AllRepos) *AllServices {
User: &UserService{repos}, User: &UserService{repos},
Admin: &AdminService{repos}, Admin: &AdminService{repos},
Location: &LocationService{repos}, Location: &LocationService{repos},
Labels: &LabelService{repos},
} }
} }

View file

@ -0,0 +1,32 @@
package mappers
import (
"github.com/hay-kot/content/backend/ent"
"github.com/hay-kot/content/backend/internal/types"
)
func ToLabelSummary(label *ent.Label) *types.LabelSummary {
return &types.LabelSummary{
ID: label.ID,
GroupID: label.Edges.Group.ID,
Name: label.Name,
Description: label.Description,
CreatedAt: label.CreatedAt,
UpdatedAt: label.UpdatedAt,
}
}
func ToLabelSummaryErr(label *ent.Label, err error) (*types.LabelSummary, error) {
return ToLabelSummary(label), err
}
func ToLabelOut(label *ent.Label) *types.LabelOut {
return &types.LabelOut{
LabelSummary: *ToLabelSummary(label),
Items: MapEach(label.Edges.Items, ToItemSummary),
}
}
func ToLabelOutErr(label *ent.Label, err error) (*types.LabelOut, error) {
return ToLabelOut(label), err
}

View file

@ -0,0 +1,63 @@
package services
import (
"context"
"github.com/google/uuid"
"github.com/hay-kot/content/backend/internal/repo"
"github.com/hay-kot/content/backend/internal/services/mappers"
"github.com/hay-kot/content/backend/internal/types"
)
type LabelService struct {
repos *repo.AllRepos
}
func (svc *LabelService) Create(ctx context.Context, groupId uuid.UUID, data types.LabelCreate) (*types.LabelSummary, error) {
label, err := svc.repos.Labels.Create(ctx, groupId, data)
return mappers.ToLabelSummaryErr(label, err)
}
func (svc *LabelService) Update(ctx context.Context, groupId uuid.UUID, data types.LabelUpdate) (*types.LabelSummary, error) {
label, err := svc.repos.Labels.Update(ctx, data)
return mappers.ToLabelSummaryErr(label, err)
}
func (svc *LabelService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error {
label, err := svc.repos.Labels.Get(ctx, id)
if err != nil {
return err
}
if label.Edges.Group.ID != groupId {
return ErrNotOwner
}
return svc.repos.Labels.Delete(ctx, id)
}
func (svc *LabelService) Get(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (*types.LabelOut, error) {
label, err := svc.repos.Labels.Get(ctx, id)
if err != nil {
return nil, err
}
if label.Edges.Group.ID != groupId {
return nil, ErrNotOwner
}
return mappers.ToLabelOut(label), nil
}
func (svc *LabelService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LabelSummary, error) {
labels, err := svc.repos.Labels.GetAll(ctx, groupId)
if err != nil {
return nil, err
}
labelsOut := make([]*types.LabelSummary, len(labels))
for i, label := range labels {
labelsOut[i] = mappers.ToLabelSummary(label)
}
return labelsOut, nil
}

View file

@ -1,7 +1,34 @@
package types package types
type LabelOut struct{} import (
"time"
type LabelCreate struct{} "github.com/google/uuid"
)
type LabelSummary struct{} type LabelCreate struct {
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
type LabelUpdate struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Color string `json:"color"`
}
type LabelSummary struct {
ID uuid.UUID `json:"id"`
GroupID uuid.UUID `json:"groupId"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type LabelOut struct {
LabelSummary
Items []*ItemSummary `json:"items"`
}

View file

@ -25,79 +25,50 @@
}, },
]; ];
const modals = reactive({
location: false,
label: false,
item: false,
});
const dropdown = [ const dropdown = [
{ {
name: 'Location', name: 'Location',
action: () => { action: () => {
modal.value = true; modals.location = true;
}, },
}, },
{ {
name: 'Item / Asset', name: 'Item / Asset',
action: () => {}, action: () => {
modals.item = true;
},
}, },
{ {
name: 'Label', name: 'Label',
action: () => {}, action: () => {
modals.label = true;
},
}, },
]; ];
// ----------------------------
// Location Stuff
// Should move to own component
const locationLoading = ref(false);
const locationForm = reactive({
name: '',
description: '',
});
const locationNameRef = ref(null);
const triggerFocus = ref(false);
const modal = ref(false);
whenever(
() => modal.value,
() => {
triggerFocus.value = true;
}
);
async function createLocation() {
locationLoading.value = true;
const { data } = await api.locations.create(locationForm);
if (data) {
navigateTo(`/location/${data.id}`);
}
locationLoading.value = false;
modal.value = false;
locationForm.name = '';
locationForm.description = '';
triggerFocus.value = false;
}
</script> </script>
<template> <template>
<!--
Confirmation Modal is a singleton used by all components so we render
it here to ensure it's always available. Possibly could move this further
up the tree
-->
<ModalConfirm /> <ModalConfirm />
<BaseModal v-model="modal"> <LabelCreateModal v-model="modals.label" />
<template #title> Create Location </template> <LocationCreateModal v-model="modals.location" />
<form @submit.prevent="createLocation">
<FormTextField
:trigger-focus="triggerFocus"
ref="locationNameRef"
:autofocus="true"
label="Location Name"
v-model="locationForm.name"
/>
<FormTextField label="Location Description" v-model="locationForm.description" />
<div class="modal-action">
<BaseButton type="submit" :loading="locationLoading"> Create </BaseButton>
</div>
</form>
</BaseModal>
<BaseContainer is="header" class="py-6"> <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">Homebox</h2> <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"> <div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
<template v-for="link in links"> <template v-for="link in links">
<NuxtLink <NuxtLink

View file

@ -0,0 +1,123 @@
<template>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 596.5055138004384 585.369487986598">
<g
stroke-linecap="round"
transform="translate(437.568672588907 210.93877417794465) rotate(332.3235338946895 66.970006481548 27.559467997664797)"
>
<path
d="M-0.3 -0.89 L131.27 -1.27 L131.05 52.32 L-2.89 53.3"
stroke="none"
stroke-width="0"
fill="#15aabf"
></path>
<path
d="M1.61 2.92 C34.39 0.43, 67.49 -3.76, 136.43 -0.16 M-1.81 1.81 C54.26 1.13, 105.28 -0.86, 133.28 -0.04 M132.66 3.06 C133.92 12.97, 132.16 31.97, 132.92 51.2 M134.28 1.16 C131.72 11.69, 133.56 23.47, 134.52 54.65 M133.93 53.09 C103.49 59.22, 80.28 58.51, -1.19 52.09 M133.03 54.88 C92.08 50.88, 53.71 52.46, -0.4 54.88 M-3.64 53.12 C-1.65 33.33, 3.49 15.58, -2.23 -1.93 M-1.08 54.38 C0.63 44.74, -0.42 33.82, 0.29 1.34"
stroke="#000"
stroke-width="2"
fill="none"
></path>
</g>
<g stroke-linecap="round">
<g transform="translate(308.4481755172761 281.2115533662909) rotate(0 1.1385609918289674 145.9953857867422)">
<path
d="M-1.01 -2.17 C-1.07 46.71, 0.3 244.44, -0.46 294.16 M3.63 2.83 C3.44 50.71, -0.72 241.22, -1.36 289.41"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(308.16925883018916 284.66360015581995) rotate(0 135.1525798049602 -68.20042785962323)">
<path
d="M2.47 0.47 C46.8 -21.36, 220.33 -110.19, 264.96 -133.2 M0.37 -1.74 C45.62 -24.11, 225.01 -114.58, 269.93 -136.88"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(311.39372316987726 570.9674003164946) rotate(0 136.24116036890297 -67.43777376368234)">
<path
d="M-2.63 2.46 C20.4 -9.48, 94.34 -47.87, 140.63 -71.17 C186.92 -94.47, 252.17 -126.49, 275.11 -137.33 M1.14 1.33 C23.81 -10.35, 94.55 -46.04, 139.83 -68.58 C185.12 -91.11, 249.48 -121.76, 272.86 -133.89"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(580.6092831051336 150.72201134532952) rotate(0 1.5678417062894852 142.75141008423634)">
<path
d="M2.66 -1.91 C3.6 45.58, 2.41 239.01, 2.99 287.41 M0.67 3.23 C1.39 51.03, -0.51 235.96, 0.31 282.74"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(306.6976102947664 283.14653391715) rotate(0 -140.18354779216435 -59.60806644015338)">
<path
d="M-0.81 0.62 C-48.48 -18.36, -235.46 -96.23, -283.34 -115.75 M3.94 -1.52 C-44.13 -21.12, -236.56 -99.84, -284.31 -119.83"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(304.3414324224632 572.5226612839633) rotate(0 -144.27019052747903 -64.7761163684645)">
<path
d="M2.34 1.71 C-46 -19.52, -242.75 -105.04, -290.88 -126.78 M0.17 0.18 C-47.25 -21.98, -237.9 -110.2, -285.78 -131.27"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g
transform="translate(15.275892138818847 448.50738095516135) rotate(0 -0.49579063445983707 -143.71703352554232)"
>
<path
d="M-2.4 0.97 C-2.94 -47.38, -0.78 -240.15, -0.9 -288.41 M1.49 -0.97 C0.55 -49.09, -0.95 -237.81, -2.03 -285.33"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(10.301143858432795 164.72182108536072) rotate(0 142.35890827057267 -76.26873721417542)">
<path
d="M2.04 -1.02 C49.94 -26.43, 238.14 -126.5, 285.02 -151.52 M-0.3 -4.04 C47.43 -29.16, 234.44 -124.45, 282.68 -148.52"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(291.46813332015165 14.258444139957646) rotate(0 143.3244001532809 66.53622476241344)">
<path
d="M-0.18 -1.16 C46.98 21.36, 236.83 111.22, 284.98 134.14 M-3.72 -4.26 C44.15 18.77, 241.76 114.69, 290.37 137.33"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
<g stroke-linecap="round">
<g transform="translate(175.60844139934756 81.23280016017816) rotate(0 131.7777041676277 66.73388742398038)">
<path
d="M-1.87 -0.8 C42.4 22.26, 220.78 113.99, 265.42 137.17 M2.32 -3.7 C46.4 18.63, 220.35 109.95, 264.21 132.92"
stroke="#000000"
stroke-width="2"
fill="none"
></path>
</g>
</g>
</svg>
</template>

View file

@ -0,0 +1,27 @@
<script setup lang="ts">
import { Label } from '~~/lib/api/classes/labels';
defineProps({
label: {
type: Object as () => Label,
required: true,
},
});
const badge = ref(null);
const isHover = useElementHover(badge);
const { focused } = useFocus(badge);
const isActive = computed(() => isHover.value || focused.value);
</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>
</template>

View file

@ -0,0 +1,66 @@
<template>
<BaseModal v-model="modal">
<template #title> Create Label </template>
<form @submit.prevent="create">
<FormTextField
:trigger-focus="focused"
ref="locationNameRef"
:autofocus="true"
label="Label Name"
v-model="form.name"
/>
<FormTextField label="Label Description" v-model="form.description" />
<div class="modal-action">
<BaseButton type="submit" :loading="loading"> Create </BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
});
const modal = useVModel(props, 'modelValue');
const loading = ref(false);
const focused = ref(false);
const form = reactive({
name: '',
description: '',
color: '', // Future!
});
function reset() {
form.name = '';
form.description = '';
form.color = '';
focused.value = false;
modal.value = false;
loading.value = false;
}
whenever(
() => modal.value,
() => {
focused.value = true;
}
);
const api = useUserApi();
const toast = useNotifier();
async function create() {
const { data, error } = await api.labels.create(form);
if (error) {
toast.error("Couldn't create label");
return;
}
toast.success('Label created');
reset();
}
</script>

View file

@ -0,0 +1,70 @@
<template>
<BaseModal v-model="modal">
<template #title> Create Location </template>
<form @submit.prevent="create">
<FormTextField
:trigger-focus="focused"
ref="locationNameRef"
:autofocus="true"
label="Location Name"
v-model="form.name"
/>
<FormTextField label="Location Description" v-model="form.description" />
<div class="modal-action">
<BaseButton type="submit" :loading="loading"> Create </BaseButton>
</div>
</form>
</BaseModal>
</template>
<script setup lang="ts">
const props = defineProps({
modelValue: {
type: Boolean,
required: true,
},
});
const modal = useVModel(props, 'modelValue');
const loading = ref(false);
const focused = ref(false);
const form = reactive({
name: '',
description: '',
});
whenever(
() => modal.value,
() => {
focused.value = true;
}
);
function reset() {
form.name = '';
form.description = '';
focused.value = false;
modal.value = false;
loading.value = false;
}
const api = useUserApi();
const toast = useNotifier();
async function create() {
loading.value = true;
const { data, error } = await api.locations.create(form);
if (error) {
toast.error("Couldn't create location");
}
if (data) {
toast.success('Location created');
navigateTo(`/location/${data.id}`);
}
reset();
}
</script>

View file

@ -1,3 +0,0 @@
export type Results<T> = {
items: T[];
};

View file

@ -0,0 +1,32 @@
import { BaseAPI, UrlBuilder } from '../base';
import { Details, OutType, Results } from './types';
export type LabelCreate = Details & {
color: string;
};
export type LabelUpdate = LabelCreate;
export type Label = LabelCreate & OutType;
export class LabelsApi extends BaseAPI {
async getAll() {
return this.http.get<Results<Label>>(UrlBuilder('/labels'));
}
async create(label: LabelCreate) {
return this.http.post<LabelCreate, Label>(UrlBuilder('/labels'), label);
}
async get(id: string) {
return this.http.get<Label>(UrlBuilder(`/labels/${id}`));
}
async delete(id: string) {
return this.http.delete<void>(UrlBuilder(`/labels/${id}`));
}
async update(id: string, label: LabelUpdate) {
return this.http.put<LabelUpdate, Label>(UrlBuilder(`/labels/${id}`), label);
}
}

View file

@ -1,16 +1,11 @@
import { BaseAPI, UrlBuilder } from '../base'; import { BaseAPI, UrlBuilder } from '../base';
import { type Results } from '../base/base-types'; import { Details, OutType, Results } from './types';
export type LocationCreate = { export type LocationCreate = Details;
name: string;
description: string;
};
export type Location = LocationCreate & { export type Location = LocationCreate &
id: string; OutType & {
groupId: string; groupId: string;
createdAt: string;
updatedAt: string;
}; };
export type LocationUpdate = LocationCreate; export type LocationUpdate = LocationCreate;

View file

@ -0,0 +1,19 @@
/**
* OutType is the base type that is returned from the API.
* In contains the common fields that are included with every
* API response that isn't a bulk result
*/
export type OutType = {
id: string;
createdAt: string;
updatedAt: string;
};
export type Details = {
name: string;
description: string;
};
export type Results<T> = {
items: T[];
};

View file

@ -1,4 +1,4 @@
import { BaseAPI, UrlBuilder } from "./base"; import { BaseAPI, UrlBuilder } from './base';
export type LoginResult = { export type LoginResult = {
token: string; token: string;
@ -21,19 +21,13 @@ export type RegisterPayload = {
export class PublicApi extends BaseAPI { export class PublicApi extends BaseAPI {
public login(username: string, password: string) { public login(username: string, password: string) {
return this.http.post<LoginPayload, LoginResult>( return this.http.post<LoginPayload, LoginResult>(UrlBuilder('/users/login'), {
UrlBuilder("/users/login"),
{
username, username,
password, password,
} });
);
} }
public register(payload: RegisterPayload) { public register(payload: RegisterPayload) {
return this.http.post<RegisterPayload, LoginResult>( return this.http.post<RegisterPayload, LoginResult>(UrlBuilder('/users/register'), payload);
UrlBuilder("/users/register"),
payload
);
} }
} }

View file

@ -1,6 +1,7 @@
import { Requests } from "~~/lib/requests"; import { Requests } from '~~/lib/requests';
import { BaseAPI, UrlBuilder } from "./base"; import { BaseAPI, UrlBuilder } from './base';
import { LocationsApi } from "./classes/locations"; import { LabelsApi } from './classes/labels';
import { LocationsApi } from './classes/locations';
export type Result<T> = { export type Result<T> = {
item: T; item: T;
@ -15,20 +16,21 @@ export type User = {
export class UserApi extends BaseAPI { export class UserApi extends BaseAPI {
locations: LocationsApi; locations: LocationsApi;
labels: LabelsApi;
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);
Object.freeze(this); Object.freeze(this);
} }
public self() { public self() {
return this.http.get<Result<User>>(UrlBuilder("/users/self")); return this.http.get<Result<User>>(UrlBuilder('/users/self'));
} }
public logout() { public logout() {
return this.http.post<object, void>(UrlBuilder("/users/logout"), {}); return this.http.post<object, void>(UrlBuilder('/users/logout'), {});
} }
} }

View file

@ -1,5 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { type Location } from '~~/lib/api/classes/locations';
definePageMeta({ definePageMeta({
layout: 'home', layout: 'home',
}); });
@ -8,17 +7,21 @@
}); });
const api = useUserApi(); const api = useUserApi();
const locations = ref<Location[]>([]);
onMounted(async () => { const { data: locations } = useAsyncData('locations', async () => {
const { data } = await api.locations.getAll(); const { data } = await api.locations.getAll();
if (data) { return data.items;
locations.value = data.items; });
}
const { data: labels } = useAsyncData('labels', async () => {
const { data } = await api.labels.getAll();
return data.items;
}); });
</script> </script>
<template> <template>
<BaseContainer> <BaseContainer class="space-y-16">
<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 md:grid-cols-3 gap-4">
<NuxtLink <NuxtLink
@ -35,5 +38,13 @@
</div> </div>
</NuxtLink> </NuxtLink>
</div> </div>
</section>
<section>
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
<div class="flex gap-2">
<LabelChip v-for="label in labels" :label="label" />
</div>
</section>
</BaseContainer> </BaseContainer>
</template> </template>

View file

@ -105,7 +105,11 @@
<template> <template>
<div> <div>
<header class="sm:px-6 py-2 lg:p-14 sm:py-6"> <header class="sm:px-6 py-2 lg:p-14 sm:py-6">
<h2 class="mt-1 text-4xl font-bold tracking-tight text-base-content sm:text-5xl lg:text-6xl">Homebox</h2> <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>
<p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p> <p class="ml-1 text-lg text-base-content/50">Track, Organize, and Manage your Shit.</p>
</header> </header>
<div class="grid p-6 sm:place-items-center min-h-[50vh]"> <div class="grid p-6 sm:place-items-center min-h-[50vh]">

View file

@ -12,9 +12,18 @@
const preferences = useLocationViewPreferences(); const preferences = useLocationViewPreferences();
const location = ref<Location | null>(null);
const locationId = computed<string>(() => route.params.id as string); const locationId = computed<string>(() => route.params.id as string);
const { data: location } = useAsyncData(locationId.value, async () => {
const { data, error } = await api.locations.get(locationId.value);
if (error) {
toast.error('Failed to load location');
navigateTo('/home');
return;
}
return data;
});
function maybeTimeAgo(date?: string): string { function maybeTimeAgo(date?: string): string {
if (!date) { if (!date) {
return '??'; return '??';
@ -41,17 +50,6 @@
return dt; return dt;
}); });
onMounted(async () => {
const { data, error } = await api.locations.get(locationId.value);
if (error) {
toast.error('Failed to load location');
navigateTo('/home');
return;
}
location.value = data;
});
const { reveal } = useConfirm(); const { reveal } = useConfirm();
async function confirmDelete() { async function confirmDelete() {