move to nuxt

This commit is contained in:
Hayden 2022-09-01 14:32:03 -08:00
parent 890eb55d27
commit 26ecb5a9d4
93 changed files with 5273 additions and 4749 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
}

View 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,
}
}

View file

@ -0,0 +1,32 @@
package mappers
import (
"github.com/hay-kot/content/backend/ent"
"github.com/hay-kot/content/backend/internal/types"
)
func 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
}

View file

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

View file

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

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

View file

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

View file

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

@ -0,0 +1,6 @@
<template>
<NuxtLayout>
<Html lang="en" data-theme="garden" />
<NuxtPage />
</NuxtLayout>
</template>

View file

@ -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']
}

View file

@ -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']
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

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

@ -0,0 +1,5 @@
<template>
<main class="w-full min-h-screen bg-blue-100 grid place-items-center">
<slot></slot>
</main>
</template>

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

View file

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>
<AppToast />
<slot />
</div>
</template>

View file

@ -0,0 +1,9 @@
<template>
<div>
<AppToast />
<AppHeader />
<main>
<slot></slot>
</main>
</div>
</template>

View file

@ -1,4 +1,4 @@
import { Requests } from '../../lib/requests'; import { Requests } from '../../requests';
// < // <
// TGetResult, // TGetResult,
// TPostData, // TPostData,

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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
View 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
View 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">&copy; 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>

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

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

View file

@ -1,2 +0,0 @@
User-agent: *
Allow: /

View file

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

View file

@ -1,8 +0,0 @@
<script setup lang="ts">
import Toast from './components/App/Toast.vue';
</script>
<template>
<Toast />
<router-view />
</template>

View file

@ -1,7 +0,0 @@
import { describe, expect, it } from 'vitest'
describe('tests', () => {
it('should works', () => {
expect(1 + 1).toEqual(2)
})
})

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
<template>
<main class="w-full min-h-screen bg-blue-100 grid place-items-center">
<router-view />
</main>
</template>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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">&copy; 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>

View file

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

View file

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

View file

@ -1,7 +0,0 @@
import { defineStore } from 'pinia';
export const useStore = defineStore('store', {
state: () => ({
count: 0,
}),
});

View file

@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,3 +0,0 @@
import { ViteSSGContext } from 'vite-ssg';
export type ViteSetupModule = (ctx: ViteSSGContext) => void;

37
frontend/stores/auth.ts Normal file
View 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;
},
},
});

View file

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

View file

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

View file

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

View file

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