Compare commits
27 Commits
a6dca00199
...
main
Author | SHA1 | Date | |
---|---|---|---|
30c552cf6c | |||
3eab2a3c9f | |||
5d225f0186 | |||
52c3679158 | |||
aca8605870 | |||
6631288843 | |||
99c2eec759 | |||
623291541a | |||
d7f7a2d8d9 | |||
14828fb901 | |||
7894a05daf | |||
b97c6cba5c | |||
a7cf9fe4f0 | |||
5987fadb03 | |||
fcd29cc082 | |||
ca1e72c676 | |||
d4cf44bb22 | |||
845f3a5836 | |||
dafd6f21c0 | |||
f93388a8e9 | |||
b34c217d98 | |||
ea796999d4 | |||
51e3c74950 | |||
dedab91898 | |||
68d9110cd2 | |||
96f81418ec | |||
a0853cf880 |
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/driver"
|
||||
@ -14,7 +15,9 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/redisstore"
|
||||
"github.com/alexedwards/scs/v2"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
const portNumber = ":8080"
|
||||
@ -53,6 +56,23 @@ func run() (*driver.DB, error) {
|
||||
gob.Register(models.User{})
|
||||
gob.Register(models.Room{})
|
||||
gob.Register(models.Restriction{})
|
||||
gob.Register(map[string]int{})
|
||||
|
||||
// read flags
|
||||
inProduction := flag.Bool("production", true, "Application is in production")
|
||||
useCache := flag.Bool("cache", true, "Use template cache")
|
||||
dbHost := flag.String("dbhost", "localhost", "Database host")
|
||||
dbName := flag.String("dbname", "bookings", "Database name")
|
||||
dbUser := flag.String("dbuser", os.Getenv("PGUSER"), "Database user")
|
||||
dbPass := flag.String("dbpass", os.Getenv("PGPWD"), "Database password")
|
||||
dbPort := flag.String("dbport", "5432", "Database port")
|
||||
dbSSL := flag.String("dbssl", "disable", "Database ssl settings (disable, prefer, require)")
|
||||
|
||||
flag.Parse()
|
||||
if *dbUser == "" {
|
||||
fmt.Println("Missing required flags")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mailChan := make(chan models.MailData)
|
||||
app.MailChan = mailChan
|
||||
@ -60,9 +80,18 @@ func run() (*driver.DB, error) {
|
||||
listenForMail()
|
||||
|
||||
// change this to true when in production
|
||||
app.InProduction = false
|
||||
app.InProduction = *inProduction
|
||||
|
||||
// Establish connection pool to Redis.
|
||||
pool := &redis.Pool{
|
||||
MaxIdle: 10,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
return redis.Dial("tcp", "localhost:6379")
|
||||
},
|
||||
}
|
||||
|
||||
session = scs.New()
|
||||
session.Store = redisstore.New(pool)
|
||||
session.Lifetime = 24 * time.Hour
|
||||
session.Cookie.Persist = true
|
||||
session.Cookie.SameSite = http.SameSiteLaxMode
|
||||
@ -72,7 +101,8 @@ func run() (*driver.DB, error) {
|
||||
|
||||
// connect to database
|
||||
log.Println("Connecting to database...")
|
||||
dsn := fmt.Sprintf("host=localhost port=5432 dbname=bookings user=%s password=%s", os.Getenv("PGUSER"), os.Getenv("PGPWD"))
|
||||
dsn := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=%s",
|
||||
*dbHost, *dbPort, *dbName, *dbUser, *dbPass, *dbSSL)
|
||||
db, err := driver.ConnectSQL(dsn)
|
||||
if err != nil {
|
||||
log.Fatal("Cannot connect to database! Dying...")
|
||||
@ -85,7 +115,7 @@ func run() (*driver.DB, error) {
|
||||
return nil, err
|
||||
}
|
||||
app.TemplateCahce = tc
|
||||
app.UseCache = false
|
||||
app.UseCache = *useCache
|
||||
|
||||
infoLog = log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
|
||||
app.InfoLog = infoLog
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"net/http"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
@ -34,3 +35,15 @@ func NoSurf(next http.Handler) http.Handler {
|
||||
func SessionLoad(next http.Handler) http.Handler {
|
||||
return session.LoadAndSave(next)
|
||||
}
|
||||
|
||||
func Auth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !helpers.IsAuthenticated(r) {
|
||||
session.Put(r.Context(), "error", "Log in first!")
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
@ -32,9 +32,27 @@ func routes(app *config.AppConfig) http.Handler {
|
||||
mux.Get("/book-room", handlers.Repo.BookRoom)
|
||||
mux.Get("/user/login", handlers.Repo.ShowLogin)
|
||||
mux.Post("/user/login", handlers.Repo.PostShowLogin)
|
||||
mux.Get("/user/logout", handlers.Repo.Logout)
|
||||
|
||||
fileServer := http.FileServer(http.Dir("./static/"))
|
||||
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
||||
|
||||
mux.Route("/admin", func(mux chi.Router) {
|
||||
if app.InProduction {
|
||||
mux.Use(Auth)
|
||||
}
|
||||
mux.Get("/dashboard", handlers.Repo.AdminDashboard)
|
||||
|
||||
mux.Get("/reservations-new", handlers.Repo.AdminNewReservations)
|
||||
mux.Get("/reservations-all", handlers.Repo.AdminAllReservations)
|
||||
mux.Get("/reservations-calendar", handlers.Repo.AdminReservationsCalendar)
|
||||
mux.Post("/reservations-calendar", handlers.Repo.AdminPostReservationsCalendar)
|
||||
mux.Get("/process-reservation/{src}/{id}/do", handlers.Repo.AdminProcessReservation)
|
||||
mux.Get("/delete-reservation/{src}/{id}/do", handlers.Repo.AdminDeleteReservation)
|
||||
|
||||
mux.Get("/reservations/{src}/{id}/show", handlers.Repo.AdminShowReservation)
|
||||
mux.Post("/reservations/{src}/{id}", handlers.Repo.AdminPostShowReservation)
|
||||
})
|
||||
|
||||
return mux
|
||||
}
|
||||
|
32
docker/docker-compose-example.yml
Normal file
@ -0,0 +1,32 @@
|
||||
# Use postgres/postgres user/password credentials
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
# set shared memory limit when using docker-compose
|
||||
shm_size: 128mb
|
||||
# or set shared memory limit when deploy via swarm stack
|
||||
#volumes:
|
||||
# - type: tmpfs
|
||||
# target: /dev/shm
|
||||
# tmpfs:
|
||||
# size: 134217728 # 128*2^20 bytes = 128Mb
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8081:8080
|
||||
|
||||
cache:
|
||||
image: redis
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
command: redis-server --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- cache:/data
|
@ -23,3 +23,9 @@ services:
|
||||
ports:
|
||||
- 8081:8080
|
||||
|
||||
cache:
|
||||
image: redis
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
command: redis-server --save 60 1 --loglevel warning
|
7
go.mod
@ -2,11 +2,12 @@ module go-udemy-web-1
|
||||
|
||||
go 1.21.0
|
||||
|
||||
// github.com/CloudyKit/jet --> Check this later
|
||||
// Template: github.com/CloudyKit/jet --> Check this later
|
||||
//
|
||||
// ORM packages:
|
||||
// upper/db https://github.com/upper/db
|
||||
// gorm.io/gorm
|
||||
// TODO Checkout sqlc and sqlx
|
||||
|
||||
// TODO Use default http package to rewrite the project with go 1.22.1
|
||||
|
||||
@ -17,10 +18,13 @@ require github.com/justinas/nosurf v1.1.1
|
||||
require github.com/alexedwards/scs/v2 v2.8.0
|
||||
|
||||
require (
|
||||
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
|
||||
github.com/gomodule/redigo v1.9.2
|
||||
github.com/jackc/pgconn v1.14.3
|
||||
github.com/jackc/pgx/v5 v5.6.0
|
||||
github.com/xhit/go-simple-mail v2.2.2+incompatible
|
||||
golang.org/x/crypto v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
@ -30,7 +34,6 @@ require (
|
||||
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
golang.org/x/crypto v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
)
|
||||
|
10
go.sum
@ -1,3 +1,5 @@
|
||||
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885 h1:UdHeICe7BgRbDq5yjA/yjCyJnohROtyD8PpJjhdAvF8=
|
||||
github.com/alexedwards/scs/redisstore v0.0.0-20240316134038-7e11d57e8885/go.mod h1:ceKFatoD+hfHWWeHOAYue1J+XgOJjE7dw8l3JtIRTGY=
|
||||
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
|
||||
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
@ -7,6 +9,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
|
||||
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/gomodule/redigo v1.8.0/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s=
|
||||
github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
@ -33,9 +38,10 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xhit/go-simple-mail v2.2.2+incompatible h1:Hm2VGfLqiQJ/NnC8SYsrPOPyVYIlvP2kmnotP4RIV74=
|
||||
github.com/xhit/go-simple-mail v2.2.2+incompatible/go.mod h1:I8Ctg6vIJZ+Sv7k/22M6oeu/tbFumDY0uxBuuLbtU7Y=
|
||||
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"go-udemy-web-1/internal/config"
|
||||
"go-udemy-web-1/internal/driver"
|
||||
"go-udemy-web-1/internal/forms"
|
||||
"go-udemy-web-1/internal/helpers"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"go-udemy-web-1/internal/render"
|
||||
"go-udemy-web-1/internal/repository"
|
||||
@ -15,6 +16,8 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// Repo the repository used by the handlers
|
||||
@ -80,14 +83,14 @@ func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
|
||||
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
room, err := m.DB.GetRoomById(res.RoomID)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't find room")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -115,13 +118,13 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
|
||||
reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "can't get reservation from session")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't parse form")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -136,15 +139,20 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
|
||||
reservation.Email = form.Get("email")
|
||||
reservation.Phone = form.Get("phone")
|
||||
|
||||
// TODO: Should I check the validity of reservation.StartDate / EndDate?
|
||||
|
||||
if !form.Valid() {
|
||||
data := make(map[string]interface{})
|
||||
data["reservation"] = reservation
|
||||
|
||||
sd := reservation.StartDate.Format("2006-01-02")
|
||||
ed := reservation.EndDate.Format("2006-01-02")
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["start_date"] = sd
|
||||
stringMap["end_date"] = ed
|
||||
|
||||
render.Template(w, r, "make-reservation.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
Form: form,
|
||||
StringMap: stringMap,
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -152,7 +160,7 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
|
||||
newReservationID, err := m.DB.InsertReservation(reservation)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't insert reservation into database")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -170,7 +178,7 @@ func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request)
|
||||
err = m.DB.InsertRoomRestriction(restriction)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't insert room restriction into database")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -221,7 +229,7 @@ func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request)
|
||||
if !ok {
|
||||
m.App.ErrorLog.Println("connot get item from session")
|
||||
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
m.App.Session.Remove(r.Context(), "reservation")
|
||||
@ -251,7 +259,7 @@ func (m *Repository) Availability(w http.ResponseWriter, r *http.Request) {
|
||||
func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "can't parse form")
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
start := r.Form.Get("start")
|
||||
@ -261,20 +269,20 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
startDate, err := time.Parse(layout, start)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse start date")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, end)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse end date")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
rooms, err := m.DB.SearchAvailabilityForAllRooms(startDate, endDate)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't connect to database")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -306,11 +314,11 @@ func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type jsonResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Message string `json:"message"`
|
||||
RoomID string `json:"room_id"`
|
||||
StartDate string `json:"start_date"`
|
||||
EndDate string `json:"end_date"`
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
|
||||
// AvailabilityJSON is the search for availability page handler
|
||||
@ -399,7 +407,7 @@ func (m *Repository) ChooseRoom(w http.ResponseWriter, r *http.Request) {
|
||||
roomID, err := strconv.Atoi(exploded[2])
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse roomID")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
m.App.Session.Get(r.Context(), "reservation")
|
||||
@ -407,7 +415,7 @@ func (m *Repository) ChooseRoom(w http.ResponseWriter, r *http.Request) {
|
||||
res, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
|
||||
if !ok {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@ -429,19 +437,19 @@ func (m *Repository) BookRoom(w http.ResponseWriter, r *http.Request) {
|
||||
startDate, err := time.Parse(layout, sd)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse start date")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse(layout, ed)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse end date")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
room, err := m.DB.GetRoomById(roomID)
|
||||
if err != nil {
|
||||
m.App.Session.Put(r.Context(), "error", "Can't parse roomId")
|
||||
http.Redirect(w, r, "/availability", http.StatusTemporaryRedirect)
|
||||
http.Redirect(w, r, "/availability", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
res.RoomID = roomID
|
||||
@ -453,6 +461,7 @@ func (m *Repository) BookRoom(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/make-reservation", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ShowLogin shows the login screen
|
||||
func (m *Repository) ShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "login.page.tmpl", &models.TemplateData{
|
||||
Form: forms.New(nil),
|
||||
@ -474,9 +483,11 @@ func (m *Repository) PostShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
password := r.Form.Get("password")
|
||||
form := forms.New(r.PostForm)
|
||||
form.Required("email", "password")
|
||||
form.IsEmail("email")
|
||||
if !form.Valid() {
|
||||
// TODO
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
render.Template(w, r, "login.page.tmpl", &models.TemplateData{
|
||||
Form: form,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@ -490,5 +501,321 @@ func (m *Repository) PostShowLogin(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
m.App.Session.Put(r.Context(), "user_id", id)
|
||||
m.App.Session.Put(r.Context(), "flash", "Logged in successfully")
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// Logout logs a user out
|
||||
func (m *Repository) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
m.App.Session.Destroy(r.Context())
|
||||
m.App.Session.RenewToken(r.Context())
|
||||
http.Redirect(w, r, "/user/login", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (m *Repository) AdminDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
render.Template(w, r, "admin-dashboard.page.tmpl", &models.TemplateData{})
|
||||
}
|
||||
|
||||
// AdminNewReservations shows all new reservations in admin tool
|
||||
func (m *Repository) AdminNewReservations(w http.ResponseWriter, r *http.Request) {
|
||||
reservations, err := m.DB.AllNewReservations()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
data["reservations"] = reservations
|
||||
|
||||
render.Template(w, r, "admin-new-reservations.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminNewReservations shows all reservations in admin tool
|
||||
func (m *Repository) AdminAllReservations(w http.ResponseWriter, r *http.Request) {
|
||||
reservations, err := m.DB.AllReservations()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
data := make(map[string]interface{})
|
||||
data["reservations"] = reservations
|
||||
|
||||
render.Template(w, r, "admin-all-reservations.page.tmpl", &models.TemplateData{
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminShowReservation shows the detail of a reservation
|
||||
func (m *Repository) AdminShowReservation(w http.ResponseWriter, r *http.Request) {
|
||||
exploded := strings.Split(r.RequestURI, "/")
|
||||
id, err := strconv.Atoi(exploded[4])
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
src := exploded[3]
|
||||
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["src"] = src
|
||||
|
||||
year := r.URL.Query().Get("y")
|
||||
month := r.URL.Query().Get("m")
|
||||
|
||||
stringMap["month"] = month
|
||||
stringMap["year"] = year
|
||||
|
||||
// get reservation from the database
|
||||
res, err := m.DB.GetReservationByID(id)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["reservation"] = res
|
||||
|
||||
render.Template(w, r, "admin-reservations-show.page.tmpl", &models.TemplateData{
|
||||
StringMap: stringMap,
|
||||
Data: data,
|
||||
Form: forms.New(nil),
|
||||
})
|
||||
}
|
||||
|
||||
// AdminShowReservation shows the detail of a reservation
|
||||
func (m *Repository) AdminPostShowReservation(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
exploded := strings.Split(r.RequestURI, "/")
|
||||
id, err := strconv.Atoi(exploded[4])
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
src := exploded[3]
|
||||
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["src"] = src
|
||||
|
||||
// get reservation from the database
|
||||
res, err := m.DB.GetReservationByID(id)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
res.FirstName = r.Form.Get("first_name")
|
||||
res.LastName = r.Form.Get("last_name")
|
||||
res.Email = r.Form.Get("email")
|
||||
res.Phone = r.Form.Get("phone")
|
||||
|
||||
// TODO error checking
|
||||
|
||||
err = m.DB.UpdateReservation(res)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
month := r.Form.Get("month")
|
||||
year := r.Form.Get("year")
|
||||
|
||||
m.App.Session.Put(r.Context(), "flash", "Changes saved")
|
||||
if year == "" {
|
||||
log.Println(year, month)
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// AdminReservationsCalendar displays the reservation calendar
|
||||
func (m *Repository) AdminReservationsCalendar(w http.ResponseWriter, r *http.Request) {
|
||||
now := time.Now()
|
||||
|
||||
if r.URL.Query().Get("y") != "" {
|
||||
year, _ := strconv.Atoi(r.URL.Query().Get("y"))
|
||||
month, _ := strconv.Atoi(r.URL.Query().Get("m"))
|
||||
now = time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
data["now"] = now
|
||||
|
||||
next := now.AddDate(0, 1, 0)
|
||||
last := now.AddDate(0, -1, 0)
|
||||
|
||||
nextMonth := next.Format("01")
|
||||
nextMonthYear := next.Format("2006")
|
||||
|
||||
lastMonth := last.Format("01")
|
||||
lastMonthYear := last.Format("2006")
|
||||
|
||||
stringMap := make(map[string]string)
|
||||
stringMap["next_month"] = nextMonth
|
||||
stringMap["next_month_year"] = nextMonthYear
|
||||
stringMap["last_month"] = lastMonth
|
||||
stringMap["last_month_year"] = lastMonthYear
|
||||
stringMap["this_month"] = now.Format("01")
|
||||
stringMap["this_month_year"] = now.Format("2006")
|
||||
|
||||
// get the first and last days from the month
|
||||
currentYear, currentMonth, _ := now.Date()
|
||||
currentLocation := now.Location()
|
||||
firstOfMonth := time.Date(currentYear, currentMonth, 1, 0, 0, 0, 0, currentLocation)
|
||||
lastOfMonth := firstOfMonth.AddDate(0, 1, -1)
|
||||
|
||||
intMap := make(map[string]int)
|
||||
intMap["days_in_month"] = lastOfMonth.Day()
|
||||
|
||||
rooms, err := m.DB.AllRooms()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
data["rooms"] = rooms
|
||||
|
||||
for _, x := range rooms {
|
||||
// create maps
|
||||
reservationMap := make(map[string]int)
|
||||
blockMap := make(map[string]int)
|
||||
|
||||
for d := firstOfMonth; !d.After(lastOfMonth); d = d.AddDate(0, 0, 1) {
|
||||
reservationMap[d.Format("2006-01-2")] = 0
|
||||
blockMap[d.Format("2006-01-2")] = 0
|
||||
}
|
||||
|
||||
// get all the restrictions for the current room
|
||||
restrictions, err := m.DB.GetRestrictionsForRoomByDate(x.ID, firstOfMonth, lastOfMonth)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, y := range restrictions {
|
||||
if y.ReservationID > 0 {
|
||||
// it's a reservation
|
||||
for d := y.StartDate; !d.After(y.EndDate); d = d.AddDate(0, 0, 1) {
|
||||
reservationMap[d.Format("2006-01-2")] = y.ReservationID
|
||||
}
|
||||
} else {
|
||||
// it's a block.
|
||||
// NOTE:A block can only be set day by day
|
||||
blockMap[y.StartDate.Format("2006-01-2")] = y.ID
|
||||
}
|
||||
}
|
||||
data[fmt.Sprintf("reservation_map_%d", x.ID)] = reservationMap
|
||||
data[fmt.Sprintf("block_map_%d", x.ID)] = blockMap
|
||||
|
||||
m.App.Session.Put(r.Context(), fmt.Sprintf("block_map_%d", x.ID), blockMap)
|
||||
}
|
||||
|
||||
render.Template(w, r, "admin-reservations-calendar.page.tmpl",
|
||||
&models.TemplateData{
|
||||
StringMap: stringMap,
|
||||
Data: data,
|
||||
IntMap: intMap,
|
||||
})
|
||||
}
|
||||
|
||||
// AdminProcessReservation marks a reservation as processed
|
||||
func (m *Repository) AdminProcessReservation(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
src := chi.URLParam(r, "src")
|
||||
|
||||
_ = m.DB.UpdateProcessedForReservation(id, 1)
|
||||
|
||||
year := r.URL.Query().Get("y")
|
||||
month := r.URL.Query().Get("m")
|
||||
|
||||
m.App.Session.Put(r.Context(), "flash", "Reservation marked as processed")
|
||||
|
||||
if year == "" {
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// AdminDeleteReservation deletes a reservation
|
||||
func (m *Repository) AdminDeleteReservation(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
src := chi.URLParam(r, "src")
|
||||
|
||||
_ = m.DB.DeleteReservation(id)
|
||||
|
||||
year := r.URL.Query().Get("y")
|
||||
month := r.URL.Query().Get("m")
|
||||
|
||||
m.App.Session.Put(r.Context(), "flash", fmt.Sprintf("Reservation %d deleted", id))
|
||||
|
||||
if year == "" {
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-%s", src), http.StatusSeeOther)
|
||||
} else {
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%s&m=%s", year, month), http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
// AdminPostReservationsCalendar handles post of reservation calendar
|
||||
func (m *Repository) AdminPostReservationsCalendar(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
year, _ := strconv.Atoi(r.Form.Get("y"))
|
||||
month, _ := strconv.Atoi(r.Form.Get("m"))
|
||||
|
||||
// process blocks
|
||||
rooms, err := m.DB.AllRooms()
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
form := forms.New(r.PostForm)
|
||||
for _, x := range rooms {
|
||||
// Get the block map from the session. Loop through entire map, if we
|
||||
// have an entry in the map taht does not exist in our posted data,
|
||||
// and if the restriction id > 0, then it is a block we need to remove.
|
||||
curMap, _ := m.App.Session.Get(r.Context(), fmt.Sprintf("block_map_%d", x.ID)).(map[string]int)
|
||||
// TODO check session get ok
|
||||
for name, value := range curMap {
|
||||
// ok will be false if the value is not in the map
|
||||
if val, ok := curMap[name]; ok {
|
||||
// only pay attention to values > 0, and that are not in the form post
|
||||
// the rest are just placeholders for days without blocks
|
||||
if val > 0 {
|
||||
if !form.Has(fmt.Sprintf("remove_block_%d_%s", x.ID, name)) {
|
||||
err := m.DB.DeleteBlockByID(value)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle new blocks
|
||||
for name := range r.PostForm {
|
||||
if strings.HasPrefix(name, "add_block") {
|
||||
exploded := strings.Split(name, "_")
|
||||
roomID, _ := strconv.Atoi(exploded[2])
|
||||
startDate, _ := time.Parse("2006-01-2", exploded[3])
|
||||
// insert a new block
|
||||
err := m.DB.InsertBlockForRoom(roomID, startDate)
|
||||
if err != nil {
|
||||
helpers.ServerError(w, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.App.Session.Put(r.Context(), "flash", "Changes saved")
|
||||
http.Redirect(w, r, fmt.Sprintf("/admin/reservations-calendar?y=%d&m=%d", year, month), http.StatusSeeOther)
|
||||
}
|
||||
|
@ -47,6 +47,13 @@ var theTests = []struct {
|
||||
{"ms", "/majors-suite", "GET", http.StatusOK},
|
||||
{"sa", "/availability", "GET", http.StatusOK},
|
||||
{"contact", "/contact", "GET", http.StatusOK},
|
||||
{"non-existant", "/some/link", "GET", http.StatusNotFound},
|
||||
{"login", "/user/login", "GET", http.StatusOK},
|
||||
{"logout", "/user/logout", "GET", http.StatusOK},
|
||||
{"dashboard", "/admin/dashboard", "GET", http.StatusOK},
|
||||
{"all reservations", "/admin/reservations-all", "GET", http.StatusOK},
|
||||
{"new reservations", "/admin/reservations-new", "GET", http.StatusOK},
|
||||
{"show reservation", "/admin/reservations/new/1/show", "GET", http.StatusOK},
|
||||
}
|
||||
|
||||
func TestHandlers(t *testing.T) {
|
||||
@ -75,8 +82,8 @@ var makeReservationTests = []struct {
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", 1, http.StatusOK},
|
||||
{"no session", 0, http.StatusTemporaryRedirect},
|
||||
{"non-existant room", 100, http.StatusTemporaryRedirect},
|
||||
{"no session", 0, http.StatusSeeOther},
|
||||
{"non-existant room", 100, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func TestRepository_MakeReservation(t *testing.T) {
|
||||
@ -141,9 +148,9 @@ var postMakeReservationTests = []struct {
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
{"no_post_data", []postData{}, http.StatusTemporaryRedirect},
|
||||
{"no_post_data", []postData{}, http.StatusSeeOther},
|
||||
{
|
||||
"missing first name",
|
||||
[]postData{
|
||||
@ -193,7 +200,7 @@ var postMakeReservationTests = []struct {
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
{
|
||||
"insert room restriction error",
|
||||
@ -206,7 +213,7 @@ var postMakeReservationTests = []struct {
|
||||
{key: "start_date", value: "2050-01-01"},
|
||||
{key: "end_date", value: "2050-01-02"},
|
||||
},
|
||||
http.StatusTemporaryRedirect,
|
||||
http.StatusSeeOther,
|
||||
},
|
||||
}
|
||||
|
||||
@ -374,7 +381,7 @@ var reservationSummaryTests = []struct {
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", true, http.StatusOK},
|
||||
{"nok", false, http.StatusTemporaryRedirect},
|
||||
{"nok", false, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_ReservationSummary(t *testing.T) {
|
||||
@ -426,7 +433,7 @@ var postAvailabilityTests = []struct {
|
||||
{"database error", []postData{
|
||||
{key: "start", value: "2050-01-03"},
|
||||
{key: "end", value: "2050-01-04"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
{"no availability", []postData{
|
||||
{key: "start", value: "2050-01-05"},
|
||||
{key: "end", value: "2050-01-06"},
|
||||
@ -434,12 +441,12 @@ var postAvailabilityTests = []struct {
|
||||
{"wrong start date", []postData{
|
||||
{key: "start", value: "2050-05"},
|
||||
{key: "end", value: "2050-01-06"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{
|
||||
{key: "start", value: "2050-01-05"},
|
||||
{key: "end", value: "01-06"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
{"wrong end date", []postData{}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{}, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_PostAvailability(t *testing.T) {
|
||||
@ -481,8 +488,8 @@ var chooseRoomTests = []struct {
|
||||
expectedStatusCode int
|
||||
}{
|
||||
{"ok", "/choose-room/1", true, http.StatusSeeOther},
|
||||
{"wrong room id", "/choose-room/1wrong", true, http.StatusTemporaryRedirect},
|
||||
{"no session", "/choose-room/1", false, http.StatusTemporaryRedirect},
|
||||
{"wrong room id", "/choose-room/1wrong", true, http.StatusSeeOther},
|
||||
{"no session", "/choose-room/1", false, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_ChooseRoom(t *testing.T) {
|
||||
@ -531,17 +538,17 @@ var bookRoomTests = []struct {
|
||||
{key: "id", value: "1"},
|
||||
{key: "s", value: "20-01-01"},
|
||||
{key: "e", value: "2050-01-02"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong end date", []postData{
|
||||
{key: "id", value: "1"},
|
||||
{key: "s", value: "2050-01-01"},
|
||||
{key: "e", value: "2050-0-02"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
{"wrong room id", []postData{
|
||||
{key: "id", value: "w"},
|
||||
{key: "s", value: "2050-01-01"},
|
||||
{key: "e", value: "2050-01-02"},
|
||||
}, http.StatusTemporaryRedirect},
|
||||
}, http.StatusSeeOther},
|
||||
}
|
||||
|
||||
func Test_BookRoom(t *testing.T) {
|
||||
@ -567,6 +574,64 @@ func Test_BookRoom(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Login
|
||||
|
||||
var loginTests = []struct {
|
||||
name string
|
||||
email string
|
||||
expectedStatusCode int
|
||||
expectedHTML string
|
||||
expectedLocation string
|
||||
}{
|
||||
{"valid-credentials", "a@b.c", http.StatusSeeOther, "", "/admin/dashboard"},
|
||||
{"invalid-credentials", "invalid@b.c", http.StatusSeeOther, "", "/user/login"},
|
||||
{"invalid-data", "a@b", http.StatusOK, `action="/user/login"`, ""},
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
// range through all tests
|
||||
for _, e := range loginTests {
|
||||
postedData := url.Values{}
|
||||
postedData.Add("email", e.email)
|
||||
postedData.Add("password", "password")
|
||||
|
||||
// create request
|
||||
req, _ := http.NewRequest("POST", "/user/login", strings.NewReader(postedData.Encode()))
|
||||
ctx := getCtx(req)
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
// set the header
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// call the handler
|
||||
handler := http.HandlerFunc(Repo.PostShowLogin)
|
||||
handler.ServeHTTP(rr, req)
|
||||
|
||||
if rr.Code != e.expectedStatusCode {
|
||||
t.Errorf("failed %s: expected code %d, but got %d", e.name, e.expectedStatusCode, rr.Code)
|
||||
}
|
||||
|
||||
if e.expectedLocation != "" {
|
||||
// get the URL from test
|
||||
actualLoc, _ := rr.Result().Location()
|
||||
if actualLoc.String() != e.expectedLocation {
|
||||
t.Errorf("failed %s: expected location %s, but got location %s", e.name, e.expectedLocation, actualLoc.String())
|
||||
}
|
||||
}
|
||||
|
||||
// checking for expected values in HTML
|
||||
if e.expectedHTML != "" {
|
||||
// read the response body into a string
|
||||
html := rr.Body.String()
|
||||
if !strings.Contains(html, e.expectedHTML) {
|
||||
t.Errorf("failed %s: expected html contains %s, but got html %s", e.name, e.expectedHTML, html)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// }}}
|
||||
// {{{ Test Helpers
|
||||
|
||||
|
@ -19,7 +19,12 @@ import (
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{}
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": render.HumanDate,
|
||||
"formatDate": render.FormatDate,
|
||||
"iterate": render.Iterate,
|
||||
"add": render.Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app config.AppConfig
|
||||
@ -28,6 +33,10 @@ var (
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
gob.Register(models.Reservation{})
|
||||
gob.Register(models.User{})
|
||||
gob.Register(models.Room{})
|
||||
gob.Register(models.Restriction{})
|
||||
gob.Register(map[string]int{})
|
||||
// change this to true when in production
|
||||
app.InProduction = false
|
||||
|
||||
@ -85,6 +94,25 @@ func getRoutes() http.Handler {
|
||||
mux.Post("/make-reservation", Repo.PostMakeReservation)
|
||||
mux.Get("/reservation-summary", Repo.ReservationSummary)
|
||||
|
||||
mux.Get("/choose-room/{id}", Repo.ChooseRoom)
|
||||
mux.Get("/book-room", Repo.BookRoom)
|
||||
|
||||
mux.Get("/user/login", Repo.ShowLogin)
|
||||
mux.Post("/user/login", Repo.PostShowLogin)
|
||||
mux.Get("/user/logout", Repo.Logout)
|
||||
|
||||
mux.Get("/admin/dashboard", Repo.AdminDashboard)
|
||||
|
||||
mux.Get("/admin/reservations-new", Repo.AdminNewReservations)
|
||||
mux.Get("/admin/reservations-all", Repo.AdminAllReservations)
|
||||
mux.Get("/admin/reservations-calendar", Repo.AdminReservationsCalendar)
|
||||
mux.Post("/admin/reservations-calendar", Repo.AdminPostReservationsCalendar)
|
||||
mux.Get("/admin/process-reservation/{src}/{id}/do", Repo.AdminProcessReservation)
|
||||
mux.Get("/admin/delete-reservation/{src}/{id}/do", Repo.AdminDeleteReservation)
|
||||
|
||||
mux.Get("/admin/reservations/{src}/{id}/show", Repo.AdminShowReservation)
|
||||
mux.Post("/admin/reservations/{src}/{id}", Repo.AdminPostShowReservation)
|
||||
|
||||
fileServer := http.FileServer(http.Dir("./static/"))
|
||||
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))
|
||||
|
||||
|
@ -24,3 +24,7 @@ func ServerError(w http.ResponseWriter, err error) {
|
||||
app.ErrorLog.Println(trace)
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func IsAuthenticated(r *http.Request) bool {
|
||||
return app.Session.Exists(r.Context(), "user_id")
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ type Reservation struct {
|
||||
Room Room
|
||||
ID int
|
||||
RoomID int
|
||||
Processed int
|
||||
}
|
||||
|
||||
// RoomRestriction is the room restriction model
|
||||
|
@ -8,9 +8,10 @@ type TemplateData struct {
|
||||
IntMap map[string]int
|
||||
FloatMap map[string]float32
|
||||
Data map[string]interface{}
|
||||
Form *forms.Form
|
||||
CSRFToken string
|
||||
Flash string
|
||||
Warning string
|
||||
Error string
|
||||
Form *forms.Form
|
||||
IsAuthenticated int
|
||||
}
|
||||
|
@ -10,11 +10,17 @@ import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
var functions = template.FuncMap{}
|
||||
var functions = template.FuncMap{
|
||||
"humanDate": HumanDate,
|
||||
"formatDate": FormatDate,
|
||||
"iterate": Iterate,
|
||||
"add": Add,
|
||||
}
|
||||
|
||||
var (
|
||||
app *config.AppConfig
|
||||
@ -26,12 +32,38 @@ func NewRenderer(a *config.AppConfig) {
|
||||
app = a
|
||||
}
|
||||
|
||||
// HumanDate returns formatted time
|
||||
func HumanDate(t time.Time) string {
|
||||
return t.Format("2006-01-02")
|
||||
}
|
||||
|
||||
func FormatDate(t time.Time, f string) string {
|
||||
return t.Format(f)
|
||||
}
|
||||
|
||||
// Iterate returns a slice of ints, starting at 1, going to count
|
||||
func Iterate(count int) []int {
|
||||
var i int
|
||||
var items []int
|
||||
for i = 0; i < count; i++ {
|
||||
items = append(items, i)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func Add(a, b int) int {
|
||||
return a + b
|
||||
}
|
||||
|
||||
// AddDefaultData adds default template data
|
||||
func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
|
||||
td.Flash = app.Session.PopString(r.Context(), "flash")
|
||||
td.Warning = app.Session.PopString(r.Context(), "warning")
|
||||
td.Error = app.Session.PopString(r.Context(), "error")
|
||||
td.CSRFToken = nosurf.Token(r)
|
||||
if app.Session.Exists(r.Context(), "user_id") {
|
||||
td.IsAuthenticated = 1
|
||||
}
|
||||
return td
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"go-udemy-web-1/internal/models"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@ -113,6 +114,8 @@ func (m *postgresDBRepo) SearchAvailabilityForAllRooms(start, end time.Time) ([]
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var room models.Room
|
||||
|
||||
@ -211,3 +214,281 @@ func (m *postgresDBRepo) Authenticate(email, testPassword string) (int, string,
|
||||
}
|
||||
return id, hashedPassword, nil
|
||||
}
|
||||
|
||||
// AllReservations returns a slice of all reservations
|
||||
func (m *postgresDBRepo) AllReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
order by r.start_date asc`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var i models.Reservation
|
||||
err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.Room.ID,
|
||||
&i.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
reservations = append(reservations, i)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// AllNewReservations returns a slice of all new reservations
|
||||
func (m *postgresDBRepo) AllNewReservations() ([]models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var reservations []models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
where r.processed = 0
|
||||
order by r.start_date asc`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var i models.Reservation
|
||||
err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.FirstName,
|
||||
&i.LastName,
|
||||
&i.Email,
|
||||
&i.Phone,
|
||||
&i.StartDate,
|
||||
&i.EndDate,
|
||||
&i.RoomID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Processed,
|
||||
&i.Room.ID,
|
||||
&i.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return reservations, err
|
||||
}
|
||||
reservations = append(reservations, i)
|
||||
}
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// GetReservationByID returns one reservation by ID
|
||||
func (m *postgresDBRepo) GetReservationByID(id int) (models.Reservation, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var res models.Reservation
|
||||
|
||||
query := `select r.id, r.first_name, r.last_name, r.email, r.phone,
|
||||
r.start_date, r.end_date, r.room_id, r.created_at,
|
||||
r.updated_at, r.processed, rm.id, rm.room_name
|
||||
from reservations r
|
||||
left join rooms rm on (r.room_id = rm.id)
|
||||
where r.id = $1`
|
||||
|
||||
row := m.DB.QueryRowContext(ctx, query, id)
|
||||
err := row.Scan(
|
||||
&res.ID,
|
||||
&res.FirstName,
|
||||
&res.LastName,
|
||||
&res.Email,
|
||||
&res.Phone,
|
||||
&res.StartDate,
|
||||
&res.EndDate,
|
||||
&res.RoomID,
|
||||
&res.CreatedAt,
|
||||
&res.UpdatedAt,
|
||||
&res.Processed,
|
||||
&res.Room.ID,
|
||||
&res.Room.RoomName,
|
||||
)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateReservation updates a user in the database
|
||||
func (m *postgresDBRepo) UpdateReservation(r models.Reservation) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `update reservations set first_name = $1, last_name = $2, email = $3, phone = $4, updated_at = $5
|
||||
where id = $6`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, r.FirstName, r.LastName, r.Email,
|
||||
r.Phone, time.Now(), r.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteReservation deletes one reservation by ID
|
||||
func (m *postgresDBRepo) DeleteReservation(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `delete from reservations where id = $1`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProcessedForReservation set processed for a reservation
|
||||
func (m *postgresDBRepo) UpdateProcessedForReservation(id, processed int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `update reservations set processed = $1 where id = $2`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, processed, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllRooms gets all rooms
|
||||
func (m *postgresDBRepo) AllRooms() ([]models.Room, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
var rooms []models.Room
|
||||
|
||||
query := `select id, room_name, created_at, updated_at from rooms order by room_name`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return rooms, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var rm models.Room
|
||||
err := rows.Scan(
|
||||
&rm.ID,
|
||||
&rm.RoomName,
|
||||
&rm.CreatedAt,
|
||||
&rm.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return rooms, err
|
||||
}
|
||||
rooms = append(rooms, rm)
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return rooms, err
|
||||
}
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
|
||||
func (m *postgresDBRepo) GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var restrictions []models.RoomRestriction
|
||||
|
||||
// coalesce use 0 if null
|
||||
query := `select id, coalesce(reservation_id, 0), restriction_id, room_id, start_date, end_date
|
||||
from room_restrictions where $1 < end_date and $2 >= start_date
|
||||
and room_id = $3`
|
||||
|
||||
rows, err := m.DB.QueryContext(ctx, query, start, end, roomId)
|
||||
if err != nil {
|
||||
return restrictions, err
|
||||
}
|
||||
defer rows.Close() // To avoid memory leak
|
||||
|
||||
for rows.Next() {
|
||||
var r models.RoomRestriction
|
||||
err := rows.Scan(
|
||||
&r.ID,
|
||||
&r.ReservationID,
|
||||
&r.RestrictionID,
|
||||
&r.RoomID,
|
||||
&r.StartDate,
|
||||
&r.EndDate,
|
||||
)
|
||||
if err != nil {
|
||||
return restrictions, err
|
||||
}
|
||||
restrictions = append(restrictions, r)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return restrictions, err
|
||||
}
|
||||
return restrictions, nil
|
||||
}
|
||||
|
||||
// InsertBlockForRoom inserts a room restriction
|
||||
func (m *postgresDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `insert into room_restrictions (start_date, end_date, room_id, restriction_id,
|
||||
created_at, updated_at)
|
||||
values ($1, $2, $3, $4, $5, $6)`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, startDate, startDate.AddDate(0, 0, 1), id, 2, time.Now(), time.Now())
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBlockByID deletes a block by ID
|
||||
func (m *postgresDBRepo) DeleteBlockByID(id int) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
query := `delete from room_restrictions where id = $1`
|
||||
|
||||
_, err := m.DB.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -79,5 +79,65 @@ func (m *testDBRepo) UpdateUser(u models.User) error {
|
||||
|
||||
// Authenticate authenticates a user
|
||||
func (m *testDBRepo) Authenticate(email, testPassword string) (int, string, error) {
|
||||
if email == "a@b.c" {
|
||||
return 1, "", nil
|
||||
}
|
||||
return 0, "", errors.New("deliberate errors")
|
||||
}
|
||||
|
||||
// AllReservations returns a slice of all reservations
|
||||
func (m *testDBRepo) AllReservations() ([]models.Reservation, error) {
|
||||
var reservations []models.Reservation
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// AllNewReservations returns a slice of all new reservations
|
||||
func (m *testDBRepo) AllNewReservations() ([]models.Reservation, error) {
|
||||
var reservations []models.Reservation
|
||||
|
||||
return reservations, nil
|
||||
}
|
||||
|
||||
// GetReservationByID returns one reservation by ID
|
||||
func (m *testDBRepo) GetReservationByID(id int) (models.Reservation, error) {
|
||||
var res models.Reservation
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UpdateReservation updates a user in the database
|
||||
func (m *testDBRepo) UpdateReservation(r models.Reservation) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDBRepo) DeleteReservation(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProcessedForReservation set processed for a reservation
|
||||
func (m *testDBRepo) UpdateProcessedForReservation(id, processed int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testDBRepo) AllRooms() ([]models.Room, error) {
|
||||
var rooms []models.Room
|
||||
|
||||
return rooms, nil
|
||||
}
|
||||
|
||||
// GetRestrictionsForRoomByDate returns restrictions for a room by date range
|
||||
func (m *testDBRepo) GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error) {
|
||||
var restrictions []models.RoomRestriction
|
||||
return restrictions, nil
|
||||
}
|
||||
|
||||
// InsertBlockForRoom inserts a room restriction
|
||||
func (m *testDBRepo) InsertBlockForRoom(id int, startDate time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBlockByID deletes a block by ID
|
||||
func (m *testDBRepo) DeleteBlockByID(id int) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -16,4 +16,14 @@ type DatabaseRepo interface {
|
||||
GetUserByID(id int) (models.User, error)
|
||||
UpdateUser(u models.User) error
|
||||
Authenticate(email, testPassword string) (int, string, error)
|
||||
AllReservations() ([]models.Reservation, error)
|
||||
AllNewReservations() ([]models.Reservation, error)
|
||||
GetReservationByID(id int) (models.Reservation, error)
|
||||
UpdateReservation(r models.Reservation) error
|
||||
DeleteReservation(id int) error
|
||||
UpdateProcessedForReservation(id, processed int) error
|
||||
AllRooms() ([]models.Room, error)
|
||||
GetRestrictionsForRoomByDate(roomId int, start, end time.Time) ([]models.RoomRestriction, error)
|
||||
InsertBlockForRoom(id int, startDate time.Time) error
|
||||
DeleteBlockByID(id int) error
|
||||
}
|
||||
|
@ -0,0 +1 @@
|
||||
drop_column("reservations", "processed")
|
@ -0,0 +1 @@
|
||||
add_column("reservations", "processed", "integer", {"default": 0})
|
2
run.sh
@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
go build -o bookings cmd/web/*go && ./bookings
|
||||
go build -o bookings cmd/web/*go && ./bookings -cache=false -production=false
|
||||
|
BIN
static/admin/css/.DS_Store
vendored
Normal file
1
static/admin/css/maps/style.css.map
Normal file
22631
static/admin/css/style.css
Normal file
BIN
static/admin/documentation/.DS_Store
vendored
Normal file
137
static/admin/documentation/documentation.html
Normal file
@ -0,0 +1,137 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>RoyalUI Admin Dashboard</title>
|
||||
<!-- plugins:css -->
|
||||
<link rel="stylesheet" href="../vendors/ti-icons/css/themify-icons.css">
|
||||
<link rel="stylesheet" href="../vendors/base/vendor.bundle.base.css">
|
||||
<!-- endinject -->
|
||||
<!-- plugin css for this page -->
|
||||
<!-- End plugin css for this page -->
|
||||
<!-- inject:css -->
|
||||
<link rel="stylesheet" href="../css/style.css">
|
||||
<!-- endinject -->
|
||||
<link rel="shortcut icon" href="../images/favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-scroller">
|
||||
<div class="container-fluid page-body-wrapper full-page-wrapper">
|
||||
<div class="main-panel w-100 documentation">
|
||||
<div class="content-wrapper">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12 doc-header">
|
||||
<a class="btn btn-success" href="../index.html"><i class="mdi mdi-home me-2"></i>Back to home</a>
|
||||
<h1 class="text-primary mt-4">Documentation</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row doc-content">
|
||||
<div class="col-12 col-md-10 offset-md-1">
|
||||
<div class="col-12 grid-margin" id="doc-intro">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mb-4 mt-4">Introduction</h3>
|
||||
<p>RoyalUI Admin is a responsive HTML template that is based on the CSS framework Bootstrap 5 and it is built with Sass. Sass compiler makes it easier to code and customize. If you are unfamiliar with Bootstrap or Sass, visit their
|
||||
website and read through the documentation. All of Bootstrap components have been modified to fit the style of RoyalUI Admin and provide a consistent look throughout the template.</p>
|
||||
<p>Before you start working with the template, we suggest you go through the pages that are bundled with the theme. Most of the template example pages contain quick tips on how to create or use a component which can
|
||||
be really helpful when you need to create something on the fly.</p>
|
||||
<p class="d-inline"><strong>Note</strong>: We are trying our best to document how to use the template. If you think that something is missing from the documentation, please do not hesitate to tell us about it. If you have any questions or issues regarding this theme please email us at <a class="d-inline text-info" href="mailto:info@bootstrapdash.com">info@bootstrapdash.com</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 grid-margin" id="doc-started">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h3 class="mb-4">Getting started</h3>
|
||||
<p>You can directly use the compiled and ready-to-use the version of the template. But in case you plan to customize the template extensively the template allows you to do so.</p>
|
||||
<p>Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations:</p>
|
||||
<pre>
|
||||
RoyalUI/
|
||||
├── template/
|
||||
├── css/
|
||||
├── fonts/
|
||||
├── images/
|
||||
├── js/
|
||||
├── pages/
|
||||
├── partials/
|
||||
├── index.html
|
||||
├── scss/
|
||||
├── vendors/
|
||||
├── gulpfile.js
|
||||
├── package.json
|
||||
├── documentation/
|
||||
├── CHANGELOG.md</pre>
|
||||
<p class="mt-1">Note: The root folder denoted further in this documentation refers to the 'template' folder inside the downloaded folder</p>
|
||||
<div class="alert alert-success mt-4 d-flex align-items-center" role="alert">
|
||||
<i class="ti-info-alt me-4"></i>
|
||||
<p>We have bundled up the vendor files needed for demo purpose into a folder 'vendors', you may not need all those vendors or may need to add more vendors in your application. If you want to make any change in the vendor package files, you can either change the src path for related tasks in the file gulpfile.js and run the task <code> bundleVendors </code> to rebuild the vendor files or manually edit the vendor folder.</p>
|
||||
</div>
|
||||
<hr class="mt-5">
|
||||
<h4 class="mt-4">Installation</h4>
|
||||
<p class="mb-0">
|
||||
You need to install package files/Dependencies for this project if you want to customize it. To do this, you must have <span class="font-weight-bold">node and npm</span> installed in your computer.
|
||||
</p>
|
||||
<p class="mb-0">Installation guide of the node can be found <a href="https://nodejs.org/en/">here</a>. As npm comes bundled with a node, a separate installation of npm is not needed.</p>
|
||||
<p>
|
||||
If you have installed them, just go to the root folder and run the following command in your command prompt or terminal (for the mac users).
|
||||
</p>
|
||||
<pre class="shell-mode">
|
||||
npm install</pre>
|
||||
<p class="mt-4">
|
||||
This will install the dev dependencies in the local <span class="font-weight-bold">node_modules</span> folder in your root directory.
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
Then you will need to install <span class="font-weight-bold">Gulp</span>. We use the Gulp task manager for the development processes. Gulp will watch for changes to the SCSS files and automatically compile the files to CSS.
|
||||
</p>
|
||||
<p>Getting started with Gulp is pretty simple. The <a href="https://gulpjs.com/" target="_blank">Gulp</a> site is a great place to get information on installing Gulp if you need more information. You need to first install Gulp-cli in your machine using the below command.</p>
|
||||
<pre class="shell-mode">
|
||||
npm install -g gulp-cli</pre>
|
||||
<p class="mt-4">This installs Gulp-cli globally to your machine. The other thing that Gulp requires, which, is really what does all the work, is the gulpfile.js. In this file, you set up all of your tasks that you will run.</p>
|
||||
<p>Don't worry. We have this file already created for you!</p>
|
||||
<p>To run this project in development mode enter the following command below. This will start the file watch by gulp and whenever a file is modified, the SCSS files will be compiled to create the CSS file.</p>
|
||||
<pre class="shell-mode">
|
||||
gulp serve</pre>
|
||||
<div class="alert alert-warning mt-4" role="alert">
|
||||
<i class="ti-info-alt-outline"></i>It is important to run <code>gulp serve</code> command from the directory where the gulpfile.js is located.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- partial:../partials/_footer.html -->
|
||||
|
||||
<footer class="footer">
|
||||
<div class="d-sm-flex justify-content-center justify-content-sm-between">
|
||||
<span class="text-muted text-center text-sm-left d-block d-sm-inline-block">Copyright © 2018 <a href="https://www.bootstrapdash.com/" target="_blank">Bootstrapdash</a>. All rights reserved.</span>
|
||||
<span class="float-none float-sm-right d-block mt-1 mt-sm-0 text-center">Hand-crafted & made with <i class="ti-heart text-danger ms-1"></i></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- partial -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- plugins:js -->
|
||||
<script src="../vendors/base/vendor.bundle.base.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- inject:js -->
|
||||
<script src="../js/off-canvas.js"></script>
|
||||
<script src="../js/hoverable-collapse.js"></script>
|
||||
<script src="../js/template.js"></script>
|
||||
<script src="../js/todolist.js"></script>
|
||||
<!-- endinject -->
|
||||
<!-- Custom js for this page-->
|
||||
<script src="../js/documentation.js"></script>
|
||||
<!-- End custom js for this page-->
|
||||
</body>
|
||||
|
||||
</html>
|
BIN
static/admin/fonts/.DS_Store
vendored
Normal file
BIN
static/admin/fonts/Roboto/.DS_Store
vendored
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Black.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Bold.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Light.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Medium.woff2
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.eot
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.ttf
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff
Normal file
BIN
static/admin/fonts/Roboto/Roboto-Regular.woff2
Normal file
176
static/admin/gulpfile.js
Normal file
@ -0,0 +1,176 @@
|
||||
'use strict'
|
||||
|
||||
var gulp = require('gulp');
|
||||
var browserSync = require('browser-sync').create();
|
||||
var sass = require('gulp-sass');
|
||||
var del = require('del');
|
||||
var replace = require('gulp-replace');
|
||||
var injectPartials = require('gulp-inject-partials');
|
||||
var inject = require('gulp-inject');
|
||||
var sourcemaps = require('gulp-sourcemaps');
|
||||
var concat = require('gulp-concat');
|
||||
var merge = require('merge-stream');
|
||||
|
||||
gulp.paths = {
|
||||
dist: 'dist',
|
||||
};
|
||||
|
||||
var paths = gulp.paths;
|
||||
|
||||
gulp.task('sass', function () {
|
||||
return gulp.src('./scss/**/style.scss')
|
||||
.pipe(sourcemaps.init())
|
||||
.pipe(sass({outputStyle: 'expanded'}).on('error', sass.logError))
|
||||
.pipe(sourcemaps.write('./maps'))
|
||||
.pipe(gulp.dest('./css'))
|
||||
.pipe(browserSync.stream());
|
||||
});
|
||||
|
||||
// Static Server + watching scss/html files
|
||||
gulp.task('serve', gulp.series('sass', function() {
|
||||
|
||||
browserSync.init({
|
||||
port: 3100,
|
||||
server: "./",
|
||||
ghostMode: false,
|
||||
notify: false
|
||||
});
|
||||
|
||||
gulp.watch('scss/**/*.scss', gulp.series('sass'));
|
||||
gulp.watch('**/*.html').on('change', browserSync.reload);
|
||||
gulp.watch('js/**/*.js').on('change', browserSync.reload);
|
||||
|
||||
}));
|
||||
|
||||
|
||||
|
||||
// Static Server without watching scss files
|
||||
gulp.task('serve:lite', function() {
|
||||
|
||||
browserSync.init({
|
||||
server: "./",
|
||||
ghostMode: false,
|
||||
notify: false
|
||||
});
|
||||
|
||||
gulp.watch('**/*.css').on('change', browserSync.reload);
|
||||
gulp.watch('**/*.html').on('change', browserSync.reload);
|
||||
gulp.watch('js/**/*.js').on('change', browserSync.reload);
|
||||
|
||||
});
|
||||
|
||||
|
||||
gulp.task('sass:watch', function () {
|
||||
gulp.watch('./scss/**/*.scss');
|
||||
});
|
||||
|
||||
|
||||
/* inject partials like sidebar and navbar */
|
||||
gulp.task('injectPartial', function () {
|
||||
var injPartial1 = gulp.src("./pages/**/*.html", { base: "./" })
|
||||
.pipe(injectPartials())
|
||||
.pipe(gulp.dest("."));
|
||||
var injPartial2 = gulp.src("./*.html", { base: "./" })
|
||||
.pipe(injectPartials())
|
||||
.pipe(gulp.dest("."));
|
||||
return merge(injPartial1, injPartial2);
|
||||
});
|
||||
|
||||
|
||||
/* inject Js and CCS assets into HTML */
|
||||
gulp.task('injectCommonAssets', function () {
|
||||
return gulp.src('./**/*.html')
|
||||
.pipe(inject(gulp.src([
|
||||
'./vendors/ti-icons/css/themify-icons.css',
|
||||
'./vendors/base/vendor.bundle.base.css',
|
||||
'./vendors/base/vendor.bundle.base.js'
|
||||
], {read: false}), {name: 'plugins', relative: true}))
|
||||
.pipe(inject(gulp.src([
|
||||
'./css/*.css',
|
||||
'./js/off-canvas.js',
|
||||
'./js/hoverable-collapse.js',
|
||||
'./js/template.js',
|
||||
'./js/todolist.js'
|
||||
], {read: false}), {relative: true}))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
/* inject Js and CCS assets into HTML */
|
||||
gulp.task('injectLayoutStyles', function () {
|
||||
return gulp.src('./**/*.html')
|
||||
.pipe(inject(gulp.src([
|
||||
'./css/style.css',
|
||||
], {read: false}), {relative: true}))
|
||||
.pipe(gulp.dest('.'));
|
||||
});
|
||||
|
||||
/*replace image path and linking after injection*/
|
||||
gulp.task('replacePath', function(){
|
||||
var replacePath1 = gulp.src(['./pages/*/*.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="../../images/'))
|
||||
.pipe(replace('href="pages/', 'href="../../pages/'))
|
||||
.pipe(replace('href="documentation/', 'href="../../documentation/'))
|
||||
.pipe(replace('href="index.html"', 'href="../../index.html"'))
|
||||
.pipe(gulp.dest('.'));
|
||||
var replacePath2 = gulp.src(['./pages/*.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="../images/'))
|
||||
.pipe(replace('"pages/', '"../pages/'))
|
||||
.pipe(replace('href="index.html"', 'href="../index.html"'))
|
||||
.pipe(gulp.dest('.'));
|
||||
var replacePath3 = gulp.src(['./index.html'], { base: "./" })
|
||||
.pipe(replace('="images/', '="images/'))
|
||||
.pipe(gulp.dest('.'));
|
||||
return merge(replacePath1, replacePath2, replacePath3);
|
||||
});
|
||||
|
||||
/*sequence for injecting partials and replacing paths*/
|
||||
gulp.task('inject', gulp.series('injectPartial' , 'injectCommonAssets' , 'injectLayoutStyles', 'replacePath'));
|
||||
|
||||
|
||||
gulp.task('clean:vendors', function () {
|
||||
return del([
|
||||
'vendors/**/*'
|
||||
]);
|
||||
});
|
||||
|
||||
/*Building vendor scripts needed for basic template rendering*/
|
||||
gulp.task('buildBaseVendorScripts', function() {
|
||||
return gulp.src([
|
||||
'./node_modules/jquery/dist/jquery.min.js',
|
||||
// './node_modules/popper.js/dist/umd/popper.min.js',
|
||||
'./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js',
|
||||
'./node_modules/perfect-scrollbar/dist/perfect-scrollbar.min.js'
|
||||
])
|
||||
.pipe(concat('vendor.bundle.base.js'))
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
});
|
||||
|
||||
/*Building vendor styles needed for basic template rendering*/
|
||||
gulp.task('buildBaseVendorStyles', function() {
|
||||
return gulp.src(['./node_modules/perfect-scrollbar/css/perfect-scrollbar.css'])
|
||||
.pipe(concat('vendor.bundle.base.css'))
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
});
|
||||
|
||||
/* Scripts for addons */
|
||||
gulp.task('copyRecursiveVendorFiles', function () {
|
||||
var chartJs = gulp.src(['./node_modules/chart.js/dist/Chart.min.js'])
|
||||
.pipe(gulp.dest('./vendors/chart.js'));
|
||||
var ti = gulp.src(['./node_modules/ti-icons/css/themify-icons.css'])
|
||||
.pipe(gulp.dest('./vendors/ti-icons/css'));
|
||||
var tiFonts = gulp.src(['./node_modules/ti-icons/fonts/*'])
|
||||
.pipe(gulp.dest('./vendors/ti-icons/fonts'));
|
||||
return merge(chartJs, ti, tiFonts);
|
||||
});
|
||||
|
||||
//Copy essential map files
|
||||
gulp.task('copyMapFiles', function() {
|
||||
var map1 = gulp.src('node_modules/bootstrap/dist/js/bootstrap.min.js.map')
|
||||
.pipe(gulp.dest('./vendors/base'));
|
||||
return merge(map1);
|
||||
});
|
||||
|
||||
/*sequence for building vendor scripts and styles*/
|
||||
gulp.task('bundleVendors', gulp.series('clean:vendors', 'buildBaseVendorStyles','buildBaseVendorScripts','copyRecursiveVendorFiles','copyMapFiles'));
|
||||
|
||||
gulp.task('default', gulp.series('serve'));
|
BIN
static/admin/images/.DS_Store
vendored
Normal file
BIN
static/admin/images/auth/.DS_Store
vendored
Normal file
BIN
static/admin/images/auth/lockscreen-bg.jpg
Normal file
After Width: | Height: | Size: 208 KiB |
BIN
static/admin/images/auth/login-bg.jpg
Executable file
After Width: | Height: | Size: 162 KiB |
BIN
static/admin/images/auth/register-bg.jpg
Executable file
After Width: | Height: | Size: 269 KiB |
3
static/admin/images/dashboard/shape-1.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="39" viewBox="0 0 56 39">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M6.18 19.87A15.62 15.62 0 0 1 6 17.5C6 8.94 12.94 2 21.5 2 30.06 2 37 8.94 37 17.5c0 1.582-.237 3.108-.677 4.545a5 5 0 1 1-4.047 6.596A15.449 15.449 0 0 1 21.5 33c-.867 0-1.717-.071-2.544-.208A7.001 7.001 0 1 1 6.17 28.123 4.995 4.995 0 0 1 4 24c0-1.715.864-3.229 2.18-4.13zM39 7a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM2 20a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm53 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM34 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM21 37a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM36.5 1a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 626 B |
3
static/admin/images/dashboard/shape-2.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="56" height="39" viewBox="0 0 56 39">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M49.82 19.13c.118.772.18 1.564.18 2.37C50 30.06 43.06 37 34.5 37 25.94 37 19 30.06 19 21.5c0-1.582.237-3.108.677-4.545a5 5 0 1 1 4.047-6.596A15.449 15.449 0 0 1 34.5 6c.867 0 1.717.071 2.544.208a7.001 7.001 0 1 1 12.785 4.669A4.995 4.995 0 0 1 52 15a4.995 4.995 0 0 1-2.18 4.13zM17 32a2 2 0 1 1 0 4 2 2 0 0 1 0-4zm37-13a2 2 0 1 1 0 4 2 2 0 0 1 0-4zM1 15a1 1 0 1 1 0 2 1 1 0 0 1 0-2zm21 20a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM35 2a1 1 0 1 1 0 2 1 1 0 0 1 0-2zM19.5 38a.5.5 0 1 1 0 1 .5.5 0 0 1 0-1z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 633 B |
3
static/admin/images/dashboard/shape-3.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="51" viewBox="0 0 48 51">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M14.41 7.005a15.62 15.62 0 0 1 1.89-1.442c7.179-4.662 16.778-2.622 21.44 4.558 4.663 7.18 2.622 16.779-4.557 21.441a15.498 15.498 0 0 1-4.18 1.908 5 5 0 1 1-7.736.198 15.449 15.449 0 0 1-9.525-6.663 15.608 15.608 0 0 1-1.212-2.248 7.001 7.001 0 1 1-3.047-13.265 4.995 4.995 0 0 1 2.276-4.066 4.995 4.995 0 0 1 4.65-.42zm28.668 20.516a2 2 0 1 1 3.355-2.179 2 2 0 0 1-3.355 2.179zM12.024 3.57a2 2 0 1 1 3.355-2.18 2 2 0 0 1-3.355 2.18zm25.511 46.627a1 1 0 1 1 1.678-1.09 1 1 0 0 1-1.678 1.09zm5.336-28.505a1 1 0 1 1 1.678-1.089 1 1 0 0 1-1.678 1.09zm-34.756 7.07a1 1 0 1 1 1.677-1.089 1 1 0 0 1-1.677 1.09zm38.634-6.607a.5.5 0 1 1 .838-.545.5.5 0 0 1-.838.545z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 801 B |
3
static/admin/images/dashboard/shape-4.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="93" height="99" viewBox="0 0 93 99">
|
||||
<path fill="#7EBBFF" fill-rule="nonzero" d="M27.66 13.596a30.44 30.44 0 0 1 3.684-2.81c13.991-9.086 32.699-5.11 41.785 8.882 9.086 13.991 5.11 32.7-8.882 41.785a30.203 30.203 0 0 1-8.148 3.718c.284.33.55.683.793 1.059 2.931 4.513 1.649 10.548-2.865 13.479-4.513 2.93-10.548 1.648-13.479-2.865-2.32-3.574-2-8.101.475-11.286A30.107 30.107 0 0 1 22.462 52.57a30.417 30.417 0 0 1-2.361-4.379c-6.127 3.065-13.71 1.102-17.53-4.78-4.103-6.318-2.307-14.767 4.012-18.87a13.578 13.578 0 0 1 7.579-2.202 9.734 9.734 0 0 1 4.435-7.924 9.734 9.734 0 0 1 9.063-.82zm55.872 39.981a3.898 3.898 0 1 1 6.537-4.245 3.898 3.898 0 0 1-6.537 4.245zM23.012 6.902a3.898 3.898 0 1 1 6.538-4.246 3.898 3.898 0 0 1-6.538 4.246zm49.717 90.87a1.949 1.949 0 1 1 3.269-2.123 1.949 1.949 0 0 1-3.269 2.123zm10.399-55.551a1.949 1.949 0 1 1 3.269-2.123 1.949 1.949 0 0 1-3.269 2.123zM15.393 56a1.949 1.949 0 1 1 3.27-2.123A1.949 1.949 0 0 1 15.392 56zm75.292-12.877a.974.974 0 1 1 1.634-1.062.974.974 0 0 1-1.634 1.062z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
BIN
static/admin/images/faces/.DS_Store
vendored
Normal file
BIN
static/admin/images/faces/face1.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face10.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face11.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/admin/images/faces/face12.jpg
Normal file
After Width: | Height: | Size: 9.8 KiB |
BIN
static/admin/images/faces/face13.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face14.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/admin/images/faces/face15.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face16.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face17.jpg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
static/admin/images/faces/face18.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face19.jpg
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
static/admin/images/faces/face2.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face20.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/admin/images/faces/face21.jpg
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
static/admin/images/faces/face22.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face23.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face24.jpg
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
static/admin/images/faces/face25.jpg
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
static/admin/images/faces/face26.jpg
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
static/admin/images/faces/face27.jpg
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
static/admin/images/faces/face28.jpg
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
static/admin/images/faces/face3.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/admin/images/faces/face4.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face5.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
static/admin/images/faces/face6.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/faces/face7.jpg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
static/admin/images/faces/face8.jpg
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
static/admin/images/faces/face9.jpg
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
static/admin/images/favicon.ico
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
static/admin/images/favicon.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
13
static/admin/images/logo-mini.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>royal mini</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="royal-mini">
|
||||
<path d="M2.40000004,0.0487804878 L16.1999999,0.0487804878 C21.0601057,0.0487804878 25,3.98867475 25,8.84878062 L25,22.6 C25,23.9254834 23.9254834,25 22.6,25 L8.80000013,25 C3.93989426,25 5.9519131e-16,21.0601057 0,16.1999999 L0,2.44878052 C-1.62324903e-16,1.1232971 1.07451662,0.0487804878 2.40000004,0.0487804878 Z" id="Rectangle" fill="#F5A622"></path>
|
||||
<path d="M0,0.0487804878 L8.08025177,0.0487804878 C12.5254705,0.0487804878 16.1290323,3.65234226 16.1290323,8.09756098 L16.1290323,13.7463414 C16.1290323,15.0718248 15.0545156,16.1463415 13.7290322,16.1463415 L8.06451613,16.1463415 C3.61060686,16.1463415 2.32180341e-15,12.5357346 1.77635684e-15,8.08182533 L0,0.0487804878 Z" id="Rectangle" fill="#FDBC0A"></path>
|
||||
<path d="M15.0129032,21.4778537 L11.0645161,14.6009756 L10.0580645,14.6009756 L10.0580645,21.4778537 L5.64516129,21.4778537 L5.64516129,3.26829268 L13.1806452,3.26829268 C14.6258137,3.26829268 15.8473068,3.5172658 16.8451613,4.01521951 C17.8430157,4.51317322 18.5913954,5.19570298 19.0903226,6.06282927 C19.5892498,6.92995556 19.8387097,7.9215554 19.8387097,9.03765854 C19.8387097,10.3598115 19.4731219,11.4844832 18.7419355,12.4117073 C18.010749,13.3389315 16.9483941,13.9742422 15.5548387,14.3176585 L19.8903226,21.4778537 L15.0129032,21.4778537 Z M10.0580645,11.5875122 L12.7935484,11.5875122 C13.6709721,11.5875122 14.316127,11.3943434 14.7290323,11.008 C15.1419375,10.6216566 15.3483871,10.0507355 15.3483871,9.29521951 C15.3483871,8.57404517 15.1376365,8.00741669 14.716129,7.59531707 C14.2946215,7.18321745 13.6537677,6.97717073 12.7935484,6.97717073 L10.0580645,6.97717073 L10.0580645,11.5875122 Z" id="R" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
16
static/admin/images/logo.svg
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
static/admin/js/.DS_Store
vendored
Normal file
352
static/admin/js/chart.js
Normal file
@ -0,0 +1,352 @@
|
||||
$(function() {
|
||||
/* ChartJS
|
||||
* -------
|
||||
* Data and config for chartjs
|
||||
*/
|
||||
'use strict';
|
||||
var data = {
|
||||
labels: ["2013", "2014", "2014", "2015", "2016", "2017"],
|
||||
datasets: [{
|
||||
label: '# of Votes',
|
||||
data: [10, 19, 3, 5, 2, 3],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(255, 206, 86, 0.2)',
|
||||
'rgba(75, 192, 192, 0.2)',
|
||||
'rgba(153, 102, 255, 0.2)',
|
||||
'rgba(255, 159, 64, 0.2)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255,99,132,1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(153, 102, 255, 1)',
|
||||
'rgba(255, 159, 64, 1)'
|
||||
],
|
||||
borderWidth: 1,
|
||||
fill: false
|
||||
}]
|
||||
};
|
||||
var multiLineData = {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [{
|
||||
label: 'Dataset 1',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
borderColor: [
|
||||
'#587ce4'
|
||||
],
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: 'Dataset 2',
|
||||
data: [5, 23, 7, 12, 42, 23],
|
||||
borderColor: [
|
||||
'#ede190'
|
||||
],
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
},
|
||||
{
|
||||
label: 'Dataset 3',
|
||||
data: [15, 10, 21, 32, 12, 33],
|
||||
borderColor: [
|
||||
'#f44252'
|
||||
],
|
||||
borderWidth: 2,
|
||||
fill: false
|
||||
}
|
||||
]
|
||||
};
|
||||
var options = {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
var doughnutPieData = {
|
||||
datasets: [{
|
||||
data: [30, 40, 30],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.5)',
|
||||
'rgba(54, 162, 235, 0.5)',
|
||||
'rgba(255, 206, 86, 0.5)',
|
||||
'rgba(75, 192, 192, 0.5)',
|
||||
'rgba(153, 102, 255, 0.5)',
|
||||
'rgba(255, 159, 64, 0.5)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255,99,132,1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(153, 102, 255, 1)',
|
||||
'rgba(255, 159, 64, 1)'
|
||||
],
|
||||
}],
|
||||
|
||||
// These labels appear in the legend and in the tooltips when hovering different arcs
|
||||
labels: [
|
||||
'Pink',
|
||||
'Blue',
|
||||
'Yellow',
|
||||
]
|
||||
};
|
||||
var doughnutPieOptions = {
|
||||
responsive: true,
|
||||
animation: {
|
||||
animateScale: true,
|
||||
animateRotate: true
|
||||
}
|
||||
};
|
||||
var areaData = {
|
||||
labels: ["2013", "2014", "2015", "2016", "2017"],
|
||||
datasets: [{
|
||||
label: '# of Votes',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(255, 206, 86, 0.2)',
|
||||
'rgba(75, 192, 192, 0.2)',
|
||||
'rgba(153, 102, 255, 0.2)',
|
||||
'rgba(255, 159, 64, 0.2)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255,99,132,1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(153, 102, 255, 1)',
|
||||
'rgba(255, 159, 64, 1)'
|
||||
],
|
||||
borderWidth: 1,
|
||||
fill: true, // 3: no fill
|
||||
}]
|
||||
};
|
||||
|
||||
var areaOptions = {
|
||||
plugins: {
|
||||
filler: {
|
||||
propagate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var multiAreaData = {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
|
||||
datasets: [{
|
||||
label: 'Facebook',
|
||||
data: [8, 11, 13, 15, 12, 13, 16, 15, 13, 19, 11, 14],
|
||||
borderColor: ['rgba(255, 99, 132, 0.5)'],
|
||||
backgroundColor: ['rgba(255, 99, 132, 0.5)'],
|
||||
borderWidth: 1,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Twitter',
|
||||
data: [7, 17, 12, 16, 14, 18, 16, 12, 15, 11, 13, 9],
|
||||
borderColor: ['rgba(54, 162, 235, 0.5)'],
|
||||
backgroundColor: ['rgba(54, 162, 235, 0.5)'],
|
||||
borderWidth: 1,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: 'Linkedin',
|
||||
data: [6, 14, 16, 20, 12, 18, 15, 12, 17, 19, 15, 11],
|
||||
borderColor: ['rgba(255, 206, 86, 0.5)'],
|
||||
backgroundColor: ['rgba(255, 206, 86, 0.5)'],
|
||||
borderWidth: 1,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var multiAreaOptions = {
|
||||
plugins: {
|
||||
filler: {
|
||||
propagate: true
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
display: false
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
var scatterChartData = {
|
||||
datasets: [{
|
||||
label: 'First Dataset',
|
||||
data: [{
|
||||
x: -10,
|
||||
y: 0
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 3
|
||||
},
|
||||
{
|
||||
x: -25,
|
||||
y: 5
|
||||
},
|
||||
{
|
||||
x: 40,
|
||||
y: 5
|
||||
}
|
||||
],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255,99,132,1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: 'Second Dataset',
|
||||
data: [{
|
||||
x: 10,
|
||||
y: 5
|
||||
},
|
||||
{
|
||||
x: 20,
|
||||
y: -30
|
||||
},
|
||||
{
|
||||
x: -25,
|
||||
y: 15
|
||||
},
|
||||
{
|
||||
x: -10,
|
||||
y: 5
|
||||
}
|
||||
],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(54, 162, 235, 1)',
|
||||
],
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
var scatterChartOptions = {
|
||||
scales: {
|
||||
xAxes: [{
|
||||
type: 'linear',
|
||||
position: 'bottom'
|
||||
}]
|
||||
}
|
||||
}
|
||||
// Get context with jQuery - using jQuery's .get() method.
|
||||
if ($("#barChart").length) {
|
||||
var barChartCanvas = $("#barChart").get(0).getContext("2d");
|
||||
// This will get the first returned node in the jQuery collection.
|
||||
var barChart = new Chart(barChartCanvas, {
|
||||
type: 'bar',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#lineChart").length) {
|
||||
var lineChartCanvas = $("#lineChart").get(0).getContext("2d");
|
||||
var lineChart = new Chart(lineChartCanvas, {
|
||||
type: 'line',
|
||||
data: data,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#linechart-multi").length) {
|
||||
var multiLineCanvas = $("#linechart-multi").get(0).getContext("2d");
|
||||
var lineChart = new Chart(multiLineCanvas, {
|
||||
type: 'line',
|
||||
data: multiLineData,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#areachart-multi").length) {
|
||||
var multiAreaCanvas = $("#areachart-multi").get(0).getContext("2d");
|
||||
var multiAreaChart = new Chart(multiAreaCanvas, {
|
||||
type: 'line',
|
||||
data: multiAreaData,
|
||||
options: multiAreaOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#doughnutChart").length) {
|
||||
var doughnutChartCanvas = $("#doughnutChart").get(0).getContext("2d");
|
||||
var doughnutChart = new Chart(doughnutChartCanvas, {
|
||||
type: 'doughnut',
|
||||
data: doughnutPieData,
|
||||
options: doughnutPieOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#pieChart").length) {
|
||||
var pieChartCanvas = $("#pieChart").get(0).getContext("2d");
|
||||
var pieChart = new Chart(pieChartCanvas, {
|
||||
type: 'pie',
|
||||
data: doughnutPieData,
|
||||
options: doughnutPieOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#areaChart").length) {
|
||||
var areaChartCanvas = $("#areaChart").get(0).getContext("2d");
|
||||
var areaChart = new Chart(areaChartCanvas, {
|
||||
type: 'line',
|
||||
data: areaData,
|
||||
options: areaOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#scatterChart").length) {
|
||||
var scatterChartCanvas = $("#scatterChart").get(0).getContext("2d");
|
||||
var scatterChart = new Chart(scatterChartCanvas, {
|
||||
type: 'scatter',
|
||||
data: scatterChartData,
|
||||
options: scatterChartOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#browserTrafficChart").length) {
|
||||
var doughnutChartCanvas = $("#browserTrafficChart").get(0).getContext("2d");
|
||||
var doughnutChart = new Chart(doughnutChartCanvas, {
|
||||
type: 'doughnut',
|
||||
data: browserTrafficData,
|
||||
options: doughnutPieOptions
|
||||
});
|
||||
}
|
||||
});
|
236
static/admin/js/dashboard.js
Normal file
@ -0,0 +1,236 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
if ($("#order-chart").length) {
|
||||
var areaData = {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"],
|
||||
datasets: [
|
||||
{
|
||||
data: [175, 200, 130, 210, 40, 60, 25],
|
||||
backgroundColor: [
|
||||
'rgba(255, 193, 2, .8)'
|
||||
],
|
||||
borderColor: [
|
||||
'transparent'
|
||||
],
|
||||
borderWidth:3,
|
||||
fill: 'origin',
|
||||
label: "services"
|
||||
},
|
||||
{
|
||||
data: [175, 145, 190, 130, 240, 160, 200],
|
||||
backgroundColor: [
|
||||
'rgba(245, 166, 35, 1)'
|
||||
],
|
||||
borderColor: [
|
||||
'transparent'
|
||||
],
|
||||
borderWidth:3,
|
||||
fill: 'origin',
|
||||
label: "purchases"
|
||||
}
|
||||
]
|
||||
};
|
||||
var areaOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
plugins: {
|
||||
filler: {
|
||||
propagate: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
xAxes: [{
|
||||
display: false,
|
||||
ticks: {
|
||||
display: true
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
drawBorder: false,
|
||||
color: 'transparent',
|
||||
zeroLineColor: '#eeeeee'
|
||||
}
|
||||
}],
|
||||
yAxes: [{
|
||||
display: false,
|
||||
ticks: {
|
||||
display: true,
|
||||
autoSkip: false,
|
||||
maxRotation: 0,
|
||||
stepSize: 100,
|
||||
min: 0,
|
||||
max: 260
|
||||
},
|
||||
gridLines: {
|
||||
drawBorder: false
|
||||
}
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltips: {
|
||||
enabled: true
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: .45
|
||||
},
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
var salesChartCanvas = $("#order-chart").get(0).getContext("2d");
|
||||
var salesChart = new Chart(salesChartCanvas, {
|
||||
type: 'line',
|
||||
data: areaData,
|
||||
options: areaOptions
|
||||
});
|
||||
}
|
||||
|
||||
if ($("#sales-chart").length) {
|
||||
var SalesChartCanvas = $("#sales-chart").get(0).getContext("2d");
|
||||
var SalesChart = new Chart(SalesChartCanvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
|
||||
datasets: [{
|
||||
label: 'Offline Sales',
|
||||
data: [480, 230, 470, 210, 330],
|
||||
backgroundColor: '#8EB0FF'
|
||||
},
|
||||
{
|
||||
label: 'Online Sales',
|
||||
data: [400, 340, 550, 480, 170],
|
||||
backgroundColor: '#316FFF'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 20,
|
||||
bottom: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
display: true,
|
||||
gridLines: {
|
||||
display: false,
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
display: false,
|
||||
min: 0,
|
||||
max: 500
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
stacked: false,
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: "#9fa0a2"
|
||||
},
|
||||
gridLines: {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
display: false
|
||||
},
|
||||
barPercentage: 1
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
document.getElementById('sales-legend').innerHTML = SalesChart.generateLegend();
|
||||
}
|
||||
|
||||
if ($("#north-america-chart").length) {
|
||||
var areaData = {
|
||||
labels: ["Jan", "Feb", "Mar"],
|
||||
datasets: [{
|
||||
data: [100, 50, 50],
|
||||
backgroundColor: [
|
||||
"#71c016", "#8caaff", "#248afd",
|
||||
],
|
||||
borderColor: "rgba(0,0,0,0)"
|
||||
}
|
||||
]
|
||||
};
|
||||
var areaOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
segmentShowStroke: false,
|
||||
cutoutPercentage: 78,
|
||||
elements: {
|
||||
arc: {
|
||||
borderWidth: 4
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltips: {
|
||||
enabled: true
|
||||
},
|
||||
legendCallback: function(chart) {
|
||||
var text = [];
|
||||
text.push('<div class="report-chart">');
|
||||
text.push('<div class="d-flex justify-content-between mx-4 mx-xl-5 mt-3"><div class="d-flex align-items-center"><div class="me-3" style="width:20px; height:20px; border-radius: 50%; background-color: ' + chart.data.datasets[0].backgroundColor[0] + '"></div><p class="mb-0">Offline sales</p></div>');
|
||||
text.push('<p class="mb-0">22789</p>');
|
||||
text.push('</div>');
|
||||
text.push('<div class="d-flex justify-content-between mx-4 mx-xl-5 mt-3"><div class="d-flex align-items-center"><div class="me-3" style="width:20px; height:20px; border-radius: 50%; background-color: ' + chart.data.datasets[0].backgroundColor[1] + '"></div><p class="mb-0">Online sales</p></div>');
|
||||
text.push('<p class="mb-0">94678</p>');
|
||||
text.push('</div>');
|
||||
text.push('<div class="d-flex justify-content-between mx-4 mx-xl-5 mt-3"><div class="d-flex align-items-center"><div class="me-3" style="width:20px; height:20px; border-radius: 50%; background-color: ' + chart.data.datasets[0].backgroundColor[2] + '"></div><p class="mb-0">Returns</p></div>');
|
||||
text.push('<p class="mb-0">12097</p>');
|
||||
text.push('</div>');
|
||||
text.push('</div>');
|
||||
return text.join("");
|
||||
},
|
||||
}
|
||||
var northAmericaChartPlugins = {
|
||||
beforeDraw: function(chart) {
|
||||
var width = chart.chart.width,
|
||||
height = chart.chart.height,
|
||||
ctx = chart.chart.ctx;
|
||||
|
||||
ctx.restore();
|
||||
var fontSize = 3.125;
|
||||
ctx.font = "600 " + fontSize + "em sans-serif";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillStyle = "#000";
|
||||
|
||||
var text = "63",
|
||||
textX = Math.round((width - ctx.measureText(text).width) / 2),
|
||||
textY = height / 2;
|
||||
|
||||
ctx.fillText(text, textX, textY);
|
||||
ctx.save();
|
||||
}
|
||||
}
|
||||
var northAmericaChartCanvas = $("#north-america-chart").get(0).getContext("2d");
|
||||
var northAmericaChart = new Chart(northAmericaChartCanvas, {
|
||||
type: 'doughnut',
|
||||
data: areaData,
|
||||
options: areaOptions,
|
||||
plugins: northAmericaChartPlugins
|
||||
});
|
||||
document.getElementById('north-america-legend').innerHTML = northAmericaChart.generateLegend();
|
||||
}
|
||||
|
||||
});
|
||||
})(jQuery);
|
24
static/admin/js/documentation.js
Normal file
@ -0,0 +1,24 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
// The function actually applying the offset
|
||||
function offsetAnchor() {
|
||||
if (location.hash.length !== 0) {
|
||||
// window.scrollTo(window.scrollX, window.scrollY - 140);
|
||||
$("html").animate({ scrollTop: $(location.hash).offset().top - 160 }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Captures click events of all <a> elements with href starting with #
|
||||
$(document).on('click', 'a[href^="#"]', function(event) {
|
||||
// Click events are captured before hashchanges. Timeout
|
||||
// causes offsetAnchor to be called after the page jump.
|
||||
window.setTimeout(function() {
|
||||
offsetAnchor();
|
||||
}, 0);
|
||||
});
|
||||
|
||||
// Set the offset when entering page with hash present in the url
|
||||
window.setTimeout(offsetAnchor, 0);
|
||||
});
|
||||
})(jQuery);
|
12
static/admin/js/file-upload.js
Normal file
@ -0,0 +1,12 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
$('.file-upload-browse').on('click', function() {
|
||||
var file = $(this).parent().parent().parent().find('.file-upload-default');
|
||||
file.trigger('click');
|
||||
});
|
||||
$('.file-upload-default').on('change', function() {
|
||||
$(this).parent().find('.form-control').val($(this).val().replace(/C:\\fakepath\\/i, ''));
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
25
static/admin/js/hoverable-collapse.js
Normal file
@ -0,0 +1,25 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
//Open submenu on hover in compact sidebar mode and horizontal menu mode
|
||||
$(document).on('mouseenter mouseleave', '.sidebar .nav-item', function(ev) {
|
||||
var body = $('body');
|
||||
var sidebarIconOnly = body.hasClass("sidebar-icon-only");
|
||||
var sidebarFixed = body.hasClass("sidebar-fixed");
|
||||
if (!('ontouchstart' in document.documentElement)) {
|
||||
if (sidebarIconOnly) {
|
||||
if (sidebarFixed) {
|
||||
if (ev.type === 'mouseenter') {
|
||||
body.removeClass('sidebar-icon-only');
|
||||
}
|
||||
} else {
|
||||
var $menuItem = $(this);
|
||||
if (ev.type === 'mouseenter') {
|
||||
$menuItem.addClass('hover-open')
|
||||
} else {
|
||||
$menuItem.removeClass('hover-open')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
117
static/admin/js/jquery.cookie.js
Executable file
@ -0,0 +1,117 @@
|
||||
/*!
|
||||
* jQuery Cookie Plugin v1.4.1
|
||||
* https://github.com/carhartl/jquery-cookie
|
||||
*
|
||||
* Copyright 2013 Klaus Hartl
|
||||
* Released under the MIT license
|
||||
*/
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// CommonJS
|
||||
factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
|
||||
var pluses = /\+/g;
|
||||
|
||||
function encode(s) {
|
||||
return config.raw ? s : encodeURIComponent(s);
|
||||
}
|
||||
|
||||
function decode(s) {
|
||||
return config.raw ? s : decodeURIComponent(s);
|
||||
}
|
||||
|
||||
function stringifyCookieValue(value) {
|
||||
return encode(config.json ? JSON.stringify(value) : String(value));
|
||||
}
|
||||
|
||||
function parseCookieValue(s) {
|
||||
if (s.indexOf('"') === 0) {
|
||||
// This is a quoted cookie as according to RFC2068, unescape...
|
||||
s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
try {
|
||||
// Replace server-side written pluses with spaces.
|
||||
// If we can't decode the cookie, ignore it, it's unusable.
|
||||
// If we can't parse the cookie, ignore it, it's unusable.
|
||||
s = decodeURIComponent(s.replace(pluses, ' '));
|
||||
return config.json ? JSON.parse(s) : s;
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function read(s, converter) {
|
||||
var value = config.raw ? s : parseCookieValue(s);
|
||||
return $.isFunction(converter) ? converter(value) : value;
|
||||
}
|
||||
|
||||
var config = $.cookie = function (key, value, options) {
|
||||
|
||||
// Write
|
||||
|
||||
if (value !== undefined && !$.isFunction(value)) {
|
||||
options = $.extend({}, config.defaults, options);
|
||||
|
||||
if (typeof options.expires === 'number') {
|
||||
var days = options.expires, t = options.expires = new Date();
|
||||
t.setTime(+t + days * 864e+5);
|
||||
}
|
||||
|
||||
return (document.cookie = [
|
||||
encode(key), '=', stringifyCookieValue(value),
|
||||
options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
|
||||
options.path ? '; path=' + options.path : '',
|
||||
options.domain ? '; domain=' + options.domain : '',
|
||||
options.secure ? '; secure' : ''
|
||||
].join(''));
|
||||
}
|
||||
|
||||
// Read
|
||||
|
||||
var result = key ? undefined : {};
|
||||
|
||||
// To prevent the for loop in the first place assign an empty array
|
||||
// in case there are no cookies at all. Also prevents odd result when
|
||||
// calling $.cookie().
|
||||
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
||||
|
||||
for (var i = 0, l = cookies.length; i < l; i++) {
|
||||
var parts = cookies[i].split('=');
|
||||
var name = decode(parts.shift());
|
||||
var cookie = parts.join('=');
|
||||
|
||||
if (key && key === name) {
|
||||
// If second argument (value) is a function it's a converter...
|
||||
result = read(cookie, value);
|
||||
break;
|
||||
}
|
||||
|
||||
// Prevent storing a cookie that we couldn't decode.
|
||||
if (!key && (cookie = read(cookie)) !== undefined) {
|
||||
result[name] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
config.defaults = {};
|
||||
|
||||
$.removeCookie = function (key, options) {
|
||||
if ($.cookie(key) === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must not alter options, thus extending a fresh object...
|
||||
$.cookie(key, '', $.extend({}, options, { expires: -1 }));
|
||||
return !$.cookie(key);
|
||||
};
|
||||
|
||||
}));
|
8
static/admin/js/off-canvas.js
Normal file
@ -0,0 +1,8 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
$('[data-toggle="offcanvas"]').on("click", function() {
|
||||
$('.sidebar-offcanvas').toggleClass('active')
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
48
static/admin/js/tabs.js
Normal file
@ -0,0 +1,48 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
if ($('.demo-tabs').length) {
|
||||
$('.demo-tabs').pwstabs({
|
||||
effect: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
if ($('.hello_world').length) {
|
||||
$('.hello_world').pwstabs();
|
||||
}
|
||||
|
||||
if ($('#rtl-tabs-1').length) {
|
||||
$('#rtl-tabs-1').pwstabs({
|
||||
effect: 'slidedown',
|
||||
defaultTab: 2,
|
||||
rtl: true
|
||||
});
|
||||
}
|
||||
|
||||
if ($('#vertical-left').length) {
|
||||
$('#vertical-left').pwstabs({
|
||||
effect: 'slideleft',
|
||||
defaultTab: 1,
|
||||
containerWidth: '600px',
|
||||
tabsPosition: 'vertical',
|
||||
verticalPosition: 'left'
|
||||
});
|
||||
}
|
||||
|
||||
if ($('#horizontal-left').length) {
|
||||
$('#horizontal-left').pwstabs({
|
||||
effect: 'slidedown',
|
||||
defaultTab: 2,
|
||||
containerWidth: '600px',
|
||||
horizontalPosition: 'bottom'
|
||||
});
|
||||
}
|
||||
|
||||
if ($('.tickets-tab').length) {
|
||||
$('.tickets-tab').pwstabs({
|
||||
effect: 'none'
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
})(jQuery);
|
96
static/admin/js/template.js
Normal file
@ -0,0 +1,96 @@
|
||||
(function($) {
|
||||
'use strict';
|
||||
$(function() {
|
||||
var body = $('body');
|
||||
var contentWrapper = $('.content-wrapper');
|
||||
var scroller = $('.container-scroller');
|
||||
var footer = $('.footer');
|
||||
var sidebar = $('.sidebar');
|
||||
|
||||
//Add active class to nav-link based on url dynamically
|
||||
//Active class can be hard coded directly in html file also as required
|
||||
|
||||
function addActiveClass(element) {
|
||||
if (current === "") {
|
||||
//for root url
|
||||
if (element.attr('href').indexOf("index.html") !== -1) {
|
||||
element.parents('.nav-item').last().addClass('active');
|
||||
if (element.parents('.sub-menu').length) {
|
||||
element.closest('.collapse').addClass('show');
|
||||
element.addClass('active');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//for other url
|
||||
if (element.attr('href').indexOf(current) !== -1) {
|
||||
element.parents('.nav-item').last().addClass('active');
|
||||
if (element.parents('.sub-menu').length) {
|
||||
element.closest('.collapse').addClass('show');
|
||||
element.addClass('active');
|
||||
}
|
||||
if (element.parents('.submenu-item').length) {
|
||||
element.addClass('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var current = location.pathname.split("/").slice(-1)[0].replace(/^\/|\/$/g, '');
|
||||
$('.nav li a', sidebar).each(function() {
|
||||
var $this = $(this);
|
||||
addActiveClass($this);
|
||||
})
|
||||
|
||||
//Close other submenu in sidebar on opening any
|
||||
|
||||
sidebar.on('show.bs.collapse', '.collapse', function() {
|
||||
sidebar.find('.collapse.show').collapse('hide');
|
||||
});
|
||||
|
||||
|
||||
//Change sidebar
|
||||
$('[data-toggle="minimize"]').on("click", function() {
|
||||
body.toggleClass('sidebar-icon-only');
|
||||
});
|
||||
|
||||
//checkbox and radios
|
||||
$(".form-check label,.form-radio label").append('<i class="input-helper"></i>');
|
||||
|
||||
});
|
||||
|
||||
// focus input when clicking on search icon
|
||||
$('#navbar-search-icon').click(function() {
|
||||
$("#navbar-search-input").focus();
|
||||
});
|
||||
if ($.cookie('royal-free-banner')!="true") {
|
||||
document.querySelector('#proBanner').classList.add('d-flex');
|
||||
document.querySelector('.navbar').classList.remove('fixed-top');
|
||||
}
|
||||
else {
|
||||
document.querySelector('#proBanner').classList.add('d-none');
|
||||
document.querySelector('.navbar').classList.add('fixed-top');
|
||||
}
|
||||
|
||||
if ($( ".navbar" ).hasClass( "fixed-top" )) {
|
||||
document.querySelector('.page-body-wrapper').classList.remove('pt-0');
|
||||
document.querySelector('.navbar').classList.remove('pt-5');
|
||||
}
|
||||
else {
|
||||
document.querySelector('.page-body-wrapper').classList.add('pt-0');
|
||||
document.querySelector('.navbar').classList.add('pt-5');
|
||||
document.querySelector('.navbar').classList.add('mt-3');
|
||||
|
||||
}
|
||||
document.querySelector('#bannerClose').addEventListener('click',function() {
|
||||
document.querySelector('#proBanner').classList.add('d-none');
|
||||
document.querySelector('#proBanner').classList.remove('d-flex');
|
||||
document.querySelector('.navbar').classList.remove('pt-5');
|
||||
document.querySelector('.navbar').classList.add('fixed-top');
|
||||
document.querySelector('.page-body-wrapper').classList.add('proBanner-padding-top');
|
||||
document.querySelector('.navbar').classList.remove('mt-3');
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime() + 24 * 60 * 60 * 1000);
|
||||
$.cookie('royal-free-banner', "true", { expires: date });
|
||||
});
|
||||
|
||||
})(jQuery);
|