change to GORM, add member and rank handlers
@@ -1,3 +1,3 @@
|
|||||||
mariadb.connectionstring=user:password@tcp(localhost:3306)/dbname?parseTime=true
|
MARIADB_CONNSTRING=user:password@tcp(localhost:3306)/dbname?parseTime=true
|
||||||
api.prefix=/api/v1
|
API_PREFIX=/api/v1
|
||||||
api.port=1323
|
API_PORT=1323
|
||||||
110
api/db/award.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
/*
|
||||||
|
DDL
|
||||||
|
CREATE TABLE `awards` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) DEFAULT NULL,
|
||||||
|
`short_name` varchar(10) DEFAULT NULL,
|
||||||
|
`description` text DEFAULT NULL,
|
||||||
|
`type` varchar(100) DEFAULT NULL,
|
||||||
|
`footprint` varchar(50) DEFAULT NULL,
|
||||||
|
`created_at` datetime DEFAULT current_timestamp(),
|
||||||
|
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`image_url` varchar(250) DEFAULT NULL,
|
||||||
|
`deleted` tinytext DEFAULT '0',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COMMENT='Contains a list of Awards for the unit.';
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Award struct {
|
||||||
|
ObjectBase
|
||||||
|
Name string `json:"name"`
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Footprint string `json:"footprint"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
|
||||||
|
Members []Member `json:"members" gorm:"many2many:member_awards;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by id
|
||||||
|
func (a *Award) GetByID(id int) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.First(a, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by name
|
||||||
|
func (a *Award) GetByName(name string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where("name = ?", name).First(a).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
func (a *Award) GetAll() ([]Award, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var awards []Award
|
||||||
|
err = db.Find(&awards).Error
|
||||||
|
return awards, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
func (a *Award) Create() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Create(a).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
func (a *Award) Update() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Save(a).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func (a *Award) Delete() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Delete(a).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all holders of an award
|
||||||
|
func (a *Award) GetHolders() ([]Member, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var members []Member
|
||||||
|
err = db.Model(a).Association("Members").Find(&members)
|
||||||
|
return members, err
|
||||||
|
}
|
||||||
142
api/db/course.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
/*
|
||||||
|
DDL
|
||||||
|
|
||||||
|
CREATE TABLE `courses` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`short_name` varchar(10) NOT NULL,
|
||||||
|
`category` varchar(100) NOT NULL,
|
||||||
|
`description` varchar(1000) DEFAULT NULL,
|
||||||
|
`image_url` varchar(255) DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`deleted` tinyint(4) DEFAULT 0,
|
||||||
|
`prereq_id` int(11) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`),
|
||||||
|
UNIQUE KEY `shortName` (`short_name`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4;
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Course struct {
|
||||||
|
ObjectBase
|
||||||
|
Name string `json:"name"`
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
|
||||||
|
PassedMembers []Member `json:"passed_members" gorm:"many2many:member_courses_pass;"`
|
||||||
|
FailedMembers []Member `json:"failed_members" gorm:"many2many:member_courses_fail;"`
|
||||||
|
Prerequisites []*Course `json:"prerequisites" gorm:"many2many:course_prerequisites;"`
|
||||||
|
TrainingEvents []TrainingEvent `json:"training_events"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by id
|
||||||
|
func (c *Course) GetByID(id int) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.First(c, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by name
|
||||||
|
func (c *Course) GetByName(name string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where("name = ?", name).First(c).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
func (c *Course) GetAll() ([]Course, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var courses []Course
|
||||||
|
err = db.Find(&courses).Error
|
||||||
|
return courses, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all training events for a course
|
||||||
|
func (c *Course) GetTrainingEvents(ctx context.Context, id int) ([]TrainingEvent, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var events []TrainingEvent
|
||||||
|
err = db.WithContext(ctx).Where("course_id = ?", id).Find(&events).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return events, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all prerequisites for a course
|
||||||
|
func (c *Course) GetPrerequisites(ctx context.Context, id int) ([]Course, error) {
|
||||||
|
var prereqs []Course
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Model(c).Where("id = ?", id).Association("Prerequisites").Find(&prereqs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return prereqs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new course
|
||||||
|
func (c *Course) Create(ctx context.Context, course Course) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.WithContext(ctx).Create(course).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a course
|
||||||
|
func (c *Course) Update(ctx context.Context, course Course) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.WithContext(ctx).Save(course).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a course
|
||||||
|
func (c *Course) Delete(ctx context.Context, course Course) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.WithContext(ctx).Delete(course).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -3,15 +3,43 @@ package db
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"gopkg.in/guregu/null.v3"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ActiveDB *sql.DB
|
var ActiveDB *gorm.DB
|
||||||
|
var activeSQL *sql.DB
|
||||||
var lock = new(sync.Mutex)
|
var lock = new(sync.Mutex)
|
||||||
|
|
||||||
func GetDB() (*sql.DB, error) {
|
type ObjectBase struct {
|
||||||
|
ID int `json:"id" gorm:"primarykey"`
|
||||||
|
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime; not null"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime; not null"`
|
||||||
|
Deleted bool `json:"deleted" gorm:"default:false; index; not null"`
|
||||||
|
DeletedAt null.Time `json:"deleted_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func AutoMigrate() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.AutoMigrate(
|
||||||
|
&Award{},
|
||||||
|
&Course{},
|
||||||
|
&Member{},
|
||||||
|
&Rank{},
|
||||||
|
&TrainingEvent{},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDB() (*gorm.DB, error) {
|
||||||
|
|
||||||
if ActiveDB != nil {
|
if ActiveDB != nil {
|
||||||
return ActiveDB, nil
|
return ActiveDB, nil
|
||||||
@@ -26,17 +54,22 @@ func GetDB() (*sql.DB, error) {
|
|||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectDB() (*sql.DB, error) {
|
func ConnectDB() (*gorm.DB, error) {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
defer lock.Unlock()
|
defer lock.Unlock()
|
||||||
|
|
||||||
if ActiveDB != nil {
|
if activeSQL != nil {
|
||||||
ActiveDB.Close()
|
activeSQL.Close()
|
||||||
ActiveDB = nil
|
activeSQL = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := viper.GetViper()
|
cfg := viper.GetViper()
|
||||||
db, err := sql.Open("mysql", cfg.GetString("MARIADB_CONNSTRING"))
|
db, err := gorm.Open(mysql.Open(cfg.GetString("MARIADB_CONNSTRING")), &gorm.Config{
|
||||||
|
FullSaveAssociations: true,
|
||||||
|
PrepareStmt: true,
|
||||||
|
})
|
||||||
|
activeSQL, err = db.DB()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
219
api/db/member.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"gopkg.in/guregu/null.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
DDL
|
||||||
|
|
||||||
|
CREATE TABLE `members` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`timezone` varchar(5) DEFAULT NULL,
|
||||||
|
`email` varchar(100) DEFAULT NULL,
|
||||||
|
`website` varchar(240) DEFAULT NULL,
|
||||||
|
`guilded_id` varchar(10) DEFAULT NULL,
|
||||||
|
`steam_id_64` varchar(17) DEFAULT NULL,
|
||||||
|
`teamspeak_uid` varchar(32) DEFAULT NULL,
|
||||||
|
`steam_profile_name` varchar(32) DEFAULT NULL,
|
||||||
|
`discord_id` varchar(20) DEFAULT NULL,
|
||||||
|
`discord_username` varchar(32) DEFAULT NULL,
|
||||||
|
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`deleted` tinyint(4) DEFAULT NULL,
|
||||||
|
`remarks` text DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`),
|
||||||
|
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
|
||||||
|
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
|
||||||
|
UNIQUE KEY `guilded_id` (`guilded_id`)
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Member is a struct that represents a member
|
||||||
|
type Member struct {
|
||||||
|
ObjectBase
|
||||||
|
Name string `json:"name"`
|
||||||
|
Company null.String `json:"company"`
|
||||||
|
Timezone null.String `json:"timezone"`
|
||||||
|
Email null.String `json:"email"`
|
||||||
|
Website null.String `json:"website"`
|
||||||
|
GuildedID null.String `json:"guilded_id"`
|
||||||
|
SteamID64 null.String `json:"steam_id_64"`
|
||||||
|
TeamspeakUID null.String `json:"teamspeak_uid"`
|
||||||
|
SteamProfileName null.String `json:"steam_profile_name"`
|
||||||
|
DiscordID null.String `json:"discord_id"`
|
||||||
|
DiscordUsername null.String `json:"discord_username"`
|
||||||
|
Aliases null.String `json:"aliases"`
|
||||||
|
Remarks null.String `json:"remarks"`
|
||||||
|
|
||||||
|
Rank Rank `json:"rank" gorm:"references:ID; foreignkey:RankID"`
|
||||||
|
RankID int `json:"rank_id"`
|
||||||
|
Awards []Award `json:"awards" gorm:"many2many:member_awards;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by id
|
||||||
|
func (m *Member) GetByID(ctx context.Context, id int) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).First(m, id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
func (m *Member) GetAll(ctx context.Context) ([]Member, error) {
|
||||||
|
var members []Member
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by provided struct
|
||||||
|
err = db.WithContext(ctx).
|
||||||
|
Model(m).Where(m).Find(&members).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new member
|
||||||
|
func (m *Member) Create(ctx context.Context, c echo.Context) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Create(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a member
|
||||||
|
func (m *Member) Update(ctx context.Context, c echo.Context) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Save(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a member
|
||||||
|
func (m *Member) Delete(ctx context.Context, c echo.Context) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Delete(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by name
|
||||||
|
func (m *Member) GetByName(ctx context.Context, name string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("name = ?", name).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by steam id
|
||||||
|
func (m *Member) GetBySteamID(ctx context.Context, steamID string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("steam_id_64 = ?", steamID).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by discord id
|
||||||
|
func (m *Member) GetByDiscordID(ctx context.Context, discordID string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("discord_id = ?", discordID).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by guilded id
|
||||||
|
func (m *Member) GetByGuildedID(ctx context.Context, guildedID string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("guilded_id = ?", guildedID).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by teamspeak uid
|
||||||
|
func (m *Member) GetByTeamspeakUID(ctx context.Context, teamspeakUID string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("teamspeak_uid = ?", teamspeakUID).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by email
|
||||||
|
func (m *Member) GetByEmail(ctx context.Context, email string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Where("email = ?", email).First(m).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get awards a member has
|
||||||
|
func (m *Member) GetAwards(ctx context.Context) ([]Award, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = db.WithContext(ctx).Model(m).Association("Awards").Find(&m.Awards)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m.Awards, nil
|
||||||
|
}
|
||||||
109
api/db/rank.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
/*
|
||||||
|
DDL
|
||||||
|
|
||||||
|
CREATE TABLE `ranks` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` varchar(100) NOT NULL,
|
||||||
|
`short_name` varchar(70) NOT NULL,
|
||||||
|
`category` varchar(100) NOT NULL,
|
||||||
|
`sort_id` int(11) NOT NULL DEFAULT 0,
|
||||||
|
`image_url` varchar(240) DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`deleted` tinyint(4) DEFAULT 0,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `name` (`name`),
|
||||||
|
UNIQUE KEY `shortName` (`short_name`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4;
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Rank struct {
|
||||||
|
ObjectBase
|
||||||
|
Name string `json:"name"`
|
||||||
|
ShortName string `json:"short_name"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
SortID int `json:"sort_id"`
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
|
||||||
|
Members []Member `json:"members" gorm:"references:ID; foreignkey:RankID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by id
|
||||||
|
func (r *Rank) GetByID(id int) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.First(r, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by name
|
||||||
|
func (r *Rank) GetByName(name string) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Where("name = ?", name).First(r).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
func (r *Rank) GetAll() ([]Rank, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranks []Rank
|
||||||
|
err = db.Find(&ranks).Error
|
||||||
|
return ranks, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a member to a rank
|
||||||
|
func (r *Rank) AddHolder(m *Member) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Model(r).Association("Members").Append(m)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
func (r *Rank) Create() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Create(r).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
func (r *Rank) Update() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Save(r).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func (r *Rank) Delete() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Delete(r).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
113
api/db/trainingEvent.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
/*
|
||||||
|
DDL
|
||||||
|
|
||||||
|
CREATE TABLE `course_events` (
|
||||||
|
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
`course_id` int(11) DEFAULT NULL,
|
||||||
|
`event_type` int(11) DEFAULT NULL,
|
||||||
|
`event_date` datetime NOT NULL,
|
||||||
|
`guilded_event_id` int(11) DEFAULT NULL,
|
||||||
|
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
||||||
|
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||||
|
`deleted` tinyint(4) DEFAULT 0,
|
||||||
|
`report_url` varchar(2048) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `fk_course_events_event_type_id` (`event_type`) USING BTREE,
|
||||||
|
KEY `courseId` (`course_id`) USING BTREE,
|
||||||
|
CONSTRAINT `fk_coures_events_course_id` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT `fk_course_events_event_type_id` FOREIGN KEY (`event_type`) REFERENCES `event_types` (`id`) ON UPDATE CASCADE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
|
||||||
|
*/
|
||||||
|
|
||||||
|
type TrainingEvent struct {
|
||||||
|
ObjectBase
|
||||||
|
EventType int `json:"event_type"`
|
||||||
|
EventDate string `json:"event_date"`
|
||||||
|
GuildedEventID int `json:"guilded_event_id"`
|
||||||
|
ReportURL string `json:"report_url"`
|
||||||
|
|
||||||
|
Course Course `json:"course" gorm:"references:ID; foreignkey:CourseID"`
|
||||||
|
CourseID int `json:"course_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get one by id
|
||||||
|
func (te *TrainingEvent) GetByID(id int) error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.First(te, id).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
func (te *TrainingEvent) GetAll() ([]TrainingEvent, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tes []TrainingEvent
|
||||||
|
err = db.Find(&tes).Error
|
||||||
|
return tes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all by course
|
||||||
|
func (te *TrainingEvent) GetAllByCourse(courseID int) ([]TrainingEvent, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tes []TrainingEvent
|
||||||
|
err = db.Where("course_id = ?", courseID).Find(&tes).Error
|
||||||
|
return tes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create
|
||||||
|
func (te *TrainingEvent) Create() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Create(te).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update
|
||||||
|
func (te *TrainingEvent) Update() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Save(te).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
func (te *TrainingEvent) Delete() error {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Delete(te).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all by date range
|
||||||
|
func (te *TrainingEvent) GetByDateRange(start string, end string) ([]TrainingEvent, error) {
|
||||||
|
db, err := GetDB()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tes []TrainingEvent
|
||||||
|
err = db.Where("event_date BETWEEN ? AND ?", start, end).Find(&tes).Error
|
||||||
|
return tes, err
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/magiconair/properties v1.8.7 // indirect
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
@@ -40,4 +42,6 @@ require (
|
|||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.4 // indirect
|
||||||
|
gorm.io/gorm v1.25.7 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
10
api/go.sum
@@ -7,6 +7,7 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
|||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
@@ -16,6 +17,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
@@ -103,3 +108,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso=
|
||||||
|
gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs=
|
||||||
|
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ func main() {
|
|||||||
logger.Log.Info().Msg("Connected to the database")
|
logger.Log.Info().Msg("Connected to the database")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = db.AutoMigrate()
|
||||||
|
if err != nil {
|
||||||
|
logger.Log.Fatal().Err(err).Msg("Error migrating the database")
|
||||||
|
} else {
|
||||||
|
logger.Log.Info().Msg("Database migrated")
|
||||||
|
}
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
|
||||||
LogLatency: true,
|
LogLatency: true,
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
package ops
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"gopkg.in/guregu/null.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
DDL
|
|
||||||
|
|
||||||
CREATE TABLE `members` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(100) NOT NULL,
|
|
||||||
`timezone` varchar(5) DEFAULT NULL,
|
|
||||||
`email` varchar(100) DEFAULT NULL,
|
|
||||||
`website` varchar(240) DEFAULT NULL,
|
|
||||||
`guilded_id` varchar(10) DEFAULT NULL,
|
|
||||||
`steam_id_64` varchar(17) DEFAULT NULL,
|
|
||||||
`teamspeak_uid` varchar(32) DEFAULT NULL,
|
|
||||||
`steam_profile_name` varchar(32) DEFAULT NULL,
|
|
||||||
`discord_id` varchar(20) DEFAULT NULL,
|
|
||||||
`discord_username` varchar(32) DEFAULT NULL,
|
|
||||||
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
|
|
||||||
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
|
||||||
`deleted` tinyint(4) DEFAULT NULL,
|
|
||||||
`remarks` text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `name` (`name`),
|
|
||||||
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
|
|
||||||
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
|
|
||||||
UNIQUE KEY `guilded_id` (`guilded_id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Member is a struct that represents a member
|
|
||||||
type Member struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Timezone null.String `json:"timezone"`
|
|
||||||
Email null.String `json:"email"`
|
|
||||||
Website null.String `json:"website"`
|
|
||||||
GuildedID null.String `json:"guilded_id"`
|
|
||||||
SteamID64 null.String `json:"steam_id_64"`
|
|
||||||
TeamspeakUID null.String `json:"teamspeak_uid"`
|
|
||||||
SteamProfileName null.String `json:"steam_profile_name"`
|
|
||||||
DiscordID null.String `json:"discord_id"`
|
|
||||||
DiscordUsername null.String `json:"discord_username"`
|
|
||||||
Aliases null.String `json:"aliases"`
|
|
||||||
CreatedAt time.Time `json:"created_at"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
Deleted null.Int `json:"deleted"`
|
|
||||||
Remarks null.String `json:"remarks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
DDL
|
|
||||||
|
|
||||||
CREATE TABLE `members` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`name` varchar(100) NOT NULL,
|
|
||||||
`timezone` varchar(5) DEFAULT NULL,
|
|
||||||
`email` varchar(100) DEFAULT NULL,
|
|
||||||
`website` varchar(240) DEFAULT NULL,
|
|
||||||
`guilded_id` varchar(10) DEFAULT NULL,
|
|
||||||
`steam_id_64` varchar(17) DEFAULT NULL,
|
|
||||||
`teamspeak_uid` varchar(32) DEFAULT NULL,
|
|
||||||
`steam_profile_name` varchar(32) DEFAULT NULL,
|
|
||||||
`discord_id` varchar(20) DEFAULT NULL,
|
|
||||||
`discord_username` varchar(32) DEFAULT NULL,
|
|
||||||
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
|
|
||||||
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
|
|
||||||
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
|
||||||
`deleted` tinyint(4) DEFAULT NULL,
|
|
||||||
`remarks` text DEFAULT NULL,
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
UNIQUE KEY `name` (`name`),
|
|
||||||
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
|
|
||||||
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
|
|
||||||
UNIQUE KEY `guilded_id` (`guilded_id`)
|
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
|
|
||||||
*/
|
|
||||||
|
|
||||||
// GetMembers returns a list of all members
|
|
||||||
func GetMembers(c echo.Context) error {
|
|
||||||
|
|
||||||
members := []Member{}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(
|
|
||||||
context.Background(),
|
|
||||||
2*time.Second,
|
|
||||||
)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
rows, err := db.ActiveDB.QueryContext(
|
|
||||||
ctx,
|
|
||||||
"SELECT * FROM members",
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(500, map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return c.JSON(500, map[string]interface{}{
|
|
||||||
"error": "request timed out",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// scan the rows into the members slice
|
|
||||||
for rows.Next() {
|
|
||||||
var m Member
|
|
||||||
err = rows.Scan(
|
|
||||||
&m.ID,
|
|
||||||
&m.Name,
|
|
||||||
&m.Timezone,
|
|
||||||
&m.Email,
|
|
||||||
&m.Website,
|
|
||||||
&m.GuildedID,
|
|
||||||
&m.SteamID64,
|
|
||||||
&m.TeamspeakUID,
|
|
||||||
&m.SteamProfileName,
|
|
||||||
&m.DiscordID,
|
|
||||||
&m.DiscordUsername,
|
|
||||||
&m.Aliases,
|
|
||||||
&m.CreatedAt,
|
|
||||||
&m.UpdatedAt,
|
|
||||||
&m.Deleted,
|
|
||||||
&m.Remarks,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(500, map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
members = append(members, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(200, members)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMember returns a single member by ID
|
|
||||||
func GetMember(c echo.Context) error {
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
if id == "" {
|
|
||||||
return fmt.Errorf("id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(
|
|
||||||
context.Background(),
|
|
||||||
2*time.Second,
|
|
||||||
)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
returnMember := Member{}
|
|
||||||
err := db.ActiveDB.QueryRowContext(
|
|
||||||
ctx,
|
|
||||||
"SELECT * FROM members WHERE id = ?",
|
|
||||||
id,
|
|
||||||
).Scan(
|
|
||||||
&returnMember.ID,
|
|
||||||
&returnMember.Name,
|
|
||||||
&returnMember.Timezone,
|
|
||||||
&returnMember.Email,
|
|
||||||
&returnMember.Website,
|
|
||||||
&returnMember.GuildedID,
|
|
||||||
&returnMember.SteamID64,
|
|
||||||
&returnMember.TeamspeakUID,
|
|
||||||
&returnMember.SteamProfileName,
|
|
||||||
&returnMember.DiscordID,
|
|
||||||
&returnMember.DiscordUsername,
|
|
||||||
&returnMember.Aliases,
|
|
||||||
&returnMember.CreatedAt,
|
|
||||||
&returnMember.UpdatedAt,
|
|
||||||
&returnMember.Deleted,
|
|
||||||
&returnMember.Remarks,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return c.JSON(500, map[string]interface{}{
|
|
||||||
"error": err.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return c.JSON(500, map[string]interface{}{
|
|
||||||
"error": "request timed out",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(200, returnMember)
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
|
|
||||||
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/ops"
|
|
||||||
|
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -19,22 +16,8 @@ func SetupRoutes(
|
|||||||
|
|
||||||
cfg := viper.GetViper()
|
cfg := viper.GetViper()
|
||||||
prefixURL := strings.TrimRight(cfg.GetString("API_PREFIX"), "/")
|
prefixURL := strings.TrimRight(cfg.GetString("API_PREFIX"), "/")
|
||||||
mainPrefix := e.Group(prefixURL, func(next echo.HandlerFunc) echo.HandlerFunc {
|
mainPrefix := e.Group(prefixURL)
|
||||||
return func(c echo.Context) error {
|
|
||||||
// do something before the next handler
|
|
||||||
// ensure we always have a db connection
|
|
||||||
_, err := db.GetDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return next(c)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// MEMBER OPERATIONS
|
|
||||||
g := mainPrefix.Group("/members")
|
|
||||||
|
|
||||||
g.GET("", ops.GetMembers)
|
|
||||||
g.GET("/:id", ops.GetMember)
|
|
||||||
|
|
||||||
|
setupMemberRoutes(e, mainPrefix)
|
||||||
|
setupRankRoutes(e, mainPrefix)
|
||||||
}
|
}
|
||||||
|
|||||||
141
api/routes/member.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupMemberRoutes(
|
||||||
|
e *echo.Echo,
|
||||||
|
mainPrefix *echo.Group,
|
||||||
|
) {
|
||||||
|
// MEMBER OPERATIONS
|
||||||
|
g := mainPrefix.Group("/member")
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
g.GET("", func(c echo.Context) error {
|
||||||
|
var err error
|
||||||
|
member := new(db.Member)
|
||||||
|
|
||||||
|
// if query params are present, use them to filter
|
||||||
|
// otherwise, return all
|
||||||
|
rank := new(db.Rank)
|
||||||
|
if c.QueryParam("rank") != "" {
|
||||||
|
queriedID, err := strconv.Atoi(c.QueryParam("rank"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.GetByID(queriedID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
award := new(db.Award)
|
||||||
|
if c.QueryParam("award") != "" {
|
||||||
|
queriedID, err := strconv.Atoi(c.QueryParam("award"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = award.GetByID(queriedID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
course := new(db.Course)
|
||||||
|
if c.QueryParam("course") != "" {
|
||||||
|
queriedID, err := strconv.Atoi(c.QueryParam("course"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = course.GetByID(queriedID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
members, err := member.GetAll(c.Request().Context())
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, members)
|
||||||
|
})
|
||||||
|
// Get one by id
|
||||||
|
g.GET("/:id", func(c echo.Context) error {
|
||||||
|
member := new(db.Member)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.GetByID(c.Request().Context(), searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, member)
|
||||||
|
})
|
||||||
|
// Create a new member
|
||||||
|
g.POST("", func(c echo.Context) error {
|
||||||
|
member := new(db.Member)
|
||||||
|
err := c.Bind(member)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.Create(c.Request().Context(), c)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, member)
|
||||||
|
})
|
||||||
|
// Update a member
|
||||||
|
g.PUT("/:id", func(c echo.Context) error {
|
||||||
|
member := new(db.Member)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.GetByID(c.Request().Context(), searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
err = c.Bind(member)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.Update(c.Request().Context(), c)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, member)
|
||||||
|
})
|
||||||
|
// Delete a member
|
||||||
|
g.DELETE("/:id", func(c echo.Context) error {
|
||||||
|
member := new(db.Member)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.GetByID(c.Request().Context(), searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
err = member.Delete(c.Request().Context(), c)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, member)
|
||||||
|
})
|
||||||
|
}
|
||||||
127
api/routes/rank.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupRankRoutes(
|
||||||
|
e *echo.Echo,
|
||||||
|
mainPrefix *echo.Group,
|
||||||
|
) {
|
||||||
|
// RANK OPERATIONS
|
||||||
|
g := mainPrefix.Group("/rank")
|
||||||
|
|
||||||
|
// Get all
|
||||||
|
g.GET("", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
ranks, err := rank.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, ranks)
|
||||||
|
})
|
||||||
|
// Get one by id
|
||||||
|
g.GET("/:id", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.GetByID(searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, rank)
|
||||||
|
})
|
||||||
|
// Create a new rank
|
||||||
|
g.POST("", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
err := c.Bind(rank)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.Create()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, rank)
|
||||||
|
})
|
||||||
|
// Update a rank
|
||||||
|
g.PUT("/:id", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.GetByID(searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
err = c.Bind(rank)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.Update()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, rank)
|
||||||
|
})
|
||||||
|
// Delete a rank
|
||||||
|
g.DELETE("/:id", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.GetByID(searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
err = rank.Delete()
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, rank)
|
||||||
|
})
|
||||||
|
// Add a member to a rank
|
||||||
|
g.POST("/:id/member/:memberID", func(c echo.Context) error {
|
||||||
|
rank := new(db.Rank)
|
||||||
|
searchID, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = rank.GetByID(searchID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
member := new(db.Member)
|
||||||
|
searchMemberID, err := strconv.Atoi(c.Param("memberID"))
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(400, err)
|
||||||
|
}
|
||||||
|
err = member.GetByID(c.Request().Context(), searchMemberID)
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return c.JSON(404, err)
|
||||||
|
} else if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
err = rank.AddHolder(member)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(500, err)
|
||||||
|
}
|
||||||
|
return c.JSON(200, rank)
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
src/css/images/layers-2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/css/images/layers.png
Normal file
|
After Width: | Height: | Size: 696 B |
BIN
src/css/images/marker-icon-2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/css/images/marker-icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/css/images/marker-shadow.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
src/css/images/spritesheet-2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/css/images/spritesheet.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
156
src/css/images/spritesheet.svg
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
viewBox="0 0 600 60"
|
||||||
|
height="60"
|
||||||
|
width="600"
|
||||||
|
id="svg4225"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="spritesheet.svg"
|
||||||
|
inkscape:export-filename="/home/fpuga/development/upstream/icarto.Leaflet.draw/src/images/spritesheet-2x.png"
|
||||||
|
inkscape:export-xdpi="90"
|
||||||
|
inkscape:export-ydpi="90">
|
||||||
|
<metadata
|
||||||
|
id="metadata4258">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs4256" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1056"
|
||||||
|
id="namedview4254"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.3101852"
|
||||||
|
inkscape:cx="237.56928"
|
||||||
|
inkscape:cy="7.2419621"
|
||||||
|
inkscape:window-x="1920"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg4225" />
|
||||||
|
<g
|
||||||
|
id="enabled"
|
||||||
|
style="fill:#464646;fill-opacity:1">
|
||||||
|
<g
|
||||||
|
id="polyline"
|
||||||
|
style="fill:#464646;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 18,36 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
|
||||||
|
id="path4229"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 36,18 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
|
||||||
|
id="path4231"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 23.142,39.145 -2.285,-2.29 16,-15.998 2.285,2.285 z"
|
||||||
|
id="path4233"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="polygon"
|
||||||
|
d="M 100,24.565 97.904,39.395 83.07,42 76,28.773 86.463,18 Z"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
id="rectangle"
|
||||||
|
d="m 140,20 20,0 0,20 -20,0 z"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
id="circle"
|
||||||
|
d="m 221,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
id="marker"
|
||||||
|
d="m 270,19 c -4.971,0 -9,4.029 -9,9 0,4.971 5.001,12 9,14 4.001,-2 9,-9.029 9,-14 0,-4.971 -4.029,-9 -9,-9 z m 0,12.5 c -2.484,0 -4.5,-2.014 -4.5,-4.5 0,-2.484 2.016,-4.5 4.5,-4.5 2.485,0 4.5,2.016 4.5,4.5 0,2.486 -2.015,4.5 -4.5,4.5 z"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<g
|
||||||
|
id="edit"
|
||||||
|
style="fill:#464646;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 337,30.156 0,0.407 0,5.604 c 0,1.658 -1.344,3 -3,3 l -10,0 c -1.655,0 -3,-1.342 -3,-3 l 0,-10 c 0,-1.657 1.345,-3 3,-3 l 6.345,0 3.19,-3.17 -9.535,0 c -3.313,0 -6,2.687 -6,6 l 0,10 c 0,3.313 2.687,6 6,6 l 10,0 c 3.314,0 6,-2.687 6,-6 l 0,-8.809 -3,2.968"
|
||||||
|
id="path4240"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 338.72,24.637 -8.892,8.892 -2.828,0 0,-2.829 8.89,-8.89 z"
|
||||||
|
id="path4242"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 338.697,17.826 4,0 0,4 -4,0 z"
|
||||||
|
transform="matrix(-0.70698336,-0.70723018,0.70723018,-0.70698336,567.55917,274.78273)"
|
||||||
|
id="path4244"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="remove"
|
||||||
|
style="fill:#464646;fill-opacity:1">
|
||||||
|
<path
|
||||||
|
d="m 381,42 18,0 0,-18 -18,0 0,18 z m 14,-16 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z"
|
||||||
|
id="path4247"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
<path
|
||||||
|
d="m 395,20 0,-4 -10,0 0,4 -6,0 0,2 22,0 0,-2 -6,0 z m -2,0 -6,0 0,-2 6,0 0,2 z"
|
||||||
|
id="path4249"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="fill:#464646;fill-opacity:1" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="disabled"
|
||||||
|
transform="translate(120,0)"
|
||||||
|
style="fill:#bbbbbb">
|
||||||
|
<use
|
||||||
|
xlink:href="#edit"
|
||||||
|
id="edit-disabled"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%" />
|
||||||
|
<use
|
||||||
|
xlink:href="#remove"
|
||||||
|
id="remove-disabled"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="100%"
|
||||||
|
height="100%" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
style="fill:none;stroke:#464646;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="circle-3"
|
||||||
|
d="m 581.65725,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
|
||||||
|
inkscape:connector-curvature="0" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.4 KiB |
6
src/css/input.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import 'leaflet.draw.css';
|
||||||
|
@import 'mapUtils.css';
|
||||||
10
src/css/leaflet.draw.css
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')}
|
||||||
|
.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block}
|
||||||
|
.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px}
|
||||||
|
.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px}
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px}
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}
|
||||||
|
.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}
|
||||||
|
.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}
|
||||||
|
.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}
|
||||||
|
.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999}
|
||||||
117
src/css/mapUtils.css
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
.leaflet-container .leaflet-grid-mouseposition {
|
||||||
|
background-color: rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: 0 0 5px #bbb;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 20px;
|
||||||
|
color: #000;
|
||||||
|
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
max-width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawingTooltip {
|
||||||
|
/* transparent white background */
|
||||||
|
background-color: rgba(255, 255, 255, 0.5);
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #000;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
#marker-select-container {
|
||||||
|
width: 300px;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-select {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: gray;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin: 8px;
|
||||||
|
outline: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-select-option {
|
||||||
|
/* make rows with name and preview to the right */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: left;
|
||||||
|
/* align text in center, vertically */
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding: 2px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-select-image {
|
||||||
|
margin: 2px;
|
||||||
|
width: 128px;
|
||||||
|
max-height: 128px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-select-name {
|
||||||
|
font-size: 12px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before,
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.sp-replacer {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: unset !important;
|
||||||
|
display: inline-block;
|
||||||
|
border: unset !important;
|
||||||
|
background: unset !important;
|
||||||
|
color: #333;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-dd {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sp-preview {
|
||||||
|
margin-right: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pm-toolbar .leaflet-pm-icon-export {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJfMSI+Cgk8cGF0aCBkPSJNMTg2LDUwLjFoMTM4LjNjOC4zLDAsMTUsNi43LDE1LDE1VjIwMmg1Ni4yYzguMywwLDE1LDYuNywxNSwxNWMwLDMuNy0xLjQsNy4zLTMuOSwxMC4xTDI2Ni4zLDM4Mi43CgkJYy01LjUsNi4yLTE1LDYuNy0yMS4yLDEuMWMtMC40LTAuMy0wLjgtMC43LTEuMS0xLjFMMTAzLjcsMjI3LjFjLTUuNi02LjEtNS4xLTE1LjYsMS4xLTIxLjJjMi44LTIuNSw2LjMtMy45LDEwLjEtMy45SDE3MVY2NS4xCgkJQzE3MSw1Ni45LDE3Ny43LDUwLjEsMTg2LDUwLjF6IE0zMDkuMyw4MC4xSDIwMVYyMTdjMCw4LjMtNi43LDE1LTE1LDE1bDAsMGgtMzcuNGwxMDYuNiwxMTguMkwzNjEuNywyMzJoLTM3LjUKCQljLTguMywwLTE1LTYuNy0xNS0xNWwwLDBWODAuMXoiLz4KCTxwYXRoIGQ9Ik0zOTAuOCw0NTIuMUgxMTkuNGMtMzYsMC02NS4zLTI5LjMtNjUuMy02NS4zdi0zOC4yYzAtOC4zLDYuNy0xNSwxNS0xNXMxNSw2LjcsMTUsMTV2MzguMmMwLDE5LjUsMTUuOCwzNS4zLDM1LjMsMzUuMwoJCWgyNzEuNGMxOS41LDAsMzUuMy0xNS44LDM1LjMtMzUuM3YtMzguMmMwLTguMyw2LjctMTUsMTUtMTVzMTUsNi43LDE1LDE1djM4LjJDNDU2LjEsNDIyLjksNDI2LjksNDUyLjEsMzkwLjgsNDUyLjF6Ii8+CjwvZz4KPC9zdmc+Cg==")
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pm-toolbar .leaflet-pm-icon-import {
|
||||||
|
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlLz48ZyBkYXRhLW5hbWU9IjEiIGlkPSJfMSI+PHBhdGggZD0iTTMyNC4zLDM4Ny42OUgxODZhMTUsMTUsMCwwLDEtMTUtMTVWMjM1LjhIMTE0LjgxYTE1LDE1LDAsMCwxLTExLjE0LTI1LjA1TDI0NCw1NS4xYTE1LDE1LDAsMCwxLDIyLjI5LDBMNDA2LjYsMjEwLjc1YTE1LDE1LDAsMCwxLTExLjE0LDI1LjA1SDMzOS4zVjM3Mi42OUExNSwxNSwwLDAsMSwzMjQuMywzODcuNjlaTTIwMSwzNTcuNjlIMzA5LjNWMjIwLjhhMTUsMTUsMCwwLDEsMTUtMTVoMzcuNDRMMjU1LjEzLDg3LjU1LDE0OC41MywyMDUuOEgxODZhMTUsMTUsMCwwLDEsMTUsMTVaIi8+PHBhdGggZD0iTTM5MC44NCw0NTIuMTVIMTE5LjQzYTY1LjM3LDY1LjM3LDAsMCwxLTY1LjMtNjUuM1YzNDguNjhhMTUsMTUsMCwwLDEsMzAsMHYzOC4xN2EzNS4zNCwzNS4zNCwwLDAsMCwzNS4zLDM1LjNIMzkwLjg0YTM1LjMzLDM1LjMzLDAsMCwwLDM1LjI5LTM1LjNWMzQ4LjY4YTE1LDE1LDAsMSwxLDMwLDB2MzguMTdBNjUuMzcsNjUuMzcsMCwwLDEsMzkwLjg0LDQ1Mi4xNVoiLz48L2c+PC9zdmc+");
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pm-toolbar .leaflet-pm-icon-logout {
|
||||||
|
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgZmlsbD0iIzAwMDAwMCIgaGVpZ2h0PSI4MDBweCIgd2lkdGg9IjgwMHB4IiB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIA0KCSB2aWV3Qm94PSIwIDAgMzg0Ljk3MSAzODQuOTcxIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxnIGlkPSJTaWduX091dCI+DQoJCTxwYXRoIGQ9Ik0xODAuNDU1LDM2MC45MUgyNC4wNjFWMjQuMDYxaDE1Ni4zOTRjNi42NDEsMCwxMi4wMy01LjM5LDEyLjAzLTEyLjAzcy01LjM5LTEyLjAzLTEyLjAzLTEyLjAzSDEyLjAzDQoJCQlDNS4zOSwwLjAwMSwwLDUuMzksMCwxMi4wMzFWMzcyLjk0YzAsNi42NDEsNS4zOSwxMi4wMywxMi4wMywxMi4wM2gxNjguNDI0YzYuNjQxLDAsMTIuMDMtNS4zOSwxMi4wMy0xMi4wMw0KCQkJQzE5Mi40ODUsMzY2LjI5OSwxODcuMDk1LDM2MC45MSwxODAuNDU1LDM2MC45MXoiLz4NCgkJPHBhdGggZD0iTTM4MS40ODEsMTg0LjA4OGwtODMuMDA5LTg0LjJjLTQuNzA0LTQuNzUyLTEyLjMxOS00Ljc0LTE3LjAxMSwwYy00LjcwNCw0Ljc0LTQuNzA0LDEyLjQzOSwwLDE3LjE3OWw2Mi41NTgsNjMuNDZIOTYuMjc5DQoJCQljLTYuNjQxLDAtMTIuMDMsNS40MzgtMTIuMDMsMTIuMTUxYzAsNi43MTMsNS4zOSwxMi4xNTEsMTIuMDMsMTIuMTUxaDI0Ny43NGwtNjIuNTU4LDYzLjQ2Yy00LjcwNCw0Ljc1Mi00LjcwNCwxMi40MzksMCwxNy4xNzkNCgkJCWM0LjcwNCw0Ljc1MiwxMi4zMTksNC43NTIsMTcuMDExLDBsODIuOTk3LTg0LjJDMzg2LjExMywxOTYuNTg4LDM4Ni4xNjEsMTg4Ljc1NiwzODEuNDgxLDE4NC4wODh6Ii8+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KPC9nPg0KPC9zdmc+');
|
||||||
|
}
|
||||||
34
src/draw.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Arma 3 Planner</title>
|
||||||
|
<link href="index.css" rel="stylesheet">
|
||||||
|
<script defer src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body style="margin: 0; padding: 0; border: 0">
|
||||||
|
<div class="mapContainer" id="mapContainer" style="width: 100%; height: 100vh; margin: 0; padding: 0; border: 0">
|
||||||
|
</div>
|
||||||
|
<div id="marker-select-container">
|
||||||
|
<div id="marker-select"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Maplibre GL -->
|
||||||
|
<link href="https://unpkg.com/maplibre-gl@2.2.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||||
|
<script src="https://unpkg.com/maplibre-gl@2.2.1/dist/maplibre-gl.js"></script>
|
||||||
|
<script src="https://unpkg.com/pmtiles@2.9.0/dist/index.js"></script>
|
||||||
|
<script src="https://unpkg.com/proj4@2.5.0/dist/proj4.js"></script>
|
||||||
|
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.0.17/leaflet-maplibre-gl.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
|
<script>
|
||||||
|
getWorldData("gulfcoast");
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
778
src/index.css
Normal file
@@ -0,0 +1,778 @@
|
|||||||
|
/*
|
||||||
|
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||||
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||||
|
*/
|
||||||
|
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* 1 */
|
||||||
|
border-width: 0;
|
||||||
|
/* 2 */
|
||||||
|
border-style: solid;
|
||||||
|
/* 2 */
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
--tw-content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use a consistent sensible line-height in all browsers.
|
||||||
|
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||||
|
3. Use a more readable tab size.
|
||||||
|
4. Use the user's configured `sans` font-family by default.
|
||||||
|
5. Use the user's configured `sans` font-feature-settings by default.
|
||||||
|
6. Use the user's configured `sans` font-variation-settings by default.
|
||||||
|
7. Disable tap highlights on iOS
|
||||||
|
*/
|
||||||
|
|
||||||
|
html,
|
||||||
|
:host {
|
||||||
|
line-height: 1.5;
|
||||||
|
/* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
/* 2 */
|
||||||
|
-moz-tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
-o-tab-size: 4;
|
||||||
|
tab-size: 4;
|
||||||
|
/* 3 */
|
||||||
|
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||||
|
/* 4 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 5 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 6 */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
/* 7 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove the margin in all browsers.
|
||||||
|
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Add the correct height in Firefox.
|
||||||
|
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||||
|
3. Ensure horizontal rules are visible by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-top-width: 1px;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr:where([title]) {
|
||||||
|
-webkit-text-decoration: underline dotted;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the default font size and weight for headings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset links to optimize for opt-in styling instead of opt-out.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font weight in Edge and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Use the user's configured `mono` font-family by default.
|
||||||
|
2. Use the user's configured `mono` font-feature-settings by default.
|
||||||
|
3. Use the user's configured `mono` font-variation-settings by default.
|
||||||
|
4. Correct the odd `em` font sizing in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp,
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: normal;
|
||||||
|
/* 2 */
|
||||||
|
font-variation-settings: normal;
|
||||||
|
/* 3 */
|
||||||
|
font-size: 1em;
|
||||||
|
/* 4 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||||
|
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||||
|
3. Remove gaps between table borders by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
text-indent: 0;
|
||||||
|
/* 1 */
|
||||||
|
border-color: inherit;
|
||||||
|
/* 2 */
|
||||||
|
border-collapse: collapse;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Change the font styles in all browsers.
|
||||||
|
2. Remove the margin in Firefox and Safari.
|
||||||
|
3. Remove default padding in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-feature-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-variation-settings: inherit;
|
||||||
|
/* 1 */
|
||||||
|
font-size: 100%;
|
||||||
|
/* 1 */
|
||||||
|
font-weight: inherit;
|
||||||
|
/* 1 */
|
||||||
|
line-height: inherit;
|
||||||
|
/* 1 */
|
||||||
|
color: inherit;
|
||||||
|
/* 1 */
|
||||||
|
margin: 0;
|
||||||
|
/* 2 */
|
||||||
|
padding: 0;
|
||||||
|
/* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inheritance of text transform in Edge and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Remove default button styles.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[type='button'],
|
||||||
|
[type='reset'],
|
||||||
|
[type='submit'] {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
background-color: transparent;
|
||||||
|
/* 2 */
|
||||||
|
background-image: none;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Use the modern Firefox focus style for all focusable elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-focusring {
|
||||||
|
outline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||||
|
*/
|
||||||
|
|
||||||
|
:-moz-ui-invalid {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct vertical alignment in Chrome and Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
progress {
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Correct the cursor style of increment and decrement buttons in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-inner-spin-button,
|
||||||
|
::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the odd appearance in Chrome and Safari.
|
||||||
|
2. Correct the outline style in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[type='search'] {
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
/* 1 */
|
||||||
|
outline-offset: -2px;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Remove the inner padding in Chrome and Safari on macOS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Correct the inability to style clickable types in iOS and Safari.
|
||||||
|
2. Change font properties to `inherit` in Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
::-webkit-file-upload-button {
|
||||||
|
-webkit-appearance: button;
|
||||||
|
/* 1 */
|
||||||
|
font: inherit;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add the correct display in Chrome and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
summary {
|
||||||
|
display: list-item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Removes the default spacing and border for appropriate elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
blockquote,
|
||||||
|
dl,
|
||||||
|
dd,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
hr,
|
||||||
|
figure,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
menu {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Reset default styling for dialogs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Prevent resizing textareas horizontally by default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||||
|
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder,
|
||||||
|
textarea::placeholder {
|
||||||
|
opacity: 1;
|
||||||
|
/* 1 */
|
||||||
|
color: #9ca3af;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the default cursor for buttons.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
[role="button"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Make sure disabled buttons don't get the pointer cursor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||||
|
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
svg,
|
||||||
|
video,
|
||||||
|
canvas,
|
||||||
|
audio,
|
||||||
|
iframe,
|
||||||
|
embed,
|
||||||
|
object {
|
||||||
|
display: block;
|
||||||
|
/* 1 */
|
||||||
|
vertical-align: middle;
|
||||||
|
/* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||||
|
*/
|
||||||
|
|
||||||
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
*, ::before, ::after {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
::backdrop {
|
||||||
|
--tw-border-spacing-x: 0;
|
||||||
|
--tw-border-spacing-y: 0;
|
||||||
|
--tw-translate-x: 0;
|
||||||
|
--tw-translate-y: 0;
|
||||||
|
--tw-rotate: 0;
|
||||||
|
--tw-skew-x: 0;
|
||||||
|
--tw-skew-y: 0;
|
||||||
|
--tw-scale-x: 1;
|
||||||
|
--tw-scale-y: 1;
|
||||||
|
--tw-pan-x: ;
|
||||||
|
--tw-pan-y: ;
|
||||||
|
--tw-pinch-zoom: ;
|
||||||
|
--tw-scroll-snap-strictness: proximity;
|
||||||
|
--tw-gradient-from-position: ;
|
||||||
|
--tw-gradient-via-position: ;
|
||||||
|
--tw-gradient-to-position: ;
|
||||||
|
--tw-ordinal: ;
|
||||||
|
--tw-slashed-zero: ;
|
||||||
|
--tw-numeric-figure: ;
|
||||||
|
--tw-numeric-spacing: ;
|
||||||
|
--tw-numeric-fraction: ;
|
||||||
|
--tw-ring-inset: ;
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||||
|
--tw-ring-offset-shadow: 0 0 #0000;
|
||||||
|
--tw-ring-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow: 0 0 #0000;
|
||||||
|
--tw-shadow-colored: 0 0 #0000;
|
||||||
|
--tw-blur: ;
|
||||||
|
--tw-brightness: ;
|
||||||
|
--tw-contrast: ;
|
||||||
|
--tw-grayscale: ;
|
||||||
|
--tw-hue-rotate: ;
|
||||||
|
--tw-invert: ;
|
||||||
|
--tw-saturate: ;
|
||||||
|
--tw-sepia: ;
|
||||||
|
--tw-drop-shadow: ;
|
||||||
|
--tw-backdrop-blur: ;
|
||||||
|
--tw-backdrop-brightness: ;
|
||||||
|
--tw-backdrop-contrast: ;
|
||||||
|
--tw-backdrop-grayscale: ;
|
||||||
|
--tw-backdrop-hue-rotate: ;
|
||||||
|
--tw-backdrop-invert: ;
|
||||||
|
--tw-backdrop-opacity: ;
|
||||||
|
--tw-backdrop-saturate: ;
|
||||||
|
--tw-backdrop-sepia: ;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.container {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.container {
|
||||||
|
max-width: 768px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1024px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1280px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1536px) {
|
||||||
|
.container {
|
||||||
|
max-width: 1536px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-auto {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb-8 {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mt-8 {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-4 {
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-screen {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-1\/3 {
|
||||||
|
width: 33.333333%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-md {
|
||||||
|
max-width: 28rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-lg {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border {
|
||||||
|
border-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-2 {
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-gray-300 {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-blue-500 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gray-100 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-green-500 {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-4 {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.px-4 {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.py-2 {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-3xl {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
line-height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-4xl {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-gray-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(107 114 128 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(34 197 94 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(239 68 68 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(255 255 255 / var(--tw-text-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-blue-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-green-700:hover {
|
||||||
|
--tw-bg-opacity: 1;
|
||||||
|
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
|
||||||
|
}
|
||||||
58
src/index.html
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!-- This is a basic page with two centered sections, one to create a new session and one to input an existing session UUID with a button to search and join. -->
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Arma 3 Planner</title>
|
||||||
|
<link href="index.css" rel="stylesheet">
|
||||||
|
<script defer src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
|
||||||
|
<div class="flex justify-center items-center h-screen">
|
||||||
|
<div class="w-1/3">
|
||||||
|
<h1 class="text-4xl font-bold text-center mb-8">Arma 3 Planner</h1>
|
||||||
|
<!-- Spacer -->
|
||||||
|
<div class="h-4"></div>
|
||||||
|
|
||||||
|
<!-- Surround the below with a border -->
|
||||||
|
<div class="border-2 border-gray-300 p-4 rounded mb-4">
|
||||||
|
|
||||||
|
<!-- Create session will post credentials and generate session -->
|
||||||
|
<form action="/create" method="post">
|
||||||
|
<input type="text" name="username" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
autocomplete="nickname" placeholder="Your Name">
|
||||||
|
<input type="text" name="sessionName" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
placeholder="Session Name">
|
||||||
|
<input type="password" name="sessionPassword" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
autocomplete="current-password" placeholder="Session Password">
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mb-4">Create New
|
||||||
|
Session</button>
|
||||||
|
</form>
|
||||||
|
<!-- divider -->
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<span class="text-gray-500">or</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Submitting form will trigger JS method to send RabbitMQ data -->
|
||||||
|
<form onsubmit="joinSession(event)" class="mb-4">
|
||||||
|
<input type="text" name="username" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
autocomplete="nickname" placeholder="Your Name">
|
||||||
|
<input type="text" name="sessionCode" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
placeholder="Session Code">
|
||||||
|
<input type="password" name="sessionPassword" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
|
||||||
|
autocomplete="current-password" placeholder="Session Password">
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join
|
||||||
|
Session</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
11
src/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// import all js
|
||||||
|
import './src/js/leaflet.draw.js'
|
||||||
|
import './src/js/defaultMap.js'
|
||||||
|
import './src/js/mapUtils.js'
|
||||||
|
import './src/js/sessionMgmt.js'
|
||||||
|
|
||||||
|
|
||||||
|
import * as L from "leaflet";
|
||||||
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import "@geoman-io/leaflet-geoman-free";
|
||||||
|
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
|
||||||
552
src/js/defaultMap.js
Normal file
@@ -0,0 +1,552 @@
|
|||||||
|
function getWorldData (worldName) {
|
||||||
|
// Get the metadata of the map
|
||||||
|
$.getJSON(`https://styles.ocap2.com/${worldName}.json`)
|
||||||
|
.done(function (styleJson) {
|
||||||
|
window.worldMeta = styleJson.metadata;
|
||||||
|
return InitMap(styleJson.metadata);
|
||||||
|
}).fail(function (jqxhr, textStatus, error) {
|
||||||
|
var err = textStatus + ', ' + error;
|
||||||
|
console.error('Request Failed: ' + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function InitMap (worldMeta) {
|
||||||
|
$(function () {
|
||||||
|
|
||||||
|
if (!worldMeta) {
|
||||||
|
throw new Error('World metadata not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
map = L.map('mapContainer', {
|
||||||
|
// crs: mapInfos.CRS,
|
||||||
|
crs: L.CRS.EPSG3857,
|
||||||
|
maxZoom: 19,
|
||||||
|
minZoom: 12,
|
||||||
|
zoom: 12,
|
||||||
|
zoomControl: true,
|
||||||
|
center: worldMeta.center,
|
||||||
|
attributionControl: false, // set up later
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
console.debug('Map initialized', map);
|
||||||
|
console.debug('World metadata', worldMeta);
|
||||||
|
|
||||||
|
|
||||||
|
// Set initial bounds
|
||||||
|
var mapBounds = [[
|
||||||
|
worldMeta.bounds[0],
|
||||||
|
worldMeta.bounds[1],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
worldMeta.bounds[2],
|
||||||
|
worldMeta.bounds[3],
|
||||||
|
]]
|
||||||
|
console.debug('Map bounds', mapBounds);
|
||||||
|
|
||||||
|
map.fitBounds(mapBounds, {
|
||||||
|
padding: [0, 0],
|
||||||
|
maxZoom: 19,
|
||||||
|
animate: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Limit map bounds (panning) at 3x the map size
|
||||||
|
var bbox = turf.bboxPolygon(worldMeta.bounds);
|
||||||
|
// console.debug('Map bbox', bbox);
|
||||||
|
var limitBoundsPoly = turf.transformScale(bbox, 3);
|
||||||
|
// console.debug('Map limitBoundspoly', limitBoundsPoly);
|
||||||
|
var limitBounds = limitBoundsPoly.geometry.coordinates[0];
|
||||||
|
// console.debug('Map limitBounds', limitBounds);
|
||||||
|
map.setMaxBounds(limitBounds);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ADD TOP RIGHT CONTROLS
|
||||||
|
// add control in top right with recent sessions
|
||||||
|
const recentSessionsControl = L.control({
|
||||||
|
position: 'topright',
|
||||||
|
})
|
||||||
|
recentSessionsControl.onAdd = (map) => {
|
||||||
|
var recentSessions = localStorage.getItem('recentSessions') || '[]';
|
||||||
|
// If session search param set, store in cache
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
const session = urlParams.get('session');
|
||||||
|
if (session) {
|
||||||
|
recentSessions = JSON.parse(recentSessions);
|
||||||
|
if (!recentSessions.includes(session)) {
|
||||||
|
// if not already listed, insert at beginning of array
|
||||||
|
recentSessions.unshift(session);
|
||||||
|
localStorage.setItem('recentSessions', JSON.stringify(recentSessions));
|
||||||
|
} else {
|
||||||
|
// move to top if already in array
|
||||||
|
recentSessions = recentSessions.filter((s) => s !== session);
|
||||||
|
recentSessions.unshift(session);
|
||||||
|
localStorage.setItem('recentSessions', JSON.stringify(recentSessions));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use recent missions to populate info window
|
||||||
|
var recentSessionsHtml = '<div id="recentSessions" style="background-color: white; padding: 10px;"><h3>Recent Sessions</h3><ul>';
|
||||||
|
for (let session of recentSessions || []) {
|
||||||
|
if (session === recentSessions[0]) {
|
||||||
|
// if first, add a "current" label
|
||||||
|
recentSessionsHtml += '<li>Current: <a href="/draw?session=' + session + '">' + session + '</a></li>';
|
||||||
|
} else {
|
||||||
|
// otherwise, add normally
|
||||||
|
recentSessionsHtml += '<li><a href="/draw?session=' + session + '">' + session + '</a></li>';
|
||||||
|
}
|
||||||
|
// skip any past 7
|
||||||
|
if (session === recentSessions[6]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentSessionsHtml += '</ul></div>';
|
||||||
|
|
||||||
|
var div = L.DomUtil.create('div', 'recentSessions');
|
||||||
|
div.innerHTML = recentSessionsHtml;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish adding the top right controls
|
||||||
|
recentSessionsControl.addTo(map);
|
||||||
|
L.control.gridMousePosition().addTo(map);
|
||||||
|
|
||||||
|
// ADD BOTTOM LEFT CONTROLS
|
||||||
|
L.control.scale({
|
||||||
|
maxWidth: 200,
|
||||||
|
imperial: false
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// ADD BOTTOM RIGHT CONTROLS
|
||||||
|
// set the Leaflet attribution control
|
||||||
|
const attributionControl = L.control.attribution({
|
||||||
|
position: 'bottomright',
|
||||||
|
});
|
||||||
|
attributionControl.addAttribution(worldMeta.attribution)
|
||||||
|
attributionControl.addTo(map);
|
||||||
|
|
||||||
|
// ADD OTHER
|
||||||
|
L.latlngGraticule({
|
||||||
|
color: '#777',
|
||||||
|
font: '12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif',
|
||||||
|
fontColor: '#777',
|
||||||
|
zoomInterval: [
|
||||||
|
{ start: 12, end: 13, interval: 10000 },
|
||||||
|
{ start: 13, end: 16, interval: 1000 },
|
||||||
|
{ start: 16, end: 20, interval: 100 }
|
||||||
|
]
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// set up maplibre vector basemap
|
||||||
|
let protocol = new pmtiles.Protocol();
|
||||||
|
maplibregl.addProtocol("pmtiles", protocol.tile);
|
||||||
|
window.maplibre = L.maplibreGL({
|
||||||
|
style: 'https://styles.ocap2.com/' + worldMeta.worldname + '.json',
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 24,
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
|
||||||
|
if (window.location.hash == '#cities') {
|
||||||
|
$.each(mapInfos.cities, function (index, city) {
|
||||||
|
L.marker([city.y, city.x]).addTo(map).bindPopup(city.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to update draw colors based on the selected color
|
||||||
|
window.DRAW_COLOR = '#3388ff';
|
||||||
|
function updateDrawColors (color) {
|
||||||
|
DRAW_COLOR = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// load w/ content
|
||||||
|
async function fetchAvailableMarkers () {
|
||||||
|
return fetch('/markers')
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
console.debug('Loaded markers', data)
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fetchAvailableMarkers().then(async (markers) => {
|
||||||
|
|
||||||
|
var markerSelectContainer = document.getElementById('marker-select-container');
|
||||||
|
|
||||||
|
// select addon keys of all markers array and dedupe
|
||||||
|
var addons = [...new Set(markers.addon)];
|
||||||
|
|
||||||
|
// Add markers to the marker select
|
||||||
|
for (let addon of addons) {
|
||||||
|
// Add a header for the addon
|
||||||
|
var addonHeader = document.createElement('h3');
|
||||||
|
addonHeader.innerHTML = addon;
|
||||||
|
markerSelectContainer.appendChild(addonHeader);
|
||||||
|
// Add a container for the addon's markers
|
||||||
|
var markerSelect = document.createElement('div');
|
||||||
|
markerSelect.className = 'marker-select';
|
||||||
|
markerSelectContainer.appendChild(markerSelect);
|
||||||
|
|
||||||
|
// Add each marker to the marker select
|
||||||
|
// get all markers where addon matches
|
||||||
|
for (let marker of markers.all) {
|
||||||
|
if (marker.addon === addon) {
|
||||||
|
var markerOption = document.createElement('div');
|
||||||
|
markerOption.className = 'marker-select-option';
|
||||||
|
markerOption.innerHTML = `<span class="marker-select-name">${marker.name}</span><img src="${marker.url}" class="marker-select-image" />`;
|
||||||
|
markerOption.addEventListener('click', function () {
|
||||||
|
// Set the marker image
|
||||||
|
var markerImage = document.getElementById('image-url');
|
||||||
|
markerImage.value = marker.url;
|
||||||
|
// Set the marker description
|
||||||
|
var markerDescription = document.getElementById('description');
|
||||||
|
markerDescription.value = marker.description;
|
||||||
|
});
|
||||||
|
markerSelect.appendChild(markerOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to prompt the user to select a marker
|
||||||
|
function promptMarkerSelection () {
|
||||||
|
// open a dialog to select a marker
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Select a Marker',
|
||||||
|
html: document.getElementById('marker-select-container'),
|
||||||
|
showConfirmButton: false,
|
||||||
|
showCloseButton: true,
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
cancelButtonColor: '#d33',
|
||||||
|
onOpen: () => {
|
||||||
|
// Add a listener to the marker select button
|
||||||
|
document.getElementById('marker-select-button').addEventListener('click', function () {
|
||||||
|
Swal.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// promptMarkerSelection()
|
||||||
|
|
||||||
|
|
||||||
|
// Update the 'pm:create' event listener
|
||||||
|
map.on('pm:create', function (event) {
|
||||||
|
console.log(event.shape);
|
||||||
|
var layer = event.layer;
|
||||||
|
var imageUrl;
|
||||||
|
var description;
|
||||||
|
|
||||||
|
if (event.shape === 'Marker') {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Set Marker Image and Description',
|
||||||
|
html: `
|
||||||
|
<p>Marker URL</p>
|
||||||
|
<input id="image-url" class="swal2-input" placeholder="Enter the URL of the marker image">
|
||||||
|
<p>You can also click here to select a marker image</p>
|
||||||
|
<button id="marker-select-button" class="swal2-input" style="width: 100%; height: 50px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-bottom: 10px;" onClick="promptMarkerSelection()">Select Marker</button>
|
||||||
|
<textarea id="description" rows="4" class="swal2-textarea" placeholder="Enter a description for the drawing"></textarea>
|
||||||
|
`,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Set',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
allowOutsideClick: false,
|
||||||
|
preConfirm: () => {
|
||||||
|
imageUrl = document.getElementById('image-url').value;
|
||||||
|
description = document.getElementById('description').value;
|
||||||
|
if (!imageUrl || imageUrl == '') {
|
||||||
|
imageUrl = 'https://i.imgur.com/SY0C1lx.png';
|
||||||
|
}
|
||||||
|
var icon = L.icon({
|
||||||
|
iconUrl: imageUrl,
|
||||||
|
iconSize: [25, 41] // Adjust the size of the icon if needed
|
||||||
|
});
|
||||||
|
layer.setIcon(icon);
|
||||||
|
|
||||||
|
if (description.trim() !== '') {
|
||||||
|
layer.description = description;
|
||||||
|
layer.bindPopup(description).openPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
// Get the selected color from the color picker
|
||||||
|
var selectedColor = $('#colorPicker').spectrum('get').toHexString();
|
||||||
|
|
||||||
|
// Send the drawing data to the server
|
||||||
|
saveDrawingToServer(layer, description, selectedColor, imageUrl);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
|
||||||
|
Swal.fire({
|
||||||
|
title: 'Enter a description for the drawing:',
|
||||||
|
input: 'textarea',
|
||||||
|
inputPlaceholder: 'Description',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'Save',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
allowOutsideClick: false
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
description = result.value;
|
||||||
|
layer.description = description;
|
||||||
|
layer.bindPopup(description);
|
||||||
|
|
||||||
|
// create tooltip
|
||||||
|
layer.bindTooltip(description, {
|
||||||
|
permanent: true,
|
||||||
|
direction: 'bottom',
|
||||||
|
className: 'drawingTooltip',
|
||||||
|
});
|
||||||
|
} else if (result.dismiss === Swal.DismissReason.cancel) {
|
||||||
|
// Cancelled, do nothing
|
||||||
|
}
|
||||||
|
}).then((result) => {
|
||||||
|
// Get the selected color from the color picker
|
||||||
|
var selectedColor = $('#colorPicker').spectrum('get').toHexString();
|
||||||
|
layer.setStyle({ color: selectedColor }); // Set the color of the layer
|
||||||
|
|
||||||
|
|
||||||
|
// Send the drawing data to the server
|
||||||
|
saveDrawingToServer(layer, description, selectedColor, imageUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Function to update draw colors based on the selected color
|
||||||
|
window.DRAW_COLOR = '#3388ff';
|
||||||
|
function updateDrawColors (color) {
|
||||||
|
DRAW_COLOR = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to send the drawing data to the server
|
||||||
|
function saveDrawingToServer (layer, description, color, imageUrl) {
|
||||||
|
// get session search param from current url
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
const session = urlParams.get('session');
|
||||||
|
$.ajax({
|
||||||
|
url: '/drawings/' + session,
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: JSON.stringify({
|
||||||
|
data: layer.toGeoJSON(),
|
||||||
|
description: description,
|
||||||
|
color: color,
|
||||||
|
imageUrl: imageUrl, // Include the imageUrl in the request
|
||||||
|
}),
|
||||||
|
success: function (response) {
|
||||||
|
// Get the ID of the saved drawing from the server response
|
||||||
|
const drawingId = response.id;
|
||||||
|
|
||||||
|
// Set the ID as a property of the layer
|
||||||
|
layer.drawingId = drawingId;
|
||||||
|
|
||||||
|
console.log('Drawing saved successfully.');
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Error saving drawing:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Event listener for drawing deletion
|
||||||
|
map.on('pm:remove', function (event) {
|
||||||
|
console.log('Drawing deleted. ');
|
||||||
|
var layer = event.layer;
|
||||||
|
deleteDrawingOnServer(layer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to delete a drawing from the server
|
||||||
|
function deleteDrawingOnServer (layer) {
|
||||||
|
console.log('Deleting drawing...');
|
||||||
|
console.log(layer.drawingId);
|
||||||
|
const drawingId = layer.drawingId;
|
||||||
|
if (drawingId) {
|
||||||
|
$.ajax({
|
||||||
|
url: `/drawings/${drawingId}`,
|
||||||
|
type: 'DELETE',
|
||||||
|
success: function () {
|
||||||
|
console.log('Drawing deleted successfully.');
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Error deleting drawing:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to export the drawings
|
||||||
|
function exportDrawings () {
|
||||||
|
window.location.href = '/export';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to import the drawings
|
||||||
|
function importDrawings () {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.json';
|
||||||
|
input.onchange = function (event) {
|
||||||
|
var file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
var fileData = e.target.result;
|
||||||
|
importDrawingsFromFile(fileData);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Function to import the drawings from a file
|
||||||
|
function importDrawingsFromFile (fileData) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/import',
|
||||||
|
type: 'POST',
|
||||||
|
contentType: 'application/json',
|
||||||
|
data: fileData,
|
||||||
|
success: function () {
|
||||||
|
console.log('Drawings imported successfully.');
|
||||||
|
// Reload the page to display the imported drawings
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Error importing drawings:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Load drawings from the server
|
||||||
|
loadDrawingsFromServer();
|
||||||
|
|
||||||
|
function loadDrawingsFromServer () {
|
||||||
|
// get session search param from current url
|
||||||
|
const queryString = window.location.search;
|
||||||
|
const urlParams = new URLSearchParams(queryString);
|
||||||
|
const session = urlParams.get('session');
|
||||||
|
$.ajax({
|
||||||
|
url: `/drawings/${session}`,
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (drawings) {
|
||||||
|
drawings.forEach(function (drawing) {
|
||||||
|
var layer;
|
||||||
|
if (drawing.data.type === 'Point') {
|
||||||
|
var icon = L.icon({
|
||||||
|
iconUrl: drawing.imageUrl,
|
||||||
|
iconSize: [25, 41], // Adjust the size of the icon if needed
|
||||||
|
});
|
||||||
|
layer = L.geoJSON(drawing.data, {
|
||||||
|
pointToLayer: function (geoJsonPoint, latlng) {
|
||||||
|
return L.marker(latlng, {
|
||||||
|
icon: icon
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
layer = L.geoJSON(drawing.data, {
|
||||||
|
style: {
|
||||||
|
color: drawing.color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
layer.eachLayer(function (l) {
|
||||||
|
var popup = L.popup({
|
||||||
|
maxWidth: 200,
|
||||||
|
className: 'drawingPopup',
|
||||||
|
});
|
||||||
|
popup.setContent(drawing.description);
|
||||||
|
l.bindPopup(popup);
|
||||||
|
|
||||||
|
// create tooltip
|
||||||
|
l.bindTooltip(drawing.description, {
|
||||||
|
permanent: true,
|
||||||
|
direction: 'bottom',
|
||||||
|
className: 'drawingTooltip',
|
||||||
|
});
|
||||||
|
|
||||||
|
l.drawingId = drawing.id; // Set the drawing ID as a property of the layer
|
||||||
|
});
|
||||||
|
|
||||||
|
layer.addTo(map);
|
||||||
|
});
|
||||||
|
console.log('Drawings loaded successfully.');
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Error loading drawings:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
checkIt();
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkIt () {
|
||||||
|
$.ajax({
|
||||||
|
url: '/loginStatus',
|
||||||
|
type: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (response) {
|
||||||
|
if (response.isLoggedIn) {
|
||||||
|
map.pm.addControls({
|
||||||
|
position: 'topleft',
|
||||||
|
drawCircle: false,
|
||||||
|
drawRectangle: true,
|
||||||
|
drawCircleMarker: false,
|
||||||
|
tooltips: true,
|
||||||
|
drawPolyline: true,
|
||||||
|
drawPolygon: true,
|
||||||
|
drawText: false,
|
||||||
|
});
|
||||||
|
// Add color picker functionality
|
||||||
|
var colorPicker = $('<input type="text" id="colorPicker" />');
|
||||||
|
var logout = $('<div class="button-container" title="Logout"><a class="leaflet-buttons-control-button" role="button" tabindex="0" href="/logout"><div class="control-icon leaflet-pm-icon-logout"></div></a></div>');
|
||||||
|
|
||||||
|
// Create the import button
|
||||||
|
var importButton = $('<div class="button-container" title="Import Drawings"><a class="leaflet-buttons-control-button" role="button" tabindex="0" id="importButton"><div class="control-icon leaflet-pm-icon-import"></div></a></div>');
|
||||||
|
$('.leaflet-pm-toolbar:last').append(importButton);
|
||||||
|
|
||||||
|
// Create the export button
|
||||||
|
var exportButton = $('<div class="button-container" title="Export Drawings"><a class="leaflet-buttons-control-button" role="button" tabindex="0" id="exportButton"><div class="control-icon leaflet-pm-icon-export"></div></a></div>');
|
||||||
|
$('.leaflet-pm-toolbar:last').append(exportButton);
|
||||||
|
$('.leaflet-pm-toolbar:last').append(logout);
|
||||||
|
$('.leaflet-pm-toolbar:first').prepend(colorPicker);
|
||||||
|
$('#colorPicker').spectrum({
|
||||||
|
color: '#3388ff', // Initial color
|
||||||
|
preferredFormat: 'hex',
|
||||||
|
showInput: true,
|
||||||
|
change: function (color) {
|
||||||
|
var selectedColor = color.toHexString();
|
||||||
|
updateDrawColors(selectedColor);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener for export button click
|
||||||
|
$('#exportButton').on('click', function () {
|
||||||
|
exportDrawings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Event listener for import button click
|
||||||
|
$('#importButton').on('click', function () {
|
||||||
|
importDrawings();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
console.error('Error checking login status:', error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
10
src/js/leaflet.draw.js
Normal file
628
src/js/mapUtils.js
Normal file
@@ -0,0 +1,628 @@
|
|||||||
|
// leaflet.latlng-graticule.js
|
||||||
|
// https://github.com/cloudybay/leaflet.latlng-graticule
|
||||||
|
|
||||||
|
/* eslint-disable indent,semi */
|
||||||
|
/**
|
||||||
|
* Create a Canvas as ImageOverlay to draw the Lat/Lon Graticule,
|
||||||
|
* and show the axis tick label on the edge of the map.
|
||||||
|
* Author: lanwei@cloudybay.com.tw
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (window, document, undefined) {
|
||||||
|
|
||||||
|
L.LatLngGraticule = L.Layer.extend({
|
||||||
|
includes: (L.Evented.prototype || L.Mixin.Events),
|
||||||
|
options: {
|
||||||
|
showLabel: true,
|
||||||
|
opacity: 1,
|
||||||
|
weight: 0.8,
|
||||||
|
color: '#aaa',
|
||||||
|
font: '12px Verdana',
|
||||||
|
dashArray: [0, 0],
|
||||||
|
lngLineCurved: 0,
|
||||||
|
latLineCurved: 0,
|
||||||
|
zoomInterval: [
|
||||||
|
{ start: 2, end: 2, interval: 40 },
|
||||||
|
{ start: 3, end: 3, interval: 20 },
|
||||||
|
{ start: 4, end: 4, interval: 10 },
|
||||||
|
{ start: 5, end: 7, interval: 5 },
|
||||||
|
{ start: 8, end: 20, interval: 1 }
|
||||||
|
],
|
||||||
|
sides: ['N', 'S', 'E', 'W']
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function (options) {
|
||||||
|
L.setOptions(this, options);
|
||||||
|
|
||||||
|
var defaultFontName = 'Verdana';
|
||||||
|
var _ff = this.options.font.split(' ');
|
||||||
|
if (_ff.length < 2) {
|
||||||
|
this.options.font += ' ' + defaultFontName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.options.fontColor) {
|
||||||
|
this.options.fontColor = this.options.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.zoomInterval) {
|
||||||
|
if (this.options.zoomInterval.latitude) {
|
||||||
|
this.options.latInterval = this.options.zoomInterval.latitude;
|
||||||
|
if (!this.options.zoomInterval.longitude) {
|
||||||
|
this.options.lngInterval = this.options.zoomInterval.latitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.options.zoomInterval.longitude) {
|
||||||
|
this.options.lngInterval = this.options.zoomInterval.longitude;
|
||||||
|
if (!this.options.zoomInterval.latitude) {
|
||||||
|
this.options.latInterval = this.options.zoomInterval.longitude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.options.latInterval) {
|
||||||
|
this.options.latInterval = this.options.zoomInterval;
|
||||||
|
}
|
||||||
|
if (!this.options.lngInterval) {
|
||||||
|
this.options.lngInterval = this.options.zoomInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._map = map;
|
||||||
|
|
||||||
|
if (!this._canvas) {
|
||||||
|
this._initCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
map._panes.overlayPane.appendChild(this._canvas);
|
||||||
|
|
||||||
|
map.on('viewreset', this._reset, this);
|
||||||
|
map.on('move', this._reset, this);
|
||||||
|
map.on('moveend', this._reset, this);
|
||||||
|
|
||||||
|
// if (map.options.zoomAnimation && L.Browser.any3d) {
|
||||||
|
// map.on('zoomanim', this._animateZoom, this);
|
||||||
|
// }
|
||||||
|
|
||||||
|
this._reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
L.DomUtil.remove(this._canvas);
|
||||||
|
|
||||||
|
map.off('viewreset', this._reset, this);
|
||||||
|
map.off('move', this._reset, this);
|
||||||
|
map.off('moveend', this._reset, this);
|
||||||
|
|
||||||
|
if (map.options.zoomAnimation) {
|
||||||
|
map.off('zoomanim', this._animateZoom, this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addTo: function (map) {
|
||||||
|
map.addLayer(this);
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
setOpacity: function (opacity) {
|
||||||
|
this.options.opacity = opacity;
|
||||||
|
this._updateOpacity();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
bringToFront: function () {
|
||||||
|
if (this._canvas) {
|
||||||
|
//this._map._panes.overlayPane.appendChild(this._canvas);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
bringToBack: function () {
|
||||||
|
var pane = this._map._panes.overlayPane;
|
||||||
|
if (this._canvas) {
|
||||||
|
//pane.insertBefore(this._canvas, pane.firstChild);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttribution: function () {
|
||||||
|
return this.options.attribution;
|
||||||
|
},
|
||||||
|
|
||||||
|
_initCanvas: function () {
|
||||||
|
|
||||||
|
this._canvas = L.DomUtil.create('canvas', '');
|
||||||
|
|
||||||
|
if (this._map.options.zoomAnimation && L.Browser.any3d) {
|
||||||
|
L.DomUtil.addClass(this._canvas, 'leaflet-zoom-animated');
|
||||||
|
} else {
|
||||||
|
L.DomUtil.addClass(this._canvas, 'leaflet-zoom-hide');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._updateOpacity();
|
||||||
|
|
||||||
|
|
||||||
|
L.extend(this._canvas, {
|
||||||
|
onselectstart: L.Util.falseFn,
|
||||||
|
onmousemove: L.Util.falseFn,
|
||||||
|
onload: L.bind(this._onCanvasLoad, this)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_animateZoom: function (e) {
|
||||||
|
var map = this._map,
|
||||||
|
canvas = this._canvas,
|
||||||
|
scale = map.getZoomScale(e.zoom),
|
||||||
|
nw = map.containerPointToLatLng([0, 0]),
|
||||||
|
se = map.containerPointToLatLng([canvas.width, canvas.height]),
|
||||||
|
topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center),
|
||||||
|
size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft),
|
||||||
|
origin = topLeft._add(size._multiplyBy((1 / 2) * (1 - 1 / scale)));
|
||||||
|
|
||||||
|
L.DomUtil.setTransform(canvas, origin, scale);
|
||||||
|
},
|
||||||
|
|
||||||
|
_reset: function () {
|
||||||
|
var canvas = this._canvas,
|
||||||
|
size = this._map.getSize(),
|
||||||
|
lt = this._map.containerPointToLayerPoint([0, 0]);
|
||||||
|
|
||||||
|
L.DomUtil.setPosition(canvas, lt);
|
||||||
|
|
||||||
|
canvas.width = size.x;
|
||||||
|
canvas.height = size.y;
|
||||||
|
canvas.style.width = size.x + 'px';
|
||||||
|
canvas.style.height = size.y + 'px';
|
||||||
|
|
||||||
|
this.__calcInterval();
|
||||||
|
|
||||||
|
this.__draw(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onCanvasLoad: function () {
|
||||||
|
this.fire('load');
|
||||||
|
},
|
||||||
|
|
||||||
|
_updateOpacity: function () {
|
||||||
|
L.DomUtil.setOpacity(this._canvas, this.options.opacity);
|
||||||
|
},
|
||||||
|
|
||||||
|
__format_coord: function (value, labelMeters, interval) {
|
||||||
|
|
||||||
|
// console.debug('__format_coord', {
|
||||||
|
// value,
|
||||||
|
// labelMeters,
|
||||||
|
// interval
|
||||||
|
// })
|
||||||
|
|
||||||
|
var padLength = 3;
|
||||||
|
switch (interval) {
|
||||||
|
case 10000:
|
||||||
|
padLength = 1;
|
||||||
|
labelMeters = labelMeters / 10000;
|
||||||
|
break;
|
||||||
|
case 1000:
|
||||||
|
padLength = 2;
|
||||||
|
labelMeters = labelMeters / 1000;
|
||||||
|
break;
|
||||||
|
case 100:
|
||||||
|
padLength = 3;
|
||||||
|
labelMeters = labelMeters / 100;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${labelMeters}`.padStart(padLength, '0')
|
||||||
|
},
|
||||||
|
|
||||||
|
__calcInterval: function () {
|
||||||
|
var zoom = this._map.getZoom();
|
||||||
|
if (this._currZoom != zoom) {
|
||||||
|
this._currLngInterval = 0;
|
||||||
|
this._currLatInterval = 0;
|
||||||
|
this._currZoom = zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
var interv;
|
||||||
|
|
||||||
|
if (!this._currLngInterval) {
|
||||||
|
try {
|
||||||
|
for (var idx in this.options.lngInterval) {
|
||||||
|
var dict = this.options.lngInterval[idx];
|
||||||
|
if (dict.start <= zoom) {
|
||||||
|
if (dict.end && dict.end >= zoom) {
|
||||||
|
this._currLngInterval = dict.interval;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this._currLngInterval = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._currLatInterval) {
|
||||||
|
try {
|
||||||
|
for (var idx in this.options.latInterval) {
|
||||||
|
var dict = this.options.latInterval[idx];
|
||||||
|
if (dict.start <= zoom) {
|
||||||
|
if (dict.end && dict.end >= zoom) {
|
||||||
|
this._currLatInterval = dict.interval;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
this._currLatInterval = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
__draw: function (label) {
|
||||||
|
function _parse_px_to_int (txt) {
|
||||||
|
if (txt.length > 2) {
|
||||||
|
if (txt.charAt(txt.length - 2) == 'p') {
|
||||||
|
txt = txt.substr(0, txt.length - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return parseInt(txt, 10);
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
var self = this,
|
||||||
|
canvas = this._canvas,
|
||||||
|
map = this._map,
|
||||||
|
curvedLon = this.options.lngLineCurved,
|
||||||
|
curvedLat = this.options.latLineCurved;
|
||||||
|
|
||||||
|
if (L.Browser.canvas && map) {
|
||||||
|
if (!this._currLngInterval || !this._currLatInterval) {
|
||||||
|
this.__calcInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
var latInterval = this._currLatInterval,
|
||||||
|
lngInterval = this._currLngInterval;
|
||||||
|
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.lineWidth = this.options.weight;
|
||||||
|
ctx.strokeStyle = this.options.color;
|
||||||
|
ctx.fillStyle = this.options.fontColor;
|
||||||
|
ctx.setLineDash(this.options.dashArray);
|
||||||
|
|
||||||
|
if (this.options.font) {
|
||||||
|
ctx.font = this.options.font;
|
||||||
|
}
|
||||||
|
var txtWidth = ctx.measureText('0').width;
|
||||||
|
var txtHeight = 12;
|
||||||
|
try {
|
||||||
|
var _font_size = ctx.font.trim().split(' ')[0];
|
||||||
|
txtHeight = _parse_px_to_int(_font_size);
|
||||||
|
}
|
||||||
|
catch (e) { }
|
||||||
|
|
||||||
|
var ww = canvas.width,
|
||||||
|
hh = canvas.height;
|
||||||
|
|
||||||
|
var lt = map.containerPointToLatLng(L.point(0, 0));
|
||||||
|
var rt = map.containerPointToLatLng(L.point(ww, 0));
|
||||||
|
var rb = map.containerPointToLatLng(L.point(ww, hh));
|
||||||
|
|
||||||
|
var _lat_b = rb.lat,
|
||||||
|
_lat_t = lt.lat;
|
||||||
|
var _lon_l = lt.lng,
|
||||||
|
_lon_r = rt.lng;
|
||||||
|
|
||||||
|
var _point_per_lat = (_lat_t - _lat_b) / (hh * 0.2);
|
||||||
|
if (isNaN(_point_per_lat)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_point_per_lat < 1) { _point_per_lat = 1; }
|
||||||
|
if (_lat_b < -90) {
|
||||||
|
_lat_b = -90;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_lat_b = parseInt(_lat_b - _point_per_lat, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lat_t > 90) {
|
||||||
|
_lat_t = 90;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_lat_t = parseInt(_lat_t + _point_per_lat, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
var _point_per_lon = (_lon_r - _lon_l) / (ww * 0.2);
|
||||||
|
if (_point_per_lon < 1) { _point_per_lon = 1; }
|
||||||
|
if (_lon_l > 0 && _lon_r < 0) {
|
||||||
|
_lon_r += 360;
|
||||||
|
}
|
||||||
|
_lon_r = parseInt(_lon_r + _point_per_lon, 10);
|
||||||
|
_lon_l = parseInt(_lon_l - _point_per_lon, 10);
|
||||||
|
|
||||||
|
var ll, latstr, lngstr, _lon_delta = 0.5;
|
||||||
|
function __draw_lat_line (self, lat_tick, label, interval) {
|
||||||
|
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, _lon_l));
|
||||||
|
latstr = self.__format_coord(lat_tick, label, interval);
|
||||||
|
txtWidth = ctx.measureText(latstr).width;
|
||||||
|
var spacer = self.options.showLabel && label ? txtWidth + 10 : 0;
|
||||||
|
|
||||||
|
if (curvedLat) {
|
||||||
|
if (typeof (curvedLat) == 'number') {
|
||||||
|
_lon_delta = curvedLat;
|
||||||
|
}
|
||||||
|
|
||||||
|
var __lon_left = _lon_l, __lon_right = _lon_r;
|
||||||
|
if (ll.x > 0) {
|
||||||
|
var __lon_left = map.containerPointToLatLng(L.point(0, ll.y));
|
||||||
|
__lon_left = __lon_left.lng - _point_per_lon;
|
||||||
|
ll.x = 0;
|
||||||
|
}
|
||||||
|
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
|
||||||
|
if (rr.x < ww) {
|
||||||
|
__lon_right = map.containerPointToLatLng(L.point(ww, rr.y));
|
||||||
|
__lon_right = __lon_right.lng + _point_per_lon;
|
||||||
|
if (__lon_left > 0 && __lon_right < 0) {
|
||||||
|
__lon_right += 360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(ll.x + spacer, ll.y);
|
||||||
|
var _prev_p = null;
|
||||||
|
for (var j = __lon_left; j <= __lon_right; j += _lon_delta) {
|
||||||
|
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, j));
|
||||||
|
ctx.lineTo(rr.x - spacer, rr.y);
|
||||||
|
|
||||||
|
if (self.options.showLabel && label && _prev_p != null) {
|
||||||
|
if (_prev_p.x < 0 && rr.x >= 0) {
|
||||||
|
var _s = (rr.x - 0) / (rr.x - _prev_p.x);
|
||||||
|
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
|
||||||
|
ctx.fillText(latstr, 0, _y + (txtHeight / 2));
|
||||||
|
}
|
||||||
|
else if (_prev_p.x <= (ww - txtWidth) && rr.x > (ww - txtWidth)) {
|
||||||
|
var _s = (rr.x - ww) / (rr.x - _prev_p.x);
|
||||||
|
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
|
||||||
|
ctx.fillText(latstr, ww - txtWidth, _y + (txtHeight / 2) - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_prev_p = { x: rr.x, y: rr.y, lon: j, lat: i };
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var __lon_right = _lon_r;
|
||||||
|
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
|
||||||
|
if (curvedLon) {
|
||||||
|
__lon_right = map.containerPointToLatLng(L.point(0, rr.y));
|
||||||
|
__lon_right = __lon_right.lng;
|
||||||
|
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
|
||||||
|
|
||||||
|
var __lon_left = map.containerPointToLatLng(L.point(ww, rr.y));
|
||||||
|
__lon_left = __lon_left.lng;
|
||||||
|
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_left));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(1 + spacer, ll.y);
|
||||||
|
ctx.lineTo(rr.x - 1 - spacer, rr.y);
|
||||||
|
ctx.stroke();
|
||||||
|
if (self.options.showLabel && label) {
|
||||||
|
var _yy = ll.y + (txtHeight / 2) - 2;
|
||||||
|
ctx.fillText(latstr, 0, _yy);
|
||||||
|
ctx.fillText(latstr, ww - txtWidth, _yy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (latInterval > 0) {
|
||||||
|
// this is hacky but serves a purpose of allowing interval options to be specified in meters (EPSG:3857).
|
||||||
|
// we want this to specify 10km, 1km, and 100m grid squares, so we need to convert the interval to degrees so that this tool will work, as it only takes degrees.
|
||||||
|
var latIntervalMeters = +latInterval;
|
||||||
|
var latIntervalLabel = 0;
|
||||||
|
var tgtPoint = proj4('EPSG:3857', 'EPSG:4326', [0, latInterval])
|
||||||
|
var latIntervalDegrees = turf.distance([0, 0], tgtPoint, { units: 'degrees' })
|
||||||
|
|
||||||
|
// console.debug('Lat grid calculations', {
|
||||||
|
// latIntervalMeters,
|
||||||
|
// latIntervalDegrees,
|
||||||
|
// tgtPoint,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// draw 0 lat line
|
||||||
|
__draw_lat_line(this, 0, 0);
|
||||||
|
|
||||||
|
// draw positive lat lines
|
||||||
|
for (var i = 0; i <= _lat_t; i += latIntervalDegrees, latIntervalLabel += latIntervalMeters) {
|
||||||
|
if (i >= _lat_b) {
|
||||||
|
__draw_lat_line(this, i, latIntervalLabel, latIntervalMeters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw negative lat lines
|
||||||
|
latIntervalLabel = 0;
|
||||||
|
for (var i = 0; i >= _lat_b; i -= latIntervalDegrees, latIntervalLabel -= latIntervalMeters) {
|
||||||
|
if (i <= _lat_t) {
|
||||||
|
__draw_lat_line(this, i, latIntervalLabel, latIntervalMeters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __draw_lon_line (self, lon_tick, label, interval) {
|
||||||
|
lngstr = self.__format_coord(lon_tick, label, interval);
|
||||||
|
txtWidth = ctx.measureText(lngstr).width;
|
||||||
|
var bb = self._latLngToCanvasPoint(L.latLng(_lat_b, lon_tick));
|
||||||
|
var spacer = self.options.showLabel && label ? txtHeight + 5 : 0;
|
||||||
|
|
||||||
|
if (curvedLon) {
|
||||||
|
if (typeof (curvedLon) == 'number') {
|
||||||
|
_lat_delta = curvedLon;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(bb.x, 5 + spacer);
|
||||||
|
var _prev_p = null;
|
||||||
|
for (var j = _lat_b; j < _lat_t; j += _lat_delta) {
|
||||||
|
var tt = self._latLngToCanvasPoint(L.latLng(j, lon_tick));
|
||||||
|
ctx.lineTo(tt.x, tt.y - spacer);
|
||||||
|
|
||||||
|
if (self.options.showLabel && label && _prev_p != null) {
|
||||||
|
if (_prev_p.y > 8 && tt.y <= 8) {
|
||||||
|
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
|
||||||
|
}
|
||||||
|
else if (_prev_p.y >= hh && tt.y < hh) {
|
||||||
|
ctx.fillText(lngstr, tt.x - (txtWidth / 2), hh - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_prev_p = { x: tt.x, y: tt.y, lon: lon_tick, lat: j };
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var __lat_top = _lat_t;
|
||||||
|
var tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
|
||||||
|
if (curvedLat) {
|
||||||
|
__lat_top = map.containerPointToLatLng(L.point(tt.x, 0));
|
||||||
|
__lat_top = __lat_top.lat;
|
||||||
|
if (__lat_top > 90) { __lat_top = 90; }
|
||||||
|
tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
|
||||||
|
|
||||||
|
var __lat_bottom = map.containerPointToLatLng(L.point(bb.x, hh));
|
||||||
|
__lat_bottom = __lat_bottom.lat;
|
||||||
|
if (__lat_bottom < -90) { __lat_bottom = -90; }
|
||||||
|
bb = self._latLngToCanvasPoint(L.latLng(__lat_bottom, lon_tick));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tt.x, 5 + spacer);
|
||||||
|
ctx.lineTo(bb.x, hh - 1 - spacer);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
if (self.options.showLabel && label) {
|
||||||
|
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
|
||||||
|
ctx.fillText(lngstr, bb.x - (txtWidth / 2), hh - 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lngInterval > 0) {
|
||||||
|
// this is hacky but serves a purpose of allowing interval options to be specified in meters (EPSG:3857).
|
||||||
|
// we want this to specify 10km, 1km, and 100m grid squares, so we need to convert the interval to degrees so that this tool will work, as it only takes degrees.
|
||||||
|
var lngIntervalMeters = +lngInterval;
|
||||||
|
var lngIntervalLabel = 0;
|
||||||
|
var tgtPoint = proj4('EPSG:3857', 'EPSG:4326', [lngInterval, 0])
|
||||||
|
var lngIntervalDegrees = turf.distance([0, 0], tgtPoint, { units: 'degrees' })
|
||||||
|
|
||||||
|
// console.debug('lng grid calculations', {
|
||||||
|
// lngIntervalMeters,
|
||||||
|
// lngIntervalDegrees,
|
||||||
|
// tgtPoint,
|
||||||
|
// })
|
||||||
|
|
||||||
|
// draw positive lng lines
|
||||||
|
for (var i = 0; i <= _lon_r; i += lngIntervalDegrees, lngIntervalLabel += lngIntervalMeters) {
|
||||||
|
if (i >= _lon_l) {
|
||||||
|
__draw_lon_line(this, i, lngIntervalLabel, latIntervalMeters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_latLngToCanvasPoint: function (latlng) {
|
||||||
|
var map = this._map;
|
||||||
|
var projectedPoint = map.latLngToLayerPoint(L.latLng(latlng));
|
||||||
|
|
||||||
|
// console.debug('_latLngToCanvasPoint latlng', latlng);
|
||||||
|
// console.debug('_latLngToCanvasPoint projectedPoint', projectedPoint);
|
||||||
|
var finalPoint = L.point(projectedPoint).add(map._getMapPanePos());
|
||||||
|
// console.debug('_latLngToCanvasPoint finalPoint', finalPoint);
|
||||||
|
return finalPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.latlngGraticule = function (options) {
|
||||||
|
return new L.LatLngGraticule(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
}(this, document));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mouse MGRS coordinates on map
|
||||||
|
*
|
||||||
|
* Author: jetelain
|
||||||
|
*/
|
||||||
|
L.Control.GridMousePosition = L.Control.extend({
|
||||||
|
options: {
|
||||||
|
position: 'topright',
|
||||||
|
precision: 4
|
||||||
|
},
|
||||||
|
|
||||||
|
onAdd: function (map) {
|
||||||
|
this._container = L.DomUtil.create('div', 'leaflet-grid-mouseposition');
|
||||||
|
L.DomEvent.disableClickPropagation(this._container);
|
||||||
|
map.on('mousemove', this._onMouseMove, this);
|
||||||
|
var placeHolder = '0'.repeat(this.options.precision);
|
||||||
|
this._container.innerHTML = `Grid: ${placeHolder} - ${placeHolder}`;
|
||||||
|
return this._container;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function (map) {
|
||||||
|
map.off('mousemove', this._onMouseMove)
|
||||||
|
},
|
||||||
|
|
||||||
|
_onMouseMove: function (e) {
|
||||||
|
var coord_4326 = e.latlng;
|
||||||
|
var mgrsStr = latlngToMGRS(coord_4326.lat, coord_4326.lng, this.options.precision);
|
||||||
|
var mousePositionXY = mgrsStr[0] + " " + mgrsStr[1];
|
||||||
|
// console.debug(mgrsStr);
|
||||||
|
// console.debug(mousePositionXY)
|
||||||
|
this._container.innerHTML = `Grid: ${mousePositionXY}`;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.gridMousePosition = function (options) {
|
||||||
|
return new L.Control.GridMousePosition(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
function latLngTo3857 (lat, lng) {
|
||||||
|
return proj4("EPSG:4326", "EPSG:3857", [lng, lat]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function latlngToMGRS (lat, lng, precision = 4) {
|
||||||
|
// console.debug("latlngToMGRS", lat, lng, precision);
|
||||||
|
var coord_3857 = latLngTo3857(lat, lng);
|
||||||
|
// console.debug("coord_3857", coord_3857)
|
||||||
|
var mercatorStr = [
|
||||||
|
Math.abs(coord_3857[0] / 10).toFixed(0).toString(),
|
||||||
|
Math.abs(coord_3857[1] / 10).toFixed(0).toString()
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
mercatorStr = mercatorStr.map((coord) => {
|
||||||
|
return `${coord}`.padStart(precision, '0')
|
||||||
|
});
|
||||||
|
|
||||||
|
// * add negative sign if needed
|
||||||
|
coord_3857[0] < 0
|
||||||
|
? (mercatorStr[0] = "-" + mercatorStr[0])
|
||||||
|
: (mercatorStr[0] = mercatorStr[0]);
|
||||||
|
coord_3857[1] < 0
|
||||||
|
? (mercatorStr[1] = "-" + mercatorStr[1])
|
||||||
|
: (mercatorStr[1] = mercatorStr[1]);
|
||||||
|
|
||||||
|
return mercatorStr;
|
||||||
|
}
|
||||||
|
|
||||||
27
src/js/sessionMgmt.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
// joinSession is initiated from form submission on the main page
|
||||||
|
const joinSession = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Get all the form data
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = Object.fromEntries(formData);
|
||||||
|
|
||||||
|
// Send the form data to RabbitMQ
|
||||||
|
fetch('/joinSession', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
// If the response is successful, redirect to the session page
|
||||||
|
if (res.success) {
|
||||||
|
window.location.href = `/session/${res.sessionId}`;
|
||||||
|
} else {
|
||||||
|
// If the response is not successful, display an error message
|
||||||
|
alert(res.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
53
src/login.html
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<link href="index.css" rel="stylesheet">
|
||||||
|
<script defer src="index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
<h1 class="text-3xl text-center font-bold mt-8">Login</h1>
|
||||||
|
<form id="loginForm" class="max-w-md mx-auto mt-8 p-4 bg-white rounded-lg shadow-md">
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="username" class="block font-bold">Username</label>
|
||||||
|
<input type="text" id="username" name="username" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="password" class="block font-bold">Password</label>
|
||||||
|
<input type="password" id="password" name="password" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="px-4 py-2 bg-blue-500 text-white font-bold rounded-md">Login</button>
|
||||||
|
</div>
|
||||||
|
<div id="message" class="form-group"></div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('#loginForm').submit(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
var username = $('#username').val();
|
||||||
|
var password = $('#password').val();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: '/login',
|
||||||
|
type: 'POST',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({ username: username, password: password }),
|
||||||
|
contentType: 'application/json',
|
||||||
|
success: function (data) {
|
||||||
|
$('#message').html('<div class="success-message text-green-500">Login successful.</div>');
|
||||||
|
// Redirect to the desired page after successful login
|
||||||
|
window.location.href = '/';
|
||||||
|
},
|
||||||
|
error: function (xhr, status, error) {
|
||||||
|
$('#message').html('<div class="error-message text-red-500">Invalid credentials.</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
src/logout.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Login</title>
|
||||||
|
<link href="index.css" rel="stylesheet">
|
||||||
|
<script defer src="index.js"></script>
|
||||||
|
<meta http-equiv="refresh" content="1;url=/">
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100">
|
||||||
|
<h1 class="text-3xl text-center font-bold mt-8">Ausgeloggt</h1>
|
||||||
|
<p class="text-1xl text-center font-bold mt-8">Du hast dich erfolgreich ausgelogt.</p>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$.ajax({
|
||||||
|
url: '/logout',
|
||||||
|
type: 'POST',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||