forked from mirrors/homebox
labels create and get
This commit is contained in:
parent
f956ec8eb2
commit
8ece3bd7bf
24 changed files with 850 additions and 132 deletions
|
@ -1273,13 +1273,70 @@ const docTemplate = `{
|
|||
}
|
||||
},
|
||||
"types.LabelCreate": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
|
|
|
@ -1265,13 +1265,70 @@
|
|||
}
|
||||
},
|
||||
"types.LabelCreate": {
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"color": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
|
|
|
@ -354,10 +354,47 @@ definitions:
|
|||
type: string
|
||||
type: object
|
||||
types.LabelCreate:
|
||||
properties:
|
||||
color:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
type: object
|
||||
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
|
||||
types.LabelSummary:
|
||||
properties:
|
||||
createdAt:
|
||||
type: string
|
||||
description:
|
||||
type: string
|
||||
groupId:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
updatedAt:
|
||||
type: string
|
||||
type: object
|
||||
types.LocationCreate:
|
||||
properties:
|
||||
|
|
|
@ -2,6 +2,10 @@ package v1
|
|||
|
||||
import (
|
||||
"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
|
||||
|
@ -13,6 +17,14 @@ import (
|
|||
// @Security Bearer
|
||||
func (ctrl *V1Controller) HandleLabelsGetAll() http.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLabelsCreate() http.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLabelDelete() http.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLabelGet() http.HandlerFunc {
|
||||
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
|
||||
func (ctrl *V1Controller) HandleLabelUpdate() http.HandlerFunc {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
60
backend/internal/repo/repo_labels.go
Normal file
60
backend/internal/repo/repo_labels.go
Normal 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)
|
||||
}
|
|
@ -8,6 +8,7 @@ type AllRepos struct {
|
|||
AuthTokens *EntTokenRepository
|
||||
Groups *EntGroupRepository
|
||||
Locations *EntLocationRepository
|
||||
Labels *EntLabelRepository
|
||||
}
|
||||
|
||||
func EntAllRepos(db *ent.Client) *AllRepos {
|
||||
|
@ -16,5 +17,6 @@ func EntAllRepos(db *ent.Client) *AllRepos {
|
|||
AuthTokens: &EntTokenRepository{db},
|
||||
Groups: &EntGroupRepository{db},
|
||||
Locations: &EntLocationRepository{db},
|
||||
Labels: &EntLabelRepository{db},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ type AllServices struct {
|
|||
User *UserService
|
||||
Admin *AdminService
|
||||
Location *LocationService
|
||||
Labels *LabelService
|
||||
}
|
||||
|
||||
func NewServices(repos *repo.AllRepos) *AllServices {
|
||||
|
@ -13,5 +14,6 @@ func NewServices(repos *repo.AllRepos) *AllServices {
|
|||
User: &UserService{repos},
|
||||
Admin: &AdminService{repos},
|
||||
Location: &LocationService{repos},
|
||||
Labels: &LabelService{repos},
|
||||
}
|
||||
}
|
||||
|
|
32
backend/internal/services/mappers/labels.go
Normal file
32
backend/internal/services/mappers/labels.go
Normal 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
|
||||
}
|
63
backend/internal/services/service_labels.go
Normal file
63
backend/internal/services/service_labels.go
Normal 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
|
||||
}
|
|
@ -1,7 +1,34 @@
|
|||
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"`
|
||||
}
|
||||
|
|
|
@ -25,79 +25,50 @@
|
|||
},
|
||||
];
|
||||
|
||||
const modals = reactive({
|
||||
location: false,
|
||||
label: false,
|
||||
item: false,
|
||||
});
|
||||
|
||||
const dropdown = [
|
||||
{
|
||||
name: 'Location',
|
||||
action: () => {
|
||||
modal.value = true;
|
||||
modals.location = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Item / Asset',
|
||||
action: () => {},
|
||||
action: () => {
|
||||
modals.item = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
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>
|
||||
|
||||
<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 />
|
||||
<BaseModal v-model="modal">
|
||||
<template #title> Create Location </template>
|
||||
<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>
|
||||
<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">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">
|
||||
<template v-for="link in links">
|
||||
<NuxtLink
|
||||
|
|
123
frontend/components/App/Logo.vue
Normal file
123
frontend/components/App/Logo.vue
Normal 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>
|
27
frontend/components/Label/Chip.vue
Normal file
27
frontend/components/Label/Chip.vue
Normal 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>
|
66
frontend/components/Label/CreateModal.vue
Normal file
66
frontend/components/Label/CreateModal.vue
Normal 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>
|
70
frontend/components/Location/CreateModal.vue
Normal file
70
frontend/components/Location/CreateModal.vue
Normal 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>
|
|
@ -1,3 +0,0 @@
|
|||
export type Results<T> = {
|
||||
items: T[];
|
||||
};
|
32
frontend/lib/api/classes/labels.ts
Normal file
32
frontend/lib/api/classes/labels.ts
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,12 @@
|
|||
import { BaseAPI, UrlBuilder } from '../base';
|
||||
import { type Results } from '../base/base-types';
|
||||
import { Details, OutType, Results } from './types';
|
||||
|
||||
export type LocationCreate = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
export type LocationCreate = Details;
|
||||
|
||||
export type Location = LocationCreate & {
|
||||
id: string;
|
||||
groupId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
export type Location = LocationCreate &
|
||||
OutType & {
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
export type LocationUpdate = LocationCreate;
|
||||
|
||||
|
|
19
frontend/lib/api/classes/types/index.ts
Normal file
19
frontend/lib/api/classes/types/index.ts
Normal 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[];
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { BaseAPI, UrlBuilder } from "./base";
|
||||
import { BaseAPI, UrlBuilder } from './base';
|
||||
|
||||
export type LoginResult = {
|
||||
token: string;
|
||||
|
@ -21,19 +21,13 @@ export type RegisterPayload = {
|
|||
|
||||
export class PublicApi extends BaseAPI {
|
||||
public login(username: string, password: string) {
|
||||
return this.http.post<LoginPayload, LoginResult>(
|
||||
UrlBuilder("/users/login"),
|
||||
{
|
||||
username,
|
||||
password,
|
||||
}
|
||||
);
|
||||
return this.http.post<LoginPayload, LoginResult>(UrlBuilder('/users/login'), {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
public register(payload: RegisterPayload) {
|
||||
return this.http.post<RegisterPayload, LoginResult>(
|
||||
UrlBuilder("/users/register"),
|
||||
payload
|
||||
);
|
||||
return this.http.post<RegisterPayload, LoginResult>(UrlBuilder('/users/register'), payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Requests } from "~~/lib/requests";
|
||||
import { BaseAPI, UrlBuilder } from "./base";
|
||||
import { LocationsApi } from "./classes/locations";
|
||||
import { Requests } from '~~/lib/requests';
|
||||
import { BaseAPI, UrlBuilder } from './base';
|
||||
import { LabelsApi } from './classes/labels';
|
||||
import { LocationsApi } from './classes/locations';
|
||||
|
||||
export type Result<T> = {
|
||||
item: T;
|
||||
|
@ -15,20 +16,21 @@ export type User = {
|
|||
|
||||
export class UserApi extends BaseAPI {
|
||||
locations: LocationsApi;
|
||||
|
||||
labels: LabelsApi;
|
||||
constructor(requests: Requests) {
|
||||
super(requests);
|
||||
|
||||
this.locations = new LocationsApi(requests);
|
||||
this.labels = new LabelsApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
public self() {
|
||||
return this.http.get<Result<User>>(UrlBuilder("/users/self"));
|
||||
return this.http.get<Result<User>>(UrlBuilder('/users/self'));
|
||||
}
|
||||
|
||||
public logout() {
|
||||
return this.http.post<object, void>(UrlBuilder("/users/logout"), {});
|
||||
return this.http.post<object, void>(UrlBuilder('/users/logout'), {});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script setup lang="ts">
|
||||
import { type Location } from '~~/lib/api/classes/locations';
|
||||
definePageMeta({
|
||||
layout: 'home',
|
||||
});
|
||||
|
@ -8,32 +7,44 @@
|
|||
});
|
||||
|
||||
const api = useUserApi();
|
||||
const locations = ref<Location[]>([]);
|
||||
onMounted(async () => {
|
||||
|
||||
const { data: locations } = useAsyncData('locations', async () => {
|
||||
const { data } = await api.locations.getAll();
|
||||
if (data) {
|
||||
locations.value = data.items;
|
||||
}
|
||||
return data.items;
|
||||
});
|
||||
|
||||
const { data: labels } = useAsyncData('labels', async () => {
|
||||
const { data } = await api.labels.getAll();
|
||||
return data.items;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseContainer>
|
||||
<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>
|
||||
<BaseContainer class="space-y-16">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<BaseSectionHeader class="mb-5"> Labels </BaseSectionHeader>
|
||||
<div class="flex gap-2">
|
||||
<LabelChip v-for="label in labels" :label="label" />
|
||||
</div>
|
||||
</section>
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
|
|
@ -105,7 +105,11 @@
|
|||
<template>
|
||||
<div>
|
||||
<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>
|
||||
</header>
|
||||
<div class="grid p-6 sm:place-items-center min-h-[50vh]">
|
||||
|
|
|
@ -12,9 +12,18 @@
|
|||
|
||||
const preferences = useLocationViewPreferences();
|
||||
|
||||
const location = ref<Location | null>(null);
|
||||
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 {
|
||||
if (!date) {
|
||||
return '??';
|
||||
|
@ -41,17 +50,6 @@
|
|||
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();
|
||||
|
||||
async function confirmDelete() {
|
||||
|
|
Loading…
Reference in a new issue