Compare commits

..

27 Commits

Author SHA1 Message Date
30c552cf6c change postgres to docker 2024-07-30 20:03:44 +02:00
3eab2a3c9f add todo 2024-07-29 22:09:06 +02:00
5d225f0186 Use redis to store the sessions 2024-07-29 21:54:37 +02:00
52c3679158 Use cmd flags 2024-07-29 14:20:51 +02:00
aca8605870 fix a make reservation error 2024-07-29 14:04:43 +02:00
6631288843 add more tests 2024-07-29 13:52:10 +02:00
99c2eec759 add some tests 2024-07-28 15:48:03 +02:00
623291541a Fixing redirects 2024-07-28 15:23:12 +02:00
d7f7a2d8d9 Handling Calendar changes 2024-07-28 14:43:39 +02:00
14828fb901 Reservation calendar 4 2024-07-28 13:59:54 +02:00
7894a05daf Reservation Calendar 3 2024-07-27 22:39:23 +02:00
b97c6cba5c reservation calendar 2 2024-07-26 13:40:56 +02:00
a7cf9fe4f0 Showing the reservation calendar 2024-07-26 10:18:27 +02:00
5987fadb03 Deleting a reservation 2024-07-25 14:09:42 +02:00
fcd29cc082 Marking a resercation as processed 2024-07-25 13:51:31 +02:00
ca1e72c676 Editing a reservation 2024-07-25 13:29:57 +02:00
d4cf44bb22 Database functions for editing a reservation 2024-07-25 13:13:16 +02:00
845f3a5836 Showing one reservation 2024-07-24 22:23:31 +02:00
dafd6f21c0 Listing new reservations 2024-07-24 14:20:30 +02:00
f93388a8e9 Listing all reservations 2024-07-22 22:14:48 +02:00
b34c217d98 create stub handlers for admin functionnality 2024-07-22 14:05:20 +02:00
ea796999d4 convert admin template into go template 2024-07-22 13:54:59 +02:00
51e3c74950 add admin dropdown 2024-07-22 13:10:17 +02:00
dedab91898 Protecting our routes with auth middleware 2024-07-22 10:13:50 +02:00
68d9110cd2 change flash to success 2024-07-22 10:04:08 +02:00
96f81418ec Checking to see if a user is logged in, and logging a user our 2024-07-22 10:00:17 +02:00
a0853cf880 Writing auth middleware 2024-07-19 18:40:19 +02:00
181 changed files with 41461 additions and 68 deletions

View File

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

View File

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

View File

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

View 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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,6 +45,7 @@ type Reservation struct {
Room Room
ID int
RoomID int
Processed int
}
// RoomRestriction is the room restriction model

View File

@ -4,13 +4,14 @@ import "go-udemy-web-1/internal/forms"
// TemplateData holds data sent from handlers to templates
type TemplateData struct {
StringMap map[string]string
IntMap map[string]int
FloatMap map[string]float32
Data map[string]interface{}
CSRFToken string
Flash string
Warning string
Error string
Form *forms.Form
StringMap map[string]string
IntMap map[string]int
FloatMap map[string]float32
Data map[string]interface{}
Form *forms.Form
CSRFToken string
Flash string
Warning string
Error string
IsAuthenticated int
}

View File

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

View File

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

View File

@ -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) {
return 1, "", nil
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
}

View File

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

View File

@ -0,0 +1 @@
drop_column("reservations", "processed")

View File

@ -0,0 +1 @@
add_column("reservations", "processed", "integer", {"default": 0})

2
run.sh
View File

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

Binary file not shown.

File diff suppressed because one or more lines are too long

22631
static/admin/css/style.css Normal file

File diff suppressed because it is too large Load Diff

BIN
static/admin/documentation/.DS_Store vendored Normal file

Binary file not shown.

View 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

Binary file not shown.

BIN
static/admin/fonts/Roboto/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

176
static/admin/gulpfile.js Normal file
View 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

Binary file not shown.

BIN
static/admin/images/auth/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

View 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

View 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

View 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

View 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

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View 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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
static/admin/js/.DS_Store vendored Normal file

Binary file not shown.

352
static/admin/js/chart.js Normal file
View 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
});
}
});

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

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

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

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

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

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

Some files were not shown because too many files have changed in this diff Show More