move to nuxt
|
@ -323,6 +323,39 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.LocationSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v1/locations/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "Gets a location and fields",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -331,6 +364,65 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "updates a location",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.LocationOut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "deletes a location",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/users/login": {
|
"/v1/users/login": {
|
||||||
|
@ -993,6 +1085,29 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"types.ItemSummary": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"locationId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"types.LocationCreate": {
|
"types.LocationCreate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -1005,6 +1120,35 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"types.LocationOut": {
|
"types.LocationOut": {
|
||||||
|
"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.LocationSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
|
|
|
@ -315,6 +315,39 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.LocationSummary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/v1/locations/{id}": {
|
||||||
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "Gets a location and fields",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
|
@ -323,6 +356,65 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "updates a location",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/types.LocationOut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Bearer": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Locations"
|
||||||
|
],
|
||||||
|
"summary": "deletes a location",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Location ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/v1/users/login": {
|
"/v1/users/login": {
|
||||||
|
@ -985,6 +1077,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"types.ItemSummary": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"locationId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"types.LocationCreate": {
|
"types.LocationCreate": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -997,6 +1112,35 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"types.LocationOut": {
|
"types.LocationOut": {
|
||||||
|
"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.LocationSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
|
|
|
@ -338,6 +338,21 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
type: object
|
type: object
|
||||||
|
types.ItemSummary:
|
||||||
|
properties:
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
locationId:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
updatedAt:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
types.LocationCreate:
|
types.LocationCreate:
|
||||||
properties:
|
properties:
|
||||||
description:
|
description:
|
||||||
|
@ -346,6 +361,25 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
types.LocationOut:
|
types.LocationOut:
|
||||||
|
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.LocationSummary:
|
||||||
properties:
|
properties:
|
||||||
createdAt:
|
createdAt:
|
||||||
type: string
|
type: string
|
||||||
|
@ -584,12 +618,68 @@ paths:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/types.LocationOut'
|
$ref: '#/definitions/types.LocationSummary'
|
||||||
security:
|
security:
|
||||||
- Bearer: []
|
- Bearer: []
|
||||||
summary: Create a new location
|
summary: Create a new location
|
||||||
tags:
|
tags:
|
||||||
- Locations
|
- Locations
|
||||||
|
/v1/locations/{id}:
|
||||||
|
delete:
|
||||||
|
parameters:
|
||||||
|
- description: Location ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: ""
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: deletes a location
|
||||||
|
tags:
|
||||||
|
- Locations
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: Location ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/types.LocationOut'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: Gets a location and fields
|
||||||
|
tags:
|
||||||
|
- Locations
|
||||||
|
put:
|
||||||
|
parameters:
|
||||||
|
- description: Location ID
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/types.LocationOut'
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
summary: updates a location
|
||||||
|
tags:
|
||||||
|
- Locations
|
||||||
/v1/users/login:
|
/v1/users/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
@ -52,6 +52,9 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
|
||||||
|
|
||||||
r.Get(v1Base("/locations"), v1Handlers.HandleLocationGetAll())
|
r.Get(v1Base("/locations"), v1Handlers.HandleLocationGetAll())
|
||||||
r.Post(v1Base("/locations"), v1Handlers.HandleLocationCreate())
|
r.Post(v1Base("/locations"), v1Handlers.HandleLocationCreate())
|
||||||
|
r.Get(v1Base("/locations/{id}"), v1Handlers.HandleLocationGet())
|
||||||
|
r.Put(v1Base("/locations/{id}"), v1Handlers.HandleLocationUpdate())
|
||||||
|
r.Delete(v1Base("/locations/{id}"), v1Handlers.HandleLocationDelete())
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
|
|
|
@ -3,12 +3,15 @@ package v1
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/content/backend/internal/services"
|
"github.com/hay-kot/content/backend/internal/services"
|
||||||
"github.com/hay-kot/content/backend/internal/types"
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
|
"github.com/hay-kot/content/backend/pkgs/logger"
|
||||||
"github.com/hay-kot/content/backend/pkgs/server"
|
"github.com/hay-kot/content/backend/pkgs/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleUserSelf godoc
|
// HandleLocationGetAll godoc
|
||||||
// @Summary Get All Locations
|
// @Summary Get All Locations
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
@ -29,12 +32,12 @@ func (ctrl *V1Controller) HandleLocationGetAll() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleUserSelf godoc
|
// HandleLocationCreate godoc
|
||||||
// @Summary Create a new location
|
// @Summary Create a new location
|
||||||
// @Tags Locations
|
// @Tags Locations
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Param payload body types.LocationCreate true "Location Data"
|
// @Param payload body types.LocationCreate true "Location Data"
|
||||||
// @Success 200 {object} types.LocationOut
|
// @Success 200 {object} types.LocationSummary
|
||||||
// @Router /v1/locations [POST]
|
// @Router /v1/locations [POST]
|
||||||
// @Security Bearer
|
// @Security Bearer
|
||||||
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||||
|
@ -57,3 +60,101 @@ func (ctrl *V1Controller) HandleLocationCreate() http.HandlerFunc {
|
||||||
server.Respond(w, http.StatusCreated, location)
|
server.Respond(w, http.StatusCreated, location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ctrl *V1Controller) partialParseIdAndUser(w http.ResponseWriter, r *http.Request) (uuid.UUID, *types.UserOut, error) {
|
||||||
|
uid, err := uuid.Parse(chi.URLParam(r, "id"))
|
||||||
|
if err != nil {
|
||||||
|
ctrl.log.Debug(err.Error(), logger.Props{
|
||||||
|
"details": "failed to convert id to valid UUID",
|
||||||
|
})
|
||||||
|
server.RespondError(w, http.StatusBadRequest, err)
|
||||||
|
return uuid.Nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user := services.UseUserCtx(r.Context())
|
||||||
|
return uid, user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLocationDelete godocs
|
||||||
|
// @Summary deletes a location
|
||||||
|
// @Tags Locations
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Location ID"
|
||||||
|
// @Success 204
|
||||||
|
// @Router /v1/locations/{id} [DELETE]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleLocationDelete() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = ctrl.svc.Location.Delete(r.Context(), user.GroupID, uid)
|
||||||
|
if err != nil {
|
||||||
|
ctrl.log.Error(err, nil)
|
||||||
|
server.RespondServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.Respond(w, http.StatusNoContent, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLocationGet godocs
|
||||||
|
// @Summary Gets a location and fields
|
||||||
|
// @Tags Locations
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Location ID"
|
||||||
|
// @Success 200 {object} types.LocationOut
|
||||||
|
// @Router /v1/locations/{id} [GET]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleLocationGet() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
uid, user, err := ctrl.partialParseIdAndUser(w, r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
location, err := ctrl.svc.Location.GetOne(r.Context(), user.GroupID, uid)
|
||||||
|
if err != nil {
|
||||||
|
ctrl.log.Error(err, nil)
|
||||||
|
server.RespondServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.Respond(w, http.StatusOK, location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleLocationUpdate godocs
|
||||||
|
// @Summary updates a location
|
||||||
|
// @Tags Locations
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path string true "Location ID"
|
||||||
|
// @Success 200 {object} types.LocationOut
|
||||||
|
// @Router /v1/locations/{id} [PUT]
|
||||||
|
// @Security Bearer
|
||||||
|
func (ctrl *V1Controller) HandleLocationUpdate() http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body := types.LocationUpdate{}
|
||||||
|
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.Location.Update(r.Context(), user.GroupID, body)
|
||||||
|
if err != nil {
|
||||||
|
ctrl.log.Error(err, nil)
|
||||||
|
server.RespondServerError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server.Respond(w, http.StatusOK, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,11 @@ type EntLocationRepository struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *EntLocationRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Location, error) {
|
func (r *EntLocationRepository) Get(ctx context.Context, ID uuid.UUID) (*ent.Location, error) {
|
||||||
return r.db.Location.Get(ctx, ID)
|
return r.db.Location.Query().
|
||||||
|
Where(location.ID(ID)).
|
||||||
|
WithGroup().
|
||||||
|
WithItems().
|
||||||
|
Only(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *EntLocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Location, error) {
|
func (r *EntLocationRepository) GetAll(ctx context.Context, groupId uuid.UUID) ([]*ent.Location, error) {
|
||||||
|
@ -37,10 +41,16 @@ func (r *EntLocationRepository) Create(ctx context.Context, groupdId uuid.UUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *EntLocationRepository) Update(ctx context.Context, data types.LocationUpdate) (*ent.Location, error) {
|
func (r *EntLocationRepository) Update(ctx context.Context, data types.LocationUpdate) (*ent.Location, error) {
|
||||||
return r.db.Location.UpdateOneID(data.ID).
|
_, err := r.db.Location.UpdateOneID(data.ID).
|
||||||
SetName(data.Name).
|
SetName(data.Name).
|
||||||
SetDescription(data.Description).
|
SetDescription(data.Description).
|
||||||
Save(ctx)
|
Save(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Get(ctx, data.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *EntLocationRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
func (r *EntLocationRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
||||||
|
|
9
backend/internal/services/mappers/helpers.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package mappers
|
||||||
|
|
||||||
|
func MapEach[T any, U any](items []T, fn func(T) U) []U {
|
||||||
|
result := make([]U, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
result[i] = fn(item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
17
backend/internal/services/mappers/items.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package mappers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hay-kot/content/backend/ent"
|
||||||
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToItemSummary(item *ent.Item) *types.ItemSummary {
|
||||||
|
return &types.ItemSummary{
|
||||||
|
ID: item.ID,
|
||||||
|
LocationID: item.Edges.Location.ID,
|
||||||
|
Name: item.Name,
|
||||||
|
Description: item.Description,
|
||||||
|
CreatedAt: item.CreatedAt,
|
||||||
|
UpdatedAt: item.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
32
backend/internal/services/mappers/locations.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 ToLocationSummary(location *ent.Location) *types.LocationSummary {
|
||||||
|
return &types.LocationSummary{
|
||||||
|
ID: location.ID,
|
||||||
|
GroupID: location.Edges.Group.ID,
|
||||||
|
Name: location.Name,
|
||||||
|
Description: location.Description,
|
||||||
|
CreatedAt: location.CreatedAt,
|
||||||
|
UpdatedAt: location.UpdatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLocationSummaryErr(location *ent.Location, err error) (*types.LocationSummary, error) {
|
||||||
|
return ToLocationSummary(location), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLocationOut(location *ent.Location) *types.LocationOut {
|
||||||
|
return &types.LocationOut{
|
||||||
|
LocationSummary: *ToLocationSummary(location),
|
||||||
|
Items: MapEach(location.Edges.Items, ToItemSummary),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToLocationOutErr(location *ent.Location, err error) (*types.LocationOut, error) {
|
||||||
|
return ToLocationOut(location), err
|
||||||
|
}
|
|
@ -2,44 +2,75 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/hay-kot/content/backend/ent"
|
|
||||||
"github.com/hay-kot/content/backend/internal/repo"
|
"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"
|
"github.com/hay-kot/content/backend/internal/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotOwner = errors.New("not owner")
|
||||||
|
)
|
||||||
|
|
||||||
type LocationService struct {
|
type LocationService struct {
|
||||||
repos *repo.AllRepos
|
repos *repo.AllRepos
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToLocationOut(location *ent.Location, err error) (*types.LocationOut, error) {
|
func (svc *LocationService) GetOne(ctx context.Context, groupId uuid.UUID, id uuid.UUID) (*types.LocationOut, error) {
|
||||||
return &types.LocationOut{
|
location, err := svc.repos.Locations.Get(ctx, id)
|
||||||
ID: location.ID,
|
|
||||||
GroupID: location.Edges.Group.ID,
|
if err != nil {
|
||||||
Name: location.Name,
|
return nil, err
|
||||||
Description: location.Description,
|
}
|
||||||
CreatedAt: location.CreatedAt,
|
|
||||||
UpdatedAt: location.UpdatedAt,
|
if location.Edges.Group.ID != groupId {
|
||||||
}, err
|
return nil, ErrNotOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.ToLocationOut(location), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data types.LocationCreate) (*types.LocationOut, error) {
|
func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LocationSummary, error) {
|
||||||
location, err := svc.repos.Locations.Create(ctx, groupId, data)
|
|
||||||
return ToLocationOut(location, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svc *LocationService) GetAll(ctx context.Context, groupId uuid.UUID) ([]*types.LocationOut, error) {
|
|
||||||
locations, err := svc.repos.Locations.GetAll(ctx, groupId)
|
locations, err := svc.repos.Locations.GetAll(ctx, groupId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
locationsOut := make([]*types.LocationOut, len(locations))
|
locationsOut := make([]*types.LocationSummary, len(locations))
|
||||||
for i, location := range locations {
|
for i, location := range locations {
|
||||||
locationOut, _ := ToLocationOut(location, nil)
|
locationsOut[i] = mappers.ToLocationSummary(location)
|
||||||
locationsOut[i] = locationOut
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return locationsOut, nil
|
return locationsOut, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (svc *LocationService) Create(ctx context.Context, groupId uuid.UUID, data types.LocationCreate) (*types.LocationSummary, error) {
|
||||||
|
location, err := svc.repos.Locations.Create(ctx, groupId, data)
|
||||||
|
return mappers.ToLocationSummaryErr(location, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *LocationService) Delete(ctx context.Context, groupId uuid.UUID, id uuid.UUID) error {
|
||||||
|
location, err := svc.repos.Locations.Get(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if location.Edges.Group.ID != groupId {
|
||||||
|
return ErrNotOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.repos.Locations.Delete(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svc *LocationService) Update(ctx context.Context, groupId uuid.UUID, data types.LocationUpdate) (*types.LocationOut, error) {
|
||||||
|
location, err := svc.repos.Locations.Get(ctx, data.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if location.Edges.Group.ID != groupId {
|
||||||
|
return nil, ErrNotOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
return mappers.ToLocationOutErr(svc.repos.Locations.Update(ctx, data))
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ type LocationUpdate struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocationOut struct {
|
type LocationSummary struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
GroupID uuid.UUID `json:"groupId"`
|
GroupID uuid.UUID `json:"groupId"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
@ -25,3 +25,17 @@ type LocationOut struct {
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemSummary struct {
|
||||||
|
ID uuid.UUID `json:"id"`
|
||||||
|
LocationID uuid.UUID `json:"locationId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocationOut struct {
|
||||||
|
LocationSummary
|
||||||
|
Items []*ItemSummary `json:"items"`
|
||||||
|
}
|
||||||
|
|
14
frontend/.gitignore
vendored
|
@ -1,10 +1,8 @@
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
*.log*
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
.*-debug.log
|
|
||||||
*.log
|
|
||||||
.vercel
|
|
||||||
.vite-ssg-temp
|
|
||||||
.idea
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 Christopher Reeve
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,137 +1,42 @@
|
||||||
<p align='center'>
|
# Nuxt 3 Minimal Starter
|
||||||
<img src='https://user-images.githubusercontent.com/45350572/138070856-731c849a-466b-41a2-b39d-c5b5e76e94fa.png' alt='Vitailse - Opinionated Vite Starter Template with TailwindCSS' width='300'/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Opinionated Vite starter template with [TailwindCSS](https://tailwindcss.com/)
|
Look at the [nuxt 3 documentation](https://v3.nuxtjs.org) to learn more.
|
||||||
|
|
||||||
Inspired by [Vitesse](https://github.com/antfu/vitesse) ❤
|
## Setup
|
||||||
|
|
||||||
## Features
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
- ⚡️ [Vue 3](https://github.com/vuejs/vue-next), [Vite 2](https://github.com/vitejs/vite), [pnpm](https://pnpm.js.org/), [ESBuild](https://github.com/evanw/esbuild) - born with fastness
|
|
||||||
|
|
||||||
- 🗂 [File based routing](./src/pages)
|
|
||||||
|
|
||||||
- 📦 [Components auto importing](./src/components)
|
|
||||||
|
|
||||||
- 🍍 [State Management via Pinia](https://pinia.esm.dev/)
|
|
||||||
|
|
||||||
- 📑 [Layout system](./src/layouts)
|
|
||||||
|
|
||||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
|
||||||
|
|
||||||
- 🌍 [I18n ready](./locales)
|
|
||||||
|
|
||||||
- 🎨 [Tailwind CSS](https://tailwindcss.com/) - Rapidly build modern websites without ever leaving your HTML.
|
|
||||||
|
|
||||||
- 😃 [Use icons from any icon sets, with no compromise](https://github.com/antfu/unplugin-icons)
|
|
||||||
|
|
||||||
- 🔥 Use the [new `<script setup>` syntax](https://github.com/vuejs/rfcs/pull/227)
|
|
||||||
|
|
||||||
- 📥 [APIs auto importing](https://github.com/antfu/unplugin-auto-import) - use Composition API and others directly
|
|
||||||
|
|
||||||
- 🖨 Server-side generation (SSG) via [vite-ssg](https://github.com/antfu/vite-ssg)
|
|
||||||
|
|
||||||
- 🦔 Critical CSS via [critters](https://github.com/GoogleChromeLabs/critters)
|
|
||||||
|
|
||||||
- 🦾 TypeScript, of course
|
|
||||||
|
|
||||||
## Pre-packed
|
|
||||||
|
|
||||||
### UI Frameworks
|
|
||||||
|
|
||||||
- [TailwindCSS](https://tailwindcss.com/)
|
|
||||||
- [TailwindCSS Typography](https://github.com/tailwindlabs/tailwindcss-typography)
|
|
||||||
- [TailwindCSS Forms](https://github.com/tailwindlabs/tailwindcss-forms)
|
|
||||||
- [TailwindCSS Aspect Ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio)
|
|
||||||
|
|
||||||
### Icons
|
|
||||||
|
|
||||||
- [Iconify](https://iconify.design) - use icons from any icon sets
|
|
||||||
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as Vue components
|
|
||||||
|
|
||||||
### Plugins
|
|
||||||
|
|
||||||
- [Vue Router](https://github.com/vuejs/vue-router)
|
|
||||||
- [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) - file system based routing
|
|
||||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) - layouts for pages
|
|
||||||
- [Pinia](https://pinia.esm.dev) - Intuitive, type safe, light and flexible Store for Vue using the composition api
|
|
||||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) - components auto import
|
|
||||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use Vue Composition API and others without importing
|
|
||||||
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
|
||||||
- [`@vueuse/head`](https://github.com/vueuse/head) - manipulate document head reactively
|
|
||||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - Internationalization
|
|
||||||
- [`vite-plugin-vue-i18n`](https://github.com/intlify/vite-plugin-vue-i18n) - Vite plugin for Vue I18n
|
|
||||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
|
||||||
|
|
||||||
### Coding Style
|
|
||||||
|
|
||||||
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
|
||||||
|
|
||||||
### Dev tools
|
|
||||||
|
|
||||||
- [TypeScript](https://www.typescriptlang.org/)
|
|
||||||
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
|
||||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - Server-side generation
|
|
||||||
- [critters](https://github.com/GoogleChromeLabs/critters) - Critical CSS
|
|
||||||
- [VS Code Extensions](./.vscode/extensions.json)
|
|
||||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - Fire up Vite server automatically
|
|
||||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) - Vue 3 `<script setup>` IDE support
|
|
||||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - Icon inline display and autocomplete
|
|
||||||
- [TailwindCSS Intellisense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - IDE support for Tailwind CSS
|
|
||||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - All in one i18n support
|
|
||||||
## Try it now!
|
|
||||||
### GitHub Template
|
|
||||||
|
|
||||||
[Create a repo from this template on GitHub](https://github.com/zynth17/vitailse/generate).
|
|
||||||
|
|
||||||
### Clone to local
|
|
||||||
|
|
||||||
If you prefer to do it manually with the cleaner git history
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npx degit zynth17/vitailse my-vitailse-app
|
# yarn
|
||||||
cd my-vitailse-app
|
yarn install
|
||||||
pnpm i # If you don't have pnpm installed, run: npm install -g pnpm
|
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install --shamefully-hoist
|
||||||
```
|
```
|
||||||
|
|
||||||
## Checklist
|
## Development Server
|
||||||
|
|
||||||
When you use this template, try follow the checklist to update your info properly
|
Start the development server on http://localhost:3000
|
||||||
|
|
||||||
- [ ] Rename `name` field in `package.json`
|
|
||||||
- [ ] Change the author name in `LICENSE`
|
|
||||||
- [ ] Change the title in `App.vue`
|
|
||||||
- [ ] Change the favicon in `public`
|
|
||||||
- [ ] Remove the `.github` folder which contains the funding info
|
|
||||||
- [ ] Clean up the READMEs and remove routes
|
|
||||||
|
|
||||||
And, enjoy :)
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
Just run and visit http://localhost:3000
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
### Preview in Https
|
## Production
|
||||||
|
|
||||||
Just run and visit https://localhost
|
Build the application for production:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm build && pnpm run https-preview
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build
|
Locally preview production build:
|
||||||
|
|
||||||
To build the App, run
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm build
|
npm run preview
|
||||||
```
|
```
|
||||||
|
|
||||||
And you will see the generated file in `dist` that ready to be served.
|
Checkout the [deployment documentation](https://v3.nuxtjs.org/guide/deploy/presets) for more information.
|
||||||
|
|
6
frontend/app.vue
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<Html lang="en" data-theme="garden" />
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</template>
|
252
frontend/auto-imports.d.ts
vendored
|
@ -1,252 +0,0 @@
|
||||||
// Generated by 'unplugin-auto-import'
|
|
||||||
export {}
|
|
||||||
declare global {
|
|
||||||
const EffectScope: typeof import('vue')['EffectScope']
|
|
||||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
|
||||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
|
||||||
const computed: typeof import('vue')['computed']
|
|
||||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
|
||||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
|
||||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
|
||||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
|
||||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
|
||||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
|
||||||
const createApp: typeof import('vue')['createApp']
|
|
||||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
|
||||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
|
||||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
|
||||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
|
||||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
|
||||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
|
||||||
const customRef: typeof import('vue')['customRef']
|
|
||||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
|
||||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
|
||||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
|
||||||
const defineComponent: typeof import('vue')['defineComponent']
|
|
||||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
|
||||||
const effectScope: typeof import('vue')['effectScope']
|
|
||||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
|
||||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
|
||||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
|
||||||
const h: typeof import('vue')['h']
|
|
||||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
|
||||||
const inject: typeof import('vue')['inject']
|
|
||||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
|
||||||
const isRef: typeof import('vue')['isRef']
|
|
||||||
const logicAnd: typeof import('@vueuse/core')['logicAnd']
|
|
||||||
const logicNot: typeof import('@vueuse/core')['logicNot']
|
|
||||||
const logicOr: typeof import('@vueuse/core')['logicOr']
|
|
||||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
|
||||||
const markRaw: typeof import('vue')['markRaw']
|
|
||||||
const nextTick: typeof import('vue')['nextTick']
|
|
||||||
const onActivated: typeof import('vue')['onActivated']
|
|
||||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
|
||||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
|
||||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
|
||||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
|
||||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
|
||||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
|
||||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
|
||||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
|
||||||
const onMounted: typeof import('vue')['onMounted']
|
|
||||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
|
||||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
|
||||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
|
||||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
|
||||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
|
||||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
|
||||||
const onUpdated: typeof import('vue')['onUpdated']
|
|
||||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
|
||||||
const provide: typeof import('vue')['provide']
|
|
||||||
const reactify: typeof import('@vueuse/core')['reactify']
|
|
||||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
|
||||||
const reactive: typeof import('vue')['reactive']
|
|
||||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
|
||||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
|
||||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
|
||||||
const readonly: typeof import('vue')['readonly']
|
|
||||||
const ref: typeof import('vue')['ref']
|
|
||||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
|
||||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
|
||||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
|
||||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
|
||||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
|
||||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
|
||||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
|
||||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
|
||||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
|
||||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
|
||||||
const shallowRef: typeof import('vue')['shallowRef']
|
|
||||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
|
||||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
|
||||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
|
||||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
|
||||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
|
||||||
const toRaw: typeof import('vue')['toRaw']
|
|
||||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
|
||||||
const toRef: typeof import('vue')['toRef']
|
|
||||||
const toRefs: typeof import('vue')['toRefs']
|
|
||||||
const triggerRef: typeof import('vue')['triggerRef']
|
|
||||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
|
||||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
|
||||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
|
||||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
|
||||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
|
||||||
const unref: typeof import('vue')['unref']
|
|
||||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
|
||||||
const until: typeof import('@vueuse/core')['until']
|
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
|
||||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
|
||||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
|
||||||
const useAttrs: typeof import('vue')['useAttrs']
|
|
||||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
|
||||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
|
||||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
|
||||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
|
||||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
|
||||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
|
||||||
const useCached: typeof import('@vueuse/core')['useCached']
|
|
||||||
const useClamp: typeof import('@vueuse/core')['useClamp']
|
|
||||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
|
||||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
|
||||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
|
||||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
|
||||||
const useCssModule: typeof import('vue')['useCssModule']
|
|
||||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
|
||||||
const useCssVars: typeof import('vue')['useCssVars']
|
|
||||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
|
||||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
|
||||||
const useDark: typeof import('@vueuse/core')['useDark']
|
|
||||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
|
||||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
|
||||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
|
||||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
|
||||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
|
||||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
|
||||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
|
||||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
|
||||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
|
||||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
|
||||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
|
||||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
|
||||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
|
||||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
|
||||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
|
||||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
|
||||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
|
||||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
|
||||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
|
||||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
|
||||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
|
||||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
|
||||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
|
||||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
|
||||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
|
||||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
|
||||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
|
||||||
const useFps: typeof import('@vueuse/core')['useFps']
|
|
||||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
|
||||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
|
||||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
|
||||||
const useHead: typeof import('@vueuse/head')['useHead']
|
|
||||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
|
||||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
|
||||||
const useImage: typeof import('@vueuse/core')['useImage']
|
|
||||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
|
||||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
|
||||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
|
||||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
|
||||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
|
||||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
|
||||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
|
||||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
|
||||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
|
||||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
|
||||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
|
||||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
|
||||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
|
||||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
|
||||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
|
||||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
|
||||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
|
||||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
|
||||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
|
||||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
|
||||||
const useNow: typeof import('@vueuse/core')['useNow']
|
|
||||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
|
||||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
|
||||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
|
||||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
|
||||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
|
||||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
|
||||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
|
||||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
|
||||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
|
||||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
|
||||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
|
||||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
|
||||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
|
||||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
|
||||||
const useRoute: typeof import('@vue-router')['useRoute']
|
|
||||||
const useRouter: typeof import('@vue-router')['useRouter']
|
|
||||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
|
||||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
|
||||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
|
||||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
|
||||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
|
||||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
|
||||||
const useShare: typeof import('@vueuse/core')['useShare']
|
|
||||||
const useSlots: typeof import('vue')['useSlots']
|
|
||||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
|
||||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
|
||||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
|
||||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
|
||||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
|
||||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
|
||||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
|
||||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
|
||||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
|
||||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
|
||||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
|
||||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
|
||||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
|
||||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
|
||||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
|
||||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
|
||||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
|
||||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
|
||||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
|
||||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
|
||||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
|
||||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
|
||||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
|
||||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
|
||||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
|
||||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
|
||||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
|
||||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
|
||||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
|
||||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
|
||||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
|
||||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
|
||||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
|
||||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
|
||||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
|
||||||
const watch: typeof import('vue')['watch']
|
|
||||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
|
||||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
|
||||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
|
||||||
const watchEffect: typeof import('vue')['watchEffect']
|
|
||||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
|
||||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
|
||||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
|
||||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
|
||||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
|
||||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
|
||||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
|
||||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
|
||||||
const whenever: typeof import('@vueuse/core')['whenever']
|
|
||||||
}
|
|
19
frontend/components.d.ts
vendored
|
@ -1,19 +0,0 @@
|
||||||
// generated by unplugin-vue-components
|
|
||||||
// We suggest you to commit this file into source control
|
|
||||||
// Read more: https://github.com/vuejs/core/pull/3399
|
|
||||||
import '@vue/runtime-core'
|
|
||||||
|
|
||||||
export {}
|
|
||||||
|
|
||||||
declare module '@vue/runtime-core' {
|
|
||||||
export interface GlobalComponents {
|
|
||||||
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
|
|
||||||
AppToast: typeof import('./src/components/App/Toast.vue')['default']
|
|
||||||
FormTextField: typeof import('./src/components/Form/TextField.vue')['default']
|
|
||||||
'Icon:bx:bxMoon': typeof import('~icons/bx/bx-moon')['default']
|
|
||||||
'Icon:bx:bxsMoon': typeof import('~icons/bx/bxs-moon')['default']
|
|
||||||
'IconAkarIcons:githubFill': typeof import('~icons/akar-icons/github-fill')['default']
|
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
|
||||||
}
|
|
||||||
}
|
|
139
frontend/components/App/Header.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useAuthStore } from '~~/stores/auth';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
const { error } = await authStore.logout(api);
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateTo('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
href: '/home',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logout',
|
||||||
|
action: logout,
|
||||||
|
last: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dropdown = [
|
||||||
|
{
|
||||||
|
name: 'Location',
|
||||||
|
action: () => {
|
||||||
|
modal.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Item / Asset',
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Label',
|
||||||
|
action: () => {},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ----------------------------
|
||||||
|
// 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>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
|
||||||
|
<template v-for="link in links">
|
||||||
|
<NuxtLink
|
||||||
|
v-if="!link.action"
|
||||||
|
class="hover:text-base-content transition-color duration-200 italic"
|
||||||
|
:to="link.href"
|
||||||
|
>
|
||||||
|
{{ link.name }}
|
||||||
|
</NuxtLink>
|
||||||
|
<button
|
||||||
|
for="location-form-modal"
|
||||||
|
v-else
|
||||||
|
@click="link.action"
|
||||||
|
class="hover:text-base-content transition-color duration-200 italic"
|
||||||
|
>
|
||||||
|
{{ link.name }}
|
||||||
|
</button>
|
||||||
|
<span v-if="!link.last"> / </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-6">
|
||||||
|
<div class="dropdown">
|
||||||
|
<label tabindex="0" class="btn btn-sm">
|
||||||
|
<span>
|
||||||
|
<Icon name="mdi-plus" class="w-5 h-5 mr-2" />
|
||||||
|
</span>
|
||||||
|
Create
|
||||||
|
</label>
|
||||||
|
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||||
|
<li v-for="btn in dropdown">
|
||||||
|
<button @click="btn.action">
|
||||||
|
{{ btn.name }}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseContainer>
|
||||||
|
</template>
|
58
frontend/components/App/Toast.vue
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div class="force-above fixed top-2 right-2 w-[300px]">
|
||||||
|
<TransitionGroup name="notify" tag="div">
|
||||||
|
<div
|
||||||
|
v-for="(notify, index) in notifications.slice(0, 4)"
|
||||||
|
:key="notify.id"
|
||||||
|
class="my-2 w-[300px] rounded-md p-3 text-sm text-white opacity-75"
|
||||||
|
:class="{
|
||||||
|
'bg-primary': notify.type === 'info',
|
||||||
|
'bg-red-600': notify.type === 'error',
|
||||||
|
'bg-green-600': notify.type === 'success',
|
||||||
|
}"
|
||||||
|
@click="dropNotification(index)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<template v-if="notify.type == 'success'">
|
||||||
|
<Icon name="heroicons-check" class="h-5 w-5" />
|
||||||
|
</template>
|
||||||
|
<template v-if="notify.type == 'info'">
|
||||||
|
<Icon name="heroicons-information-circle" class="h-5 w-5" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notify.type == 'error'">
|
||||||
|
<Icon name="heroicons-bell-alert" class="h-5 w-5" />
|
||||||
|
</template>
|
||||||
|
{{ notify.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useNotifications } from '@/composables/use-notifier';
|
||||||
|
|
||||||
|
const { notifications, dropNotification } = useNotifications();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.force-above {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notify-move,
|
||||||
|
.notify-enter-active,
|
||||||
|
.notify-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
.notify-enter-from,
|
||||||
|
.notify-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
.notify-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
</style>
|
21
frontend/components/Base/ActionsDivider.vue
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div class="w-full border-t border-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center">
|
||||||
|
<span class="isolate inline-flex -space-x-px rounded-md shadow-sm">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button @click="$emit('edit')" name="options" class="btn btn-sm btn-primary">
|
||||||
|
<Icon name="heroicons-pencil" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||||
|
<span> Edit </span>
|
||||||
|
</button>
|
||||||
|
<button @click="$emit('delete')" name="options" class="btn btn-sm btn-primary">
|
||||||
|
<Icon name="heroicons-trash" class="h-5 w-5 mr-1" aria-hidden="true" />
|
||||||
|
<span> Delete </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
24
frontend/components/Base/Button.vue
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:disabled="disabled || loading"
|
||||||
|
class="btn"
|
||||||
|
:class="{
|
||||||
|
loading: loading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
defineProps({
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
14
frontend/components/Base/Container.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps({
|
||||||
|
is: {
|
||||||
|
type: String,
|
||||||
|
default: 'div',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="is" class="container max-w-6xl mx-auto px-4">
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
37
frontend/components/Base/Details.vue
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="overflow-hidden card bg-base-100 shadow-xl sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:px-6 bg-neutral">
|
||||||
|
<h3 class="text-lg font-medium leading-6 text-neutral-content">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</h3>
|
||||||
|
<p v-if="$slots.subtitle" class="mt-1 max-w-2xl text-sm text-gray-500">
|
||||||
|
<slot name="subtitle"></slot>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-gray-300 px-4 py-5 sm:p-0">
|
||||||
|
<dl class="sm:divide-y sm:divide-gray-300">
|
||||||
|
<div v-for="(dValue, dKey) in details" class="py-4 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
|
||||||
|
<dt class="text-sm font-medium text-gray-500">
|
||||||
|
{{ dKey }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
{{ dValue }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type StringLike = string | number | boolean;
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
details: {
|
||||||
|
type: Object as () => Record<string, StringLike>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
45
frontend/components/Base/Modal.vue
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<div class="z-[9999]">
|
||||||
|
<input type="checkbox" :id="modalId" class="modal-toggle" v-model="modal" />
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-box relative">
|
||||||
|
<button @click="close" :for="modalId" class="btn btn-sm btn-circle absolute right-2 top-2">✕</button>
|
||||||
|
|
||||||
|
<h3 class="font-bold text-lg">
|
||||||
|
<slot name="title"></slot>
|
||||||
|
</h3>
|
||||||
|
<slot> </slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const emit = defineEmits(['cancel', 'update:modelValue']);
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* in readonly mode the modal only `emits` a "cancel" event to indicate
|
||||||
|
* that the modal was closed via the "x" button. The parent component is
|
||||||
|
* responsible for closing the modal.
|
||||||
|
*/
|
||||||
|
readonly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
if (props.readonly) {
|
||||||
|
emit('cancel');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalId = useId();
|
||||||
|
const modal = useVModel(props, 'modelValue', emit);
|
||||||
|
</script>
|
10
frontend/components/Base/SectionHeader.vue
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<div class="border-b border-base-200 pb-3">
|
||||||
|
<h3 class="text-xl font-medium leading-4 text-base-content">
|
||||||
|
<slot />
|
||||||
|
</h3>
|
||||||
|
<p v-if="$slots.description" class="mt-2 max-w-4xl text-sm text-gray-500">
|
||||||
|
<slot name="description" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
42
frontend/components/Form/TextField.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="form-control w-full">
|
||||||
|
<label class="label">
|
||||||
|
<span class="label-text">{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
<input ref="input" :type="type" v-model="value" class="input input-bordered w-full" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'text',
|
||||||
|
},
|
||||||
|
triggerFocus: {
|
||||||
|
type: Boolean,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => props.triggerFocus,
|
||||||
|
() => {
|
||||||
|
if (input.value) {
|
||||||
|
input.value.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = useVModel(props, 'modelValue');
|
||||||
|
</script>
|
31
frontend/components/Icon.vue
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
import type { IconifyIcon } from '@iconify/vue';
|
||||||
|
import { Icon as Iconify, loadIcon } from '@iconify/vue';
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon: Ref<IconifyIcon | null> = ref(null);
|
||||||
|
const component = computed(() => nuxtApp.vueApp.component(props.name));
|
||||||
|
|
||||||
|
icon.value = await loadIcon(props.name).catch(_ => null);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.name,
|
||||||
|
async () => {
|
||||||
|
icon.value = await loadIcon(props.name).catch(_ => null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Iconify v-if="icon" :icon="icon" class="inline-block w-5 h-5" />
|
||||||
|
<Component :is="component" v-else-if="component" />
|
||||||
|
<span v-else>{{ name }}</span>
|
||||||
|
</template>
|
15
frontend/components/ModalConfirm.vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<BaseModal @cancel="cancel(false)" v-model="isRevealed" readonly>
|
||||||
|
<template #title> Confirm </template>
|
||||||
|
<div>
|
||||||
|
<p>{{ text }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-action">
|
||||||
|
<BaseButton type="submit" @click="confirm(true)"> Confirm </BaseButton>
|
||||||
|
</div>
|
||||||
|
</BaseModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { text, isRevealed, confirm, cancel } = useConfirm();
|
||||||
|
</script>
|
23
frontend/composables/use-api.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { PublicApi } from "~~/lib/api/public";
|
||||||
|
import { UserApi } from "~~/lib/api/user";
|
||||||
|
import { Requests } from "~~/lib/requests";
|
||||||
|
import { useAuthStore } from "~~/stores/auth";
|
||||||
|
|
||||||
|
async function ApiDebugger(r: Response) {
|
||||||
|
console.table({
|
||||||
|
"Request Url": r.url,
|
||||||
|
"Response Status": r.status,
|
||||||
|
"Response Status Text": r.statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublicApi(): PublicApi {
|
||||||
|
const requests = new Requests("", "", {}, ApiDebugger);
|
||||||
|
return new PublicApi(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserApi(): UserApi {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const requests = new Requests("", () => authStore.token, {}, ApiDebugger);
|
||||||
|
return new UserApi(requests);
|
||||||
|
}
|
40
frontend/composables/use-confirm.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { UseConfirmDialogReturn } from '@vueuse/core';
|
||||||
|
import { Ref } from 'vue';
|
||||||
|
|
||||||
|
type Store = UseConfirmDialogReturn<any, Boolean, Boolean> & {
|
||||||
|
text: Ref<string>;
|
||||||
|
setup: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const store: Partial<Store> = {
|
||||||
|
text: ref('Are you sure you want to delete this item? '),
|
||||||
|
setup: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is used to wrap the ModalConfirmation which is a "Singleton" component
|
||||||
|
* that is used to confirm actions. It's mounded once on the root of the page and reused
|
||||||
|
* for every confirmation action that is required.
|
||||||
|
*
|
||||||
|
* This is in an experimental phase of development and may have unknown or unexpected side effects.
|
||||||
|
*/
|
||||||
|
export function useConfirm(): Store {
|
||||||
|
if (!store.setup) {
|
||||||
|
store.setup = true;
|
||||||
|
const { isRevealed, reveal, confirm, cancel } = useConfirmDialog<any, Boolean, Boolean>();
|
||||||
|
store.isRevealed = isRevealed;
|
||||||
|
store.reveal = reveal;
|
||||||
|
store.confirm = confirm;
|
||||||
|
store.cancel = cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDialog(msg: string) {
|
||||||
|
store.text.value = msg;
|
||||||
|
return await store.reveal();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...(store as Store),
|
||||||
|
reveal: openDialog,
|
||||||
|
};
|
||||||
|
}
|
23
frontend/composables/use-preferences.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Ref } from 'vue';
|
||||||
|
|
||||||
|
export type LocationViewPreferences = {
|
||||||
|
showDetails: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useLocationViewPreferences loads the view preferences from local storage and hydrates
|
||||||
|
* them. These are reactive and will update the local storage when changed.
|
||||||
|
*/
|
||||||
|
export function useLocationViewPreferences(): Ref<LocationViewPreferences> {
|
||||||
|
const results = useLocalStorage(
|
||||||
|
'homebox/preferences/location',
|
||||||
|
{
|
||||||
|
showDetails: true,
|
||||||
|
},
|
||||||
|
{ mergeDefaults: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// casting is required because the type returned is removable, however since we
|
||||||
|
// use `mergeDefaults` the result _should_ always be present.
|
||||||
|
return results as unknown as Ref<LocationViewPreferences>;
|
||||||
|
}
|
|
@ -1,20 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en" data-theme="garden">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vitailse | Opinionated vite starter template with TailwindCSS</title>
|
|
||||||
|
|
||||||
<meta name="description" content="Opinionated vite starter template with TailwindCSS" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
<link rel="icon" href="/favicon.png" type="image/png" />
|
|
||||||
<link rel="alternate icon" href="/favicon.ico" type="image/png" sizes="16x16" />
|
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
|
|
||||||
<link rel="mask-icon" href="/favicon.png" color="#076AE0" />
|
|
||||||
<meta name="theme-color" content="#076AE0" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
5
frontend/layouts/404.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<main class="w-full min-h-screen bg-blue-100 grid place-items-center">
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</template>
|
10
frontend/layouts/default.vue
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppToast />
|
||||||
|
<AppHeader />
|
||||||
|
<main class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen">
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
7
frontend/layouts/empty.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppToast />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
9
frontend/layouts/home.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<AppToast />
|
||||||
|
<AppHeader />
|
||||||
|
<main>
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,4 +1,4 @@
|
||||||
import { Requests } from '../../lib/requests';
|
import { Requests } from '../../requests';
|
||||||
// <
|
// <
|
||||||
// TGetResult,
|
// TGetResult,
|
||||||
// TPostData,
|
// TPostData,
|
|
@ -13,6 +13,8 @@ export type Location = LocationCreate & {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LocationUpdate = LocationCreate;
|
||||||
|
|
||||||
export class LocationsApi extends BaseAPI {
|
export class LocationsApi extends BaseAPI {
|
||||||
async getAll() {
|
async getAll() {
|
||||||
return this.http.get<Results<Location>>(UrlBuilder('/locations'));
|
return this.http.get<Results<Location>>(UrlBuilder('/locations'));
|
||||||
|
@ -21,4 +23,15 @@ export class LocationsApi extends BaseAPI {
|
||||||
async create(location: LocationCreate) {
|
async create(location: LocationCreate) {
|
||||||
return this.http.post<LocationCreate, Location>(UrlBuilder('/locations'), location);
|
return this.http.post<LocationCreate, Location>(UrlBuilder('/locations'), location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get(id: string) {
|
||||||
|
return this.http.get<Location>(UrlBuilder(`/locations/${id}`));
|
||||||
|
}
|
||||||
|
async delete(id: string) {
|
||||||
|
return this.http.delete<void>(UrlBuilder(`/locations/${id}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, location: LocationUpdate) {
|
||||||
|
return this.http.put<LocationUpdate, Location>(UrlBuilder(`/locations/${id}`), location);
|
||||||
|
}
|
||||||
}
|
}
|
39
frontend/lib/api/public.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { BaseAPI, UrlBuilder } from "./base";
|
||||||
|
|
||||||
|
export type LoginResult = {
|
||||||
|
token: string;
|
||||||
|
expiresAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginPayload = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RegisterPayload = {
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
groupName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PublicApi extends BaseAPI {
|
||||||
|
public login(username: string, password: string) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
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 { LocationsApi } from "./classes/locations";
|
||||||
|
|
||||||
export type Result<T> = {
|
export type Result<T> = {
|
||||||
item: T;
|
item: T;
|
||||||
|
@ -25,10 +25,10 @@ export class UserApi extends BaseAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
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"), {});
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -65,7 +65,6 @@ export class Requests {
|
||||||
|
|
||||||
const token = this.token();
|
const token = this.token();
|
||||||
if (token !== '' && args.headers !== undefined) {
|
if (token !== '' && args.headers !== undefined) {
|
||||||
// @ts-expect-error -- headers is always defined at this point
|
|
||||||
args.headers['Authorization'] = token;
|
args.headers['Authorization'] = token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +79,10 @@ export class Requests {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: T = await (async () => {
|
const data: T = await (async () => {
|
||||||
|
if (response.status === 204) {
|
||||||
|
return {} as T;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
} catch (e) {
|
} catch (e) {
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"pages": {
|
|
||||||
"home": "Home",
|
|
||||||
"other": {
|
|
||||||
"menu": "Other Page",
|
|
||||||
"desc": "An example of other pages"
|
|
||||||
},
|
|
||||||
"not-found": "Page not found"
|
|
||||||
},
|
|
||||||
"app": {
|
|
||||||
"offline": "App ready to work offline",
|
|
||||||
"new-content": "New content available, click on reload button to update."
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"desc": "Welcome to Vitailse, Vite starter template with {tailwindurl}",
|
|
||||||
"github": "Please give stars and report any issues on our {githuburl}"
|
|
||||||
},
|
|
||||||
"button": {
|
|
||||||
"reload": "Reload",
|
|
||||||
"close": "Close"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"pages": {
|
|
||||||
"home": "Beranda",
|
|
||||||
"other": {
|
|
||||||
"menu": "Halaman lain",
|
|
||||||
"desc": "Contoh untuk halaman lain"
|
|
||||||
},
|
|
||||||
"not-found": "Laman tidak ditemukan"
|
|
||||||
},
|
|
||||||
"app": {
|
|
||||||
"offline": "Aplikasi siap digunakan tanpa jaringan internet",
|
|
||||||
"new-content": "Konten baru ditemukan, Tekan tombol perbarui untuk memperbarui laman."
|
|
||||||
},
|
|
||||||
"intro": {
|
|
||||||
"desc": "Selamat datang di Vitailse, Template awal vite dengan ",
|
|
||||||
"github": "Mohon berikan bintang dan laporkan masalah pada "
|
|
||||||
},
|
|
||||||
"button": {
|
|
||||||
"reload": "Perbarui",
|
|
||||||
"close": "Tutup"
|
|
||||||
}
|
|
||||||
}
|
|
14
frontend/nuxt.config.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineNuxtConfig } from 'nuxt';
|
||||||
|
|
||||||
|
// https://v3.nuxtjs.org/api/configuration/nuxt.config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
ssr: false,
|
||||||
|
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt', '@vueuse/nuxt'],
|
||||||
|
vite: {
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:7745',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,71 +1,29 @@
|
||||||
{
|
{
|
||||||
"name": "@zynth/vitailse",
|
"private": true,
|
||||||
"description": "Vite starter template with TailwindCSS",
|
"scripts": {
|
||||||
"version": "0.1.0",
|
"build": "nuxt build",
|
||||||
"main": "src/main.ts",
|
"dev": "nuxt dev",
|
||||||
"repository": {
|
"generate": "nuxt generate",
|
||||||
"type": "git",
|
"preview": "nuxt preview",
|
||||||
"url": "git+https://github.com/zynth17/vitailse.git"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"devDependencies": {
|
||||||
"vitailse",
|
"nuxt": "3.0.0-rc.8",
|
||||||
"tailwindcss",
|
"vitest": "^0.22.1"
|
||||||
"vite",
|
},
|
||||||
"vitesse"
|
"dependencies": {
|
||||||
],
|
"@iconify/vue": "^3.2.1",
|
||||||
"author": "Christopher Reeeve",
|
"@nuxtjs/tailwindcss": "^5.3.2",
|
||||||
"license": "MIT",
|
"@pinia/nuxt": "^0.4.1",
|
||||||
"bugs": {
|
"@tailwindcss/aspect-ratio": "^0.4.0",
|
||||||
"url": "https://github.com/zynth17/vitailse/issues"
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
},
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"homepage": "https://github.com/zynth17/vitailse#readme",
|
"@vueuse/nuxt": "^9.1.1",
|
||||||
"scripts": {
|
"autoprefixer": "^10.4.8",
|
||||||
"dev": "vite",
|
"daisyui": "^2.24.0",
|
||||||
"build": "vite-ssg build",
|
"pinia": "^2.0.21",
|
||||||
"serve": "vite preview",
|
"postcss": "^8.4.16",
|
||||||
"test:watch": "vitest --watch",
|
"tailwindcss": "^3.1.8",
|
||||||
"https-preview": "serve dist"
|
"vue": "^3.2.38"
|
||||||
},
|
}
|
||||||
"dependencies": {
|
|
||||||
"@tailwindcss/aspect-ratio": "^0.4.0",
|
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
|
||||||
"@tailwindcss/typography": "^0.5.4",
|
|
||||||
"@types/node": "^18.0.4",
|
|
||||||
"@vueuse/components": "^8.9.3",
|
|
||||||
"@vueuse/core": "^8.9.3",
|
|
||||||
"@vueuse/head": "^0.7.6",
|
|
||||||
"autoprefixer": "^10.4.7",
|
|
||||||
"daisyui": "^2.24.0",
|
|
||||||
"pinia": "^2.0.16",
|
|
||||||
"postcss": "^8.4.14",
|
|
||||||
"tailwindcss": "^3.1.6",
|
|
||||||
"vue": "^3.2.37",
|
|
||||||
"vue-i18n": "^9.1.10",
|
|
||||||
"vue-router": "^4.1.2",
|
|
||||||
"workbox": "^0.0.0",
|
|
||||||
"workbox-window": "^6.5.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@iconify/json": "^2.1.78",
|
|
||||||
"@iconify/vue": "^3.2.1",
|
|
||||||
"@intlify/vite-plugin-vue-i18n": "^5.0.0",
|
|
||||||
"@vitejs/plugin-vue": "^3.0.0",
|
|
||||||
"@vue/compiler-sfc": "^3.2.37",
|
|
||||||
"@vue/server-renderer": "^3.2.37",
|
|
||||||
"critters": "^0.0.16",
|
|
||||||
"https-localhost": "^4.7.1",
|
|
||||||
"typescript": "^4.7.4",
|
|
||||||
"unplugin-auto-import": "^0.9.3",
|
|
||||||
"unplugin-icons": "^0.14.7",
|
|
||||||
"unplugin-vue-components": "0.21.1",
|
|
||||||
"unplugin-vue-router": "^0.0.21",
|
|
||||||
"vite": "^3.0.0",
|
|
||||||
"vite-plugin-pwa": "^0.12.3",
|
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
|
||||||
"vite-plugin-vue-type-imports": "^0.2.0",
|
|
||||||
"vite-ssg": "^0.20.2",
|
|
||||||
"vite-ssg-sitemap": "^0.3.2",
|
|
||||||
"vitest": "^0.18.0",
|
|
||||||
"vue-tsc": "^0.38.5"
|
|
||||||
}
|
|
||||||
}
|
}
|
15
frontend/pages/[...all].vue
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
useHead({
|
||||||
|
title: "404. Not Found",
|
||||||
|
});
|
||||||
|
definePageMeta({
|
||||||
|
layout: "404",
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1 class="text-blue-500 font-extrabold flex flex-col text-center">
|
||||||
|
<span class="text-7xl">404.</span>
|
||||||
|
<span class="text-5xl mt-5"> Page Not Found </span>
|
||||||
|
</h1>
|
||||||
|
</template>
|
39
frontend/pages/home.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type Location } from '~~/lib/api/classes/locations';
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'home',
|
||||||
|
});
|
||||||
|
useHead({
|
||||||
|
title: 'Homebox | Home',
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = useUserApi();
|
||||||
|
const locations = ref<Location[]>([]);
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data } = await api.locations.getAll();
|
||||||
|
if (data) {
|
||||||
|
locations.value = 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>
|
||||||
|
</template>
|
190
frontend/pages/index.vue
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TextField from '@/components/Form/TextField.vue';
|
||||||
|
import { useNotifier } from '@/composables/use-notifier';
|
||||||
|
import { usePublicApi } from '@/composables/use-api';
|
||||||
|
import { useAuthStore } from '~~/stores/auth';
|
||||||
|
useHead({
|
||||||
|
title: 'Homebox | Organize and Tag Your Stuff',
|
||||||
|
});
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'empty',
|
||||||
|
});
|
||||||
|
|
||||||
|
const registerFields = [
|
||||||
|
{
|
||||||
|
label: "What's your name?",
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "What's your email?",
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Name your group',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Set your password',
|
||||||
|
value: '',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Confirm your password',
|
||||||
|
value: '',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const api = usePublicApi();
|
||||||
|
|
||||||
|
async function registerUser() {
|
||||||
|
loading.value = true;
|
||||||
|
// Print Values of registerFields
|
||||||
|
|
||||||
|
const { data, error } = await api.register({
|
||||||
|
user: {
|
||||||
|
name: registerFields[0].value,
|
||||||
|
email: registerFields[1].value,
|
||||||
|
password: registerFields[3].value,
|
||||||
|
},
|
||||||
|
groupName: registerFields[2].value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Problem registering user');
|
||||||
|
} else {
|
||||||
|
toast.success('User registered');
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginFields = [
|
||||||
|
{
|
||||||
|
label: 'Email',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Password',
|
||||||
|
value: '',
|
||||||
|
type: 'password',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const toast = useNotifier();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data, error } = await api.login(loginFields[0].value, loginFields[1].value);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Invalid email or password');
|
||||||
|
} else {
|
||||||
|
toast.success('Logged in successfully');
|
||||||
|
|
||||||
|
authStore.$patch({
|
||||||
|
token: data.token,
|
||||||
|
expires: data.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigateTo('/home');
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerForm = ref(false);
|
||||||
|
function toggleLogin() {
|
||||||
|
registerForm.value = !registerForm.value;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<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]">
|
||||||
|
<Transition name="slide-fade">
|
||||||
|
<form v-if="registerForm" @submit.prevent="registerUser">
|
||||||
|
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Register</h2>
|
||||||
|
<TextField
|
||||||
|
v-for="field in registerFields"
|
||||||
|
v-model="field.value"
|
||||||
|
:label="field.label"
|
||||||
|
:key="field.label"
|
||||||
|
:type="field.type"
|
||||||
|
/>
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary mt-2"
|
||||||
|
:class="loading ? 'loading' : ''"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<form v-else @submit.prevent="login">
|
||||||
|
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Login</h2>
|
||||||
|
<TextField
|
||||||
|
v-for="field in loginFields"
|
||||||
|
v-model="field.value"
|
||||||
|
:label="field.label"
|
||||||
|
:key="field.label"
|
||||||
|
:type="field.type"
|
||||||
|
/>
|
||||||
|
<div class="card-actions justify-end mt-2">
|
||||||
|
<button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading">
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Transition>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<button @click="toggleLogin">
|
||||||
|
{{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="min-w-full absolute bottom-0 z-[-1]">
|
||||||
|
<svg class="fill-primary" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" viewBox="0 0 1440 320">
|
||||||
|
<path
|
||||||
|
fill-opacity="1"
|
||||||
|
d="M0,32L30,42.7C60,53,120,75,180,80C240,85,300,75,360,80C420,85,480,107,540,128C600,149,660,171,720,160C780,149,840,107,900,90.7C960,75,1020,85,1080,122.7C1140,160,1200,224,1260,234.7C1320,245,1380,203,1410,181.3L1440,160L1440,320L1410,320C1380,320,1320,320,1260,320C1200,320,1140,320,1080,320C1020,320,960,320,900,320C840,320,780,320,720,320C660,320,600,320,540,320C480,320,420,320,360,320C300,320,240,320,180,320C120,320,60,320,30,320L0,320Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div class="bg-primary flex-col flex min-h-[32vh]">
|
||||||
|
<div class="mt-auto mx-auto mb-8">
|
||||||
|
<p class="text-center text-gray-200">© 2022 Contents. All Rights Reserved. Haybytes LLC</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.slide-fade-enter-active {
|
||||||
|
transition: all 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-fade-enter-from,
|
||||||
|
.slide-fade-leave-to {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
136
frontend/pages/location/[id].vue
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Location } from '~~/lib/api/classes/locations';
|
||||||
|
import ActionsDivider from '../../components/Base/ActionsDivider.vue';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'home',
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const api = useUserApi();
|
||||||
|
const toast = useNotifier();
|
||||||
|
|
||||||
|
const preferences = useLocationViewPreferences();
|
||||||
|
|
||||||
|
const location = ref<Location | null>(null);
|
||||||
|
const locationId = computed<string>(() => route.params.id as string);
|
||||||
|
|
||||||
|
function maybeTimeAgo(date?: string): string {
|
||||||
|
if (!date) {
|
||||||
|
return '??';
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = new Date(date);
|
||||||
|
|
||||||
|
return `${useTimeAgo(time).value} (${useDateFormat(time, 'MM-DD-YYYY').value})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = computed(() => {
|
||||||
|
const dt = {
|
||||||
|
Name: location.value?.name || '',
|
||||||
|
Description: location.value?.description || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (preferences.value.showDetails) {
|
||||||
|
dt['Created At'] = maybeTimeAgo(location.value?.createdAt);
|
||||||
|
dt['Updated At'] = maybeTimeAgo(location.value?.updatedAt);
|
||||||
|
dt['Database ID'] = location.value?.id || '';
|
||||||
|
dt['Group Id'] = location.value?.groupId || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const { isCanceled } = await reveal('Are you sure you want to delete this location? This action cannot be undone.');
|
||||||
|
|
||||||
|
if (isCanceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error } = await api.locations.delete(locationId.value);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Failed to delete location');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success('Location deleted');
|
||||||
|
navigateTo('/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateModal = ref(false);
|
||||||
|
const updating = ref(false);
|
||||||
|
const updateData = reactive({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
function openUpdate() {
|
||||||
|
updateData.name = location.value?.name || '';
|
||||||
|
updateData.description = location.value?.description || '';
|
||||||
|
updateModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
updating.value = true;
|
||||||
|
const { error, data } = await api.locations.update(locationId.value, updateData);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Failed to update location');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Location updated');
|
||||||
|
console.log(data);
|
||||||
|
location.value = data;
|
||||||
|
updateModal.value = false;
|
||||||
|
updating.value = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseContainer>
|
||||||
|
<BaseModal v-model="updateModal">
|
||||||
|
<template #title> Update Location </template>
|
||||||
|
<form v-if="location" @submit.prevent="update">
|
||||||
|
<FormTextField :autofocus="true" label="Location Name" v-model="updateData.name" />
|
||||||
|
<FormTextField label="Location Description" v-model="updateData.description" />
|
||||||
|
<div class="modal-action">
|
||||||
|
<BaseButton type="submit" :loading="updating"> Update </BaseButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</BaseModal>
|
||||||
|
<section>
|
||||||
|
<BaseSectionHeader class="mb-5">
|
||||||
|
{{ location ? location.name : '' }}
|
||||||
|
</BaseSectionHeader>
|
||||||
|
<BaseDetails class="mb-2" :details="details">
|
||||||
|
<template #title> Location Details </template>
|
||||||
|
</BaseDetails>
|
||||||
|
<div class="form-control ml-auto mr-2 max-w-[130px]">
|
||||||
|
<label class="label cursor-pointer">
|
||||||
|
<input type="checkbox" v-model.checked="preferences.showDetails" class="checkbox" />
|
||||||
|
<span class="label-text"> Detailed View </span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<ActionsDivider @delete="confirmDelete" @edit="openUpdate" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- <section>
|
||||||
|
<BaseSectionHeader> Items </BaseSectionHeader>
|
||||||
|
</section> -->
|
||||||
|
</BaseContainer>
|
||||||
|
</template>
|
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 647 B |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 8.7 KiB |
Before Width: | Height: | Size: 22 KiB |
|
@ -1,2 +0,0 @@
|
||||||
User-agent: *
|
|
||||||
Allow: /
|
|
|
@ -1 +0,0 @@
|
||||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
|
@ -1,8 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import Toast from './components/App/Toast.vue';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Toast />
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { describe, expect, it } from 'vitest'
|
|
||||||
|
|
||||||
describe('tests', () => {
|
|
||||||
it('should works', () => {
|
|
||||||
expect(1 + 1).toEqual(2)
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { BaseAPI, UrlBuilder } from './base';
|
|
||||||
|
|
||||||
export type LoginResult = {
|
|
||||||
token: string;
|
|
||||||
expiresAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoginPayload = {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RegisterPayload = {
|
|
||||||
user: {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
groupName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class PublicApi extends BaseAPI {
|
|
||||||
public login(username: string, password: string) {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 35 KiB |
|
@ -1,71 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="force-above fixed top-2 right-2 w-[300px]">
|
|
||||||
<TransitionGroup name="notify" tag="div">
|
|
||||||
<div
|
|
||||||
v-for="(notify, index) in notifications.slice(0, 4)"
|
|
||||||
:key="notify.id"
|
|
||||||
class="my-2 w-[300px] rounded-md p-3 text-sm text-white opacity-75"
|
|
||||||
:class="{
|
|
||||||
'bg-primary': notify.type === 'info',
|
|
||||||
'bg-red-600': notify.type === 'error',
|
|
||||||
'bg-green-600': notify.type === 'success',
|
|
||||||
}"
|
|
||||||
@click="dropNotification(index)"
|
|
||||||
>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<template v-if="notify.type == 'info'">
|
|
||||||
<Icon
|
|
||||||
icon="mdi-information-outline"
|
|
||||||
class="h-5 w-5"
|
|
||||||
height="25"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template v-if="notify.type == 'success'">
|
|
||||||
<Icon
|
|
||||||
icon="mdi-check-circle-outline"
|
|
||||||
class="h-5 w-5"
|
|
||||||
height="25"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="notify.type == 'error'">
|
|
||||||
<Icon
|
|
||||||
icon="mdi-alert-circle-outline"
|
|
||||||
class="h-5 w-5"
|
|
||||||
height="25"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
{{ notify.message }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useNotifications } from '@/composables/use-notifier';
|
|
||||||
|
|
||||||
const { notifications, dropNotification } = useNotifications();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.force-above {
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.notify-move,
|
|
||||||
.notify-enter-active,
|
|
||||||
.notify-leave-active {
|
|
||||||
transition: all 0.5s ease;
|
|
||||||
}
|
|
||||||
.notify-enter-from,
|
|
||||||
.notify-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-30px);
|
|
||||||
}
|
|
||||||
.notify-leave-active {
|
|
||||||
position: absolute;
|
|
||||||
transform: translateY(30px);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,80 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
defineEmits(['toggleSidebar']);
|
|
||||||
|
|
||||||
const { availableLocales } = useI18n();
|
|
||||||
|
|
||||||
const preferedDark = usePreferredDark();
|
|
||||||
const isDark = useStorage('isDark', preferedDark.value);
|
|
||||||
const body = ref<HTMLBodyElement | null>(null);
|
|
||||||
|
|
||||||
const toggleDarkMode = () => {
|
|
||||||
if (body.value) {
|
|
||||||
if (isDark.value) {
|
|
||||||
body.value.classList.remove('dark');
|
|
||||||
} else {
|
|
||||||
body.value.classList.add('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDark.value = !isDark.value;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
body.value = document.querySelector('body') as HTMLBodyElement;
|
|
||||||
if (body.value) {
|
|
||||||
if (isDark.value) body.value.classList.add('dark');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<header>
|
|
||||||
<nav
|
|
||||||
class="
|
|
||||||
w-full
|
|
||||||
bg-white
|
|
||||||
text-gray-800
|
|
||||||
dark:bg-gray-800 dark:text-white
|
|
||||||
py-4
|
|
||||||
px-8
|
|
||||||
shadow-md
|
|
||||||
dark:shadow-md
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
border-b border-gray-400/50
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<router-link :to="{ name: 'home' }">
|
|
||||||
<div class="font-bold lg:text-xl md:text-lg text-md">Vitailse</div>
|
|
||||||
</router-link>
|
|
||||||
<div class="ml-auto flex items-center h-full">
|
|
||||||
<select
|
|
||||||
id="language"
|
|
||||||
v-model="$i18n.locale"
|
|
||||||
class="py-1 focus:outline-none rounded dark:text-gray-800"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="locale in availableLocales"
|
|
||||||
:key="locale"
|
|
||||||
:value="locale"
|
|
||||||
>
|
|
||||||
{{ locale }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
class="mx-5 cursor-pointer focus:outline-none"
|
|
||||||
@click="toggleDarkMode"
|
|
||||||
>
|
|
||||||
<icon:bx:bx-moon class="w-6 h-6" v-if="!isDark" />
|
|
||||||
<icon:bx:bxs-moon class="w-6 h-6" v-else />
|
|
||||||
</button>
|
|
||||||
<a href="https://github.com/zynth17/vitailse">
|
|
||||||
<icon-akar-icons:github-fill />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
|
@ -1,31 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="form-control w-full">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">{{ label }}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
:type="type"
|
|
||||||
v-model="value"
|
|
||||||
class="input input-bordered w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
default: 'text',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const value = useVModel(props, 'modelValue');
|
|
||||||
</script>
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { PublicApi } from '@/api/public';
|
|
||||||
import { UserApi } from '@/api/user';
|
|
||||||
import { Requests } from '@/lib/requests';
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
|
||||||
|
|
||||||
async function ApiDebugger(r: Response) {
|
|
||||||
console.table({
|
|
||||||
'Request Url': r.url,
|
|
||||||
'Response Status': r.status,
|
|
||||||
'Response Status Text': r.statusText,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function usePublicApi(): PublicApi {
|
|
||||||
const requests = new Requests('', '', {}, ApiDebugger);
|
|
||||||
return new PublicApi(requests);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useUserApi(): UserApi {
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const requests = new Requests('', () => authStore.token, {}, ApiDebugger);
|
|
||||||
return new UserApi(requests);
|
|
||||||
}
|
|
8
frontend/src/env.d.ts
vendored
|
@ -1,8 +0,0 @@
|
||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
declare module '*.vue' {
|
|
||||||
import { DefineComponent } from 'vue'
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
|
||||||
const component: DefineComponent<{}, {}, any>
|
|
||||||
export default component
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
<template>
|
|
||||||
<main class="w-full min-h-screen bg-blue-100 grid place-items-center">
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import Toast from '@/components/App/Toast.vue';
|
|
||||||
</script>
|
|
||||||
<template>
|
|
||||||
<Toast />
|
|
||||||
<header>
|
|
||||||
<app-header />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main
|
|
||||||
class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen"
|
|
||||||
>
|
|
||||||
<router-view />
|
|
||||||
</main>
|
|
||||||
</template>
|
|
|
@ -1,19 +0,0 @@
|
||||||
import App from '@/App.vue';
|
|
||||||
import { ViteSSG } from 'vite-ssg';
|
|
||||||
|
|
||||||
import '@/styles/index.css';
|
|
||||||
import { ViteSetupModule } from './types/ViteSetupModule';
|
|
||||||
import { extendedRoutes } from '@/router';
|
|
||||||
|
|
||||||
export const createApp = ViteSSG(
|
|
||||||
App,
|
|
||||||
{ routes: extendedRoutes },
|
|
||||||
async ctx => {
|
|
||||||
Object.values(
|
|
||||||
import.meta.glob<{ install: ViteSetupModule }>('./modules/*.ts', {
|
|
||||||
eager: true,
|
|
||||||
})
|
|
||||||
).map(i => i.install?.(ctx));
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
);
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { ViteSetupModule } from '@/types/ViteSetupModule';
|
|
||||||
import { createI18n } from 'vue-i18n';
|
|
||||||
|
|
||||||
// Import i18n resources
|
|
||||||
// https://vitejs.dev/guide/features.html#glob-import
|
|
||||||
|
|
||||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
|
||||||
const messages = Object.fromEntries(
|
|
||||||
Object.entries(
|
|
||||||
import.meta.glob<{ default: any }>('../../locales/*.{y(a)?ml,json}', {
|
|
||||||
eager: true,
|
|
||||||
})
|
|
||||||
).map(([key, value]) => {
|
|
||||||
const isYamlOrJson = key.endsWith('.yaml') || key.endsWith('.json');
|
|
||||||
|
|
||||||
return [key.slice(14, isYamlOrJson ? -5 : -4), value.default];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const install: ViteSetupModule = ({ app }) => {
|
|
||||||
const i18n = createI18n({
|
|
||||||
legacy: false,
|
|
||||||
locale: 'en',
|
|
||||||
messages,
|
|
||||||
globalInjection: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
app.use(i18n);
|
|
||||||
};
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { ViteSetupModule } from '@/types/ViteSetupModule';
|
|
||||||
import { createPinia } from 'pinia';
|
|
||||||
|
|
||||||
// Setup Pinia
|
|
||||||
// https://pinia.esm.dev/
|
|
||||||
export const install: ViteSetupModule = ({ isClient, initialState, app }) => {
|
|
||||||
const pinia = createPinia();
|
|
||||||
app.use(pinia);
|
|
||||||
// Refer to
|
|
||||||
// https://github.com/antfu/vite-ssg/blob/main/README.md#state-serialization
|
|
||||||
// for other serialization strategies.
|
|
||||||
if (isClient) pinia.state.value = initialState.pinia || {};
|
|
||||||
else initialState.pinia = pinia.state.value;
|
|
||||||
};
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { ViteSetupModule } from '@/types/ViteSetupModule';
|
|
||||||
|
|
||||||
export const install: ViteSetupModule = ({ isClient, router }) => {
|
|
||||||
if (!isClient) return;
|
|
||||||
|
|
||||||
router.isReady().then(async () => {
|
|
||||||
const { registerSW } = await import('virtual:pwa-register');
|
|
||||||
registerSW({ immediate: true });
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1,19 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
useHead({
|
|
||||||
title: '404. Not Found',
|
|
||||||
});
|
|
||||||
const { t } = useI18n();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1 class="text-blue-500 font-extrabold flex flex-col text-center">
|
|
||||||
<span class="text-7xl">404.</span>
|
|
||||||
<span class="text-5xl mt-5">{{ t('pages.not-found') }}</span>
|
|
||||||
</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<route lang="yaml">
|
|
||||||
name : not-found
|
|
||||||
meta:
|
|
||||||
layout: 404
|
|
||||||
</route>
|
|
|
@ -1,137 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
|
||||||
import { type Location } from '@/api/classes/locations';
|
|
||||||
import { Icon } from '@iconify/vue';
|
|
||||||
import { useUserApi } from '@/composables/use-api';
|
|
||||||
useHead({
|
|
||||||
title: 'Homebox | Home',
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
const user = ref({});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const { data } = await api.self();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
user.value = data.item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const locations = ref<Location[]>([]);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const { data } = await api.locations.getAll();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
console.log(data);
|
|
||||||
locations.value = data.items;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
const { error } = await authStore.logout(api);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
const links = [
|
|
||||||
{
|
|
||||||
name: 'Home',
|
|
||||||
href: '/home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Logout',
|
|
||||||
action: logout,
|
|
||||||
last: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const dropdown = [
|
|
||||||
{
|
|
||||||
name: 'Location',
|
|
||||||
action: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Item / Asset',
|
|
||||||
action: () => {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Label',
|
|
||||||
action: () => {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<section class="max-w-6xl mx-auto">
|
|
||||||
<header class="sm:px-6 py-2 lg:px-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>
|
|
||||||
<div class="ml-1 mt-2 text-lg text-base-content/50 space-x-2">
|
|
||||||
<template v-for="link in links">
|
|
||||||
<router-link
|
|
||||||
v-if="!link.action"
|
|
||||||
class="hover:text-base-content transition-color duration-200 italic"
|
|
||||||
:to="link.href"
|
|
||||||
>
|
|
||||||
{{ link.name }}
|
|
||||||
</router-link>
|
|
||||||
<button v-else @click="link.action" class="hover:text-base-content transition-color duration-200 italic">
|
|
||||||
{{ link.name }}
|
|
||||||
</button>
|
|
||||||
<span v-if="!link.last"> / </span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="flex mt-6">
|
|
||||||
<div class="dropdown">
|
|
||||||
<label tabindex="0" class="btn btn-sm">
|
|
||||||
<span>
|
|
||||||
<Icon icon="mdi-plus" class="w-5 h-5 mr-2" />
|
|
||||||
</span>
|
|
||||||
Create
|
|
||||||
</label>
|
|
||||||
<ul tabindex="0" class="dropdown-content menu p-2 shadow bg-base-100 rounded-box w-52">
|
|
||||||
<li v-for="btn in dropdown">
|
|
||||||
<button @click="btn.action">
|
|
||||||
{{ btn.name }}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
</section>
|
|
||||||
<section class="max-w-6xl mx-auto sm:px-6 lg:px-14">
|
|
||||||
<div class="border-b border-gray-600 pb-3 mb-3">
|
|
||||||
<h3 class="text-lg text-base-content font-medium leading-6">Storage Locations</h3>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<a
|
|
||||||
:href="`#${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 icon="mdi-light:home" class="h-5 w-5" height="25" />
|
|
||||||
{{ l.name }}
|
|
||||||
<span class="badge badge-accent badge-lg ml-auto text-accent-content text-lg">0</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<route lang="yaml">
|
|
||||||
name: home
|
|
||||||
</route>
|
|
|
@ -1,188 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import TextField from '@/components/Form/TextField.vue';
|
|
||||||
import { useNotifier } from '@/composables/use-notifier';
|
|
||||||
import { usePublicApi } from '@/composables/use-api';
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
|
||||||
useHead({
|
|
||||||
title: 'Homebox | Organize and Tag Your Stuff',
|
|
||||||
});
|
|
||||||
|
|
||||||
const registerFields = [
|
|
||||||
{
|
|
||||||
label: "What's your name?",
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "What's your email?",
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Name your group',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Set your password',
|
|
||||||
value: '',
|
|
||||||
type: 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Confirm your password',
|
|
||||||
value: '',
|
|
||||||
type: 'password',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const api = usePublicApi();
|
|
||||||
|
|
||||||
async function registerUser() {
|
|
||||||
loading.value = true;
|
|
||||||
// Print Values of registerFields
|
|
||||||
|
|
||||||
const { data, error } = await api.register({
|
|
||||||
user: {
|
|
||||||
name: registerFields[0].value,
|
|
||||||
email: registerFields[1].value,
|
|
||||||
password: registerFields[3].value,
|
|
||||||
},
|
|
||||||
groupName: registerFields[2].value,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
toast.error('Problem registering user');
|
|
||||||
} else {
|
|
||||||
toast.success('User registered');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginFields = [
|
|
||||||
{
|
|
||||||
label: 'Email',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Password',
|
|
||||||
value: '',
|
|
||||||
type: 'password',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const authStore = useAuthStore();
|
|
||||||
|
|
||||||
const toast = useNotifier();
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
async function login() {
|
|
||||||
loading.value = true;
|
|
||||||
const { data, error } = await api.login(loginFields[0].value, loginFields[1].value);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
toast.error('Invalid email or password');
|
|
||||||
} else {
|
|
||||||
toast.success('Logged in successfully');
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
authStore.$patch({
|
|
||||||
token: data.token,
|
|
||||||
expires: data.expiresAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
router.push({ name: 'home' });
|
|
||||||
}
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const registerForm = ref(false);
|
|
||||||
function toggleLogin() {
|
|
||||||
registerForm.value = !registerForm.value;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<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>
|
|
||||||
<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]">
|
|
||||||
<Transition name="slide-fade">
|
|
||||||
<form v-if="registerForm" @submit.prevent="registerUser">
|
|
||||||
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Register</h2>
|
|
||||||
<TextField
|
|
||||||
v-for="field in registerFields"
|
|
||||||
v-model="field.value"
|
|
||||||
:label="field.label"
|
|
||||||
:key="field.label"
|
|
||||||
:type="field.type"
|
|
||||||
/>
|
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<button type="submit" class="btn btn-primary mt-2" :class="loading ? 'loading' : ''" :disabled="loading">
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form v-else @submit.prevent="login">
|
|
||||||
<div class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl">
|
|
||||||
<div class="card-body">
|
|
||||||
<h2 class="card-title">Login</h2>
|
|
||||||
<TextField
|
|
||||||
v-for="field in loginFields"
|
|
||||||
v-model="field.value"
|
|
||||||
:label="field.label"
|
|
||||||
:key="field.label"
|
|
||||||
:type="field.type"
|
|
||||||
/>
|
|
||||||
<div class="card-actions justify-end mt-2">
|
|
||||||
<button type="submit" class="btn btn-primary" :class="loading ? 'loading' : ''" :disabled="loading">
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Transition>
|
|
||||||
<div class="text-center mt-2">
|
|
||||||
<button @click="toggleLogin">
|
|
||||||
{{ registerForm ? 'Already a User? Login' : 'Not a User? Register' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="min-w-full absolute bottom-0 z-[-1]">
|
|
||||||
<svg class="fill-primary" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" viewBox="0 0 1440 320">
|
|
||||||
<path
|
|
||||||
fill-opacity="1"
|
|
||||||
d="M0,32L30,42.7C60,53,120,75,180,80C240,85,300,75,360,80C420,85,480,107,540,128C600,149,660,171,720,160C780,149,840,107,900,90.7C960,75,1020,85,1080,122.7C1140,160,1200,224,1260,234.7C1320,245,1380,203,1410,181.3L1440,160L1440,320L1410,320C1380,320,1320,320,1260,320C1200,320,1140,320,1080,320C1020,320,960,320,900,320C840,320,780,320,720,320C660,320,600,320,540,320C480,320,420,320,360,320C300,320,240,320,180,320C120,320,60,320,30,320L0,320Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<div class="bg-primary flex-col flex min-h-[32vh]">
|
|
||||||
<div class="mt-auto mx-auto mb-8">
|
|
||||||
<p class="text-center text-gray-200">© 2022 Contents. All Rights Reserved. Haybytes LLC</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<route lang="yaml">
|
|
||||||
name: login
|
|
||||||
</route>
|
|
||||||
|
|
||||||
<style lang="css" scoped>
|
|
||||||
.slide-fade-enter-active {
|
|
||||||
transition: all 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slide-fade-enter-from,
|
|
||||||
.slide-fade-leave-to {
|
|
||||||
position: absolute;
|
|
||||||
transform: translateX(20px);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,17 +0,0 @@
|
||||||
import {
|
|
||||||
createRouter,
|
|
||||||
createWebHistory,
|
|
||||||
createMemoryHistory,
|
|
||||||
} from '@vue-router';
|
|
||||||
|
|
||||||
import { setupLayouts } from 'virtual:generated-layouts';
|
|
||||||
export let extendedRoutes: any = null;
|
|
||||||
export const router = createRouter({
|
|
||||||
history: import.meta.env.SSR ? createMemoryHistory() : createWebHistory(),
|
|
||||||
// You don't need to pass the routes anymore,
|
|
||||||
// the plugin writes it for you 🤖
|
|
||||||
extendRoutes: routes => {
|
|
||||||
extendedRoutes = routes;
|
|
||||||
return setupLayouts(routes);
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
import { UserApi } from '@/api/user';
|
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
|
||||||
state: () => ({
|
|
||||||
token: useLocalStorage('pinia/auth/token', ''),
|
|
||||||
expires: useLocalStorage('pinia/auth/expires', ''),
|
|
||||||
}),
|
|
||||||
getters: {
|
|
||||||
isTokenExpired: state => {
|
|
||||||
if (!state.expires) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof state.expires === 'string') {
|
|
||||||
return new Date(state.expires) < new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
return state.expires < new Date();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
async logout(api: UserApi) {
|
|
||||||
const result = await api.logout();
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.token = '';
|
|
||||||
this.expires = '';
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -1,7 +0,0 @@
|
||||||
import { defineStore } from 'pinia';
|
|
||||||
|
|
||||||
export const useStore = defineStore('store', {
|
|
||||||
state: () => ({
|
|
||||||
count: 0,
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,3 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import { ViteSSGContext } from 'vite-ssg';
|
|
||||||
|
|
||||||
export type ViteSetupModule = (ctx: ViteSSGContext) => void;
|
|
37
frontend/stores/auth.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { UserApi } from "~~/lib/api/user";
|
||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore("auth", {
|
||||||
|
state: () => ({
|
||||||
|
token: useLocalStorage("pinia/auth/token", ""),
|
||||||
|
expires: useLocalStorage("pinia/auth/expires", ""),
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isTokenExpired: (state) => {
|
||||||
|
if (!state.expires) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof state.expires === "string") {
|
||||||
|
return new Date(state.expires) < new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.expires < new Date();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
async logout(api: UserApi) {
|
||||||
|
const result = await api.logout();
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.token = "";
|
||||||
|
this.expires = "";
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,16 +1,16 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
content: ['./app.vue', './{components,pages,layouts}/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
darkMode: 'class', // or 'media' or 'class'
|
darkMode: 'class', // or 'media' or 'class'
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/forms'),
|
require('@tailwindcss/forms'),
|
||||||
require('@tailwindcss/aspect-ratio'),
|
require('@tailwindcss/aspect-ratio'),
|
||||||
require('@tailwindcss/typography'),
|
require('@tailwindcss/typography'),
|
||||||
require('daisyui'),
|
require('daisyui'),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,34 +1,4 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
// https://v3.nuxtjs.org/concepts/typescript
|
||||||
"target": "esnext",
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"strict": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"sourceMap": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"lib": ["esnext", "dom"],
|
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
},
|
|
||||||
"types": [
|
|
||||||
"vite/client",
|
|
||||||
"vite-plugin-vue-layouts/client",
|
|
||||||
"unplugin-icons/types/vue",
|
|
||||||
"vite-plugin-pwa/client",
|
|
||||||
"@intlify/vite-plugin-vue-i18n/client"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts",
|
|
||||||
"src/**/*.d.ts",
|
|
||||||
"src/**/*.tsx",
|
|
||||||
"src/**/*.vue",
|
|
||||||
"components.d.ts",
|
|
||||||
"auto-imports.d.ts",
|
|
||||||
"typed-router.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
96
frontend/typed-router.d.ts
vendored
|
@ -1,96 +0,0 @@
|
||||||
|
|
||||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
|
||||||
// It's recommended to commit this file.
|
|
||||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
|
||||||
|
|
||||||
/// <reference types="unplugin-vue-router/client" />
|
|
||||||
|
|
||||||
import type {
|
|
||||||
// type safe route locations
|
|
||||||
RouteLocationTypedList,
|
|
||||||
RouteLocationResolvedTypedList,
|
|
||||||
RouteLocationNormalizedTypedList,
|
|
||||||
RouteLocationNormalizedLoadedTypedList,
|
|
||||||
|
|
||||||
// helper types
|
|
||||||
// route definitions
|
|
||||||
RouteRecordInfo,
|
|
||||||
ParamValue,
|
|
||||||
ParamValueOneOrMore,
|
|
||||||
ParamValueZeroOrMore,
|
|
||||||
ParamValueZeroOrOne,
|
|
||||||
|
|
||||||
// vue-router extensions
|
|
||||||
_RouterTyped,
|
|
||||||
RouterLinkTyped,
|
|
||||||
NavigationGuard,
|
|
||||||
UseLinkFnTyped,
|
|
||||||
} from 'unplugin-vue-router'
|
|
||||||
|
|
||||||
declare module '@vue-router/routes' {
|
|
||||||
export interface RouteNamedMap {
|
|
||||||
'login': RouteRecordInfo<'login', '/', Record<never, never>, Record<never, never>>,
|
|
||||||
'not-found': RouteRecordInfo<'not-found', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
|
|
||||||
'home': RouteRecordInfo<'home', '/home', Record<never, never>, Record<never, never>>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '@vue-router' {
|
|
||||||
import type { RouteNamedMap } from '@vue-router/routes'
|
|
||||||
|
|
||||||
export type RouterTyped = _RouterTyped<RouteNamedMap>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type safe version of `RouteLocationNormalized` (the type of `to` and `from` in navigation guards).
|
|
||||||
* Allows passing the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteLocationNormalized<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedTypedList<RouteNamedMap>[Name]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type safe version of `RouteLocationNormalizedLoaded` (the return type of `useRoute()`).
|
|
||||||
* Allows passing the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteLocationNormalizedLoaded<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type safe version of `RouteLocationResolved` (the returned route of `router.resolve()`).
|
|
||||||
* Allows passing the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteLocationResolved<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationResolvedTypedList<RouteNamedMap>[Name]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Type safe version of `RouteLocation` . Allows passing the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteLocation<Name extends keyof RouteNamedMap = keyof RouteNamedMap> = RouteLocationTypedList<RouteNamedMap>[Name]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a type safe params for a route location. Requires the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteParams<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['params']
|
|
||||||
/**
|
|
||||||
* Generate a type safe raw params for a route location. Requires the name of the route to be passed as a generic.
|
|
||||||
*/
|
|
||||||
export type RouteParamsRaw<Name extends keyof RouteNamedMap> = RouteNamedMap[Name]['paramsRaw']
|
|
||||||
|
|
||||||
export function useRouter(): RouterTyped
|
|
||||||
export function useRoute<Name extends keyof RouteNamedMap = keyof RouteNamedMap>(name?: Name): RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[Name]
|
|
||||||
|
|
||||||
export const useLink: UseLinkFnTyped<RouteNamedMap>
|
|
||||||
|
|
||||||
export function onBeforeRouteLeave(guard: NavigationGuard<RouteNamedMap>): void
|
|
||||||
export function onBeforeRouteUpdate(guard: NavigationGuard<RouteNamedMap>): void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'vue-router' {
|
|
||||||
import type { RouteNamedMap } from '@vue-router/routes'
|
|
||||||
|
|
||||||
export interface TypesConfig {
|
|
||||||
beforeRouteUpdate: NavigationGuard<RouteNamedMap>
|
|
||||||
beforeRouteLeave: NavigationGuard<RouteNamedMap>
|
|
||||||
|
|
||||||
$route: RouteLocationNormalizedLoadedTypedList<RouteNamedMap>[keyof RouteNamedMap]
|
|
||||||
$router: _RouterTyped<RouteNamedMap>
|
|
||||||
|
|
||||||
RouterLink: RouterLinkTyped<RouteNamedMap>
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
|
||||||
import { resolve } from 'path';
|
|
||||||
import Components from 'unplugin-vue-components/vite';
|
|
||||||
import AutoImport from 'unplugin-auto-import/vite';
|
|
||||||
import Icons from 'unplugin-icons/vite';
|
|
||||||
import IconsResolver from 'unplugin-icons/resolver';
|
|
||||||
import Layouts from 'vite-plugin-vue-layouts';
|
|
||||||
import { VitePWA } from 'vite-plugin-pwa';
|
|
||||||
import VueI18n from '@intlify/vite-plugin-vue-i18n';
|
|
||||||
import generateSitemap from 'vite-ssg-sitemap';
|
|
||||||
import VueRouter from 'unplugin-vue-router/vite';
|
|
||||||
import { VueRouterExports } from 'unplugin-vue-router';
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [
|
|
||||||
vue(),
|
|
||||||
VueRouter({
|
|
||||||
dts: true,
|
|
||||||
routesFolder: 'src/pages',
|
|
||||||
}),
|
|
||||||
Components({
|
|
||||||
dts: true,
|
|
||||||
dirs: ['src/components'],
|
|
||||||
directoryAsNamespace: true,
|
|
||||||
resolvers: [
|
|
||||||
IconsResolver({
|
|
||||||
prefix: 'icon',
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
Icons({
|
|
||||||
compiler: 'vue3',
|
|
||||||
}),
|
|
||||||
AutoImport({
|
|
||||||
dts: true,
|
|
||||||
// targets to transform
|
|
||||||
include: [
|
|
||||||
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
|
||||||
/\.vue\??/, // .vue
|
|
||||||
],
|
|
||||||
dirs: ['./composables'],
|
|
||||||
|
|
||||||
// global imports to register
|
|
||||||
imports: [
|
|
||||||
// presets
|
|
||||||
'vue',
|
|
||||||
{ '@vue-router': VueRouterExports },
|
|
||||||
'vue-i18n',
|
|
||||||
'@vueuse/core',
|
|
||||||
'@vueuse/head',
|
|
||||||
// custom
|
|
||||||
],
|
|
||||||
|
|
||||||
// custom resolvers
|
|
||||||
// see https://github.com/antfu/unplugin-auto-import/pull/23/
|
|
||||||
resolvers: [],
|
|
||||||
}),
|
|
||||||
Layouts(),
|
|
||||||
VitePWA({
|
|
||||||
includeAssets: ['favicon-16x16.png', 'favicon-32x32.png', 'favicon.ico', 'robots.txt', 'apple-touch-icon.png'],
|
|
||||||
manifest: {
|
|
||||||
name: 'Vitailse',
|
|
||||||
short_name: 'Vitailse',
|
|
||||||
description: 'Opinionated vite template with TailwindCSS',
|
|
||||||
theme_color: '#076AE0',
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: 'pwa-192x192.png',
|
|
||||||
sizes: '192x192',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'pwa-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: 'pwa-512x512.png',
|
|
||||||
sizes: '512x512',
|
|
||||||
type: 'image/png',
|
|
||||||
purpose: 'any maskable',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
VueI18n({
|
|
||||||
runtimeOnly: true,
|
|
||||||
compositionOnly: true,
|
|
||||||
include: [resolve(__dirname, 'locales/**')],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': resolve(__dirname, './src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
server: {
|
|
||||||
fs: {
|
|
||||||
strict: true,
|
|
||||||
},
|
|
||||||
proxy: {
|
|
||||||
'/api': {
|
|
||||||
target: 'http://localhost:7745',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
include: ['vue', 'vue-router', '@vueuse/core', '@vueuse/head'],
|
|
||||||
},
|
|
||||||
// @ts-ignore
|
|
||||||
ssgOptions: {
|
|
||||||
script: 'async',
|
|
||||||
formatting: 'minify',
|
|
||||||
format: 'cjs',
|
|
||||||
onFinished() {
|
|
||||||
generateSitemap();
|
|
||||||
},
|
|
||||||
mock: true,
|
|
||||||
},
|
|
||||||
// https://github.com/vitest-dev/vitest
|
|
||||||
test: {
|
|
||||||
include: ['src/__test__/**/*.test.ts', 'src/**/*.test.ts', 'src/__test__/**/*.spec.ts'],
|
|
||||||
environment: 'jsdom',
|
|
||||||
deps: {
|
|
||||||
inline: ['@vue', '@vueuse', 'vue-demi'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ssr: {
|
|
||||||
// TODO: workaround until they support native ESM
|
|
||||||
noExternal: ['workbox-window', /vue-i18n/],
|
|
||||||
},
|
|
||||||
});
|
|