This commit is contained in:
Robert Janetzko 2022-05-01 10:29:39 +00:00
parent 001f398137
commit 9f38f9b17d
17 changed files with 530 additions and 79 deletions

3
.gitignore vendored
View file

@ -5,4 +5,5 @@ legendsbrowser
*.png
/*.json
.DS_Store
bin
bin
/inputs/*

View file

@ -5,6 +5,36 @@
"df_world|historical_events|historical_event+KnowledgeDiscovered|knowledge": true
},
"AdditionalFields": {
"DfWorld": [
{
"Name": "FilePath",
"Type": "string"
},
{
"Name": "PlusFilePath",
"Type": "string"
},
{
"Name": "MapReady",
"Type": "bool"
},
{
"Name": "MapData",
"Type": "[]byte"
},
{
"Name": "Width",
"Type": "int"
},
{
"Name": "Height",
"Type": "int"
},
{
"Name": "EndYear",
"Type": "int"
}
],
"Structure": [
{
"Name": "SiteId",

View file

@ -10,6 +10,7 @@ require (
github.com/pkg/profile v1.6.0
github.com/shirou/gopsutil v3.21.11+incompatible
golang.org/x/exp v0.0.0-20220428152302-39d4317da171
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9
)
require (

View file

@ -29,9 +29,13 @@ github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPR
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 h1:TfdoLivD44QwvssI9Sv1xwa5DcL5XQr4au4sZ2F2NV4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9 h1:LRtI4W37N+KFebI/qV0OFiLUv4GLOWeEW5hn/KEJvxE=
golang.org/x/image v0.0.0-20220413100746-70e8d0d3baa9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View file

@ -3,10 +3,9 @@ package main
import (
"embed"
"flag"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"os"
"runtime"
"github.com/pkg/profile"
@ -26,6 +25,8 @@ func main() {
templates.DebugTemplates = *d
var world *model.DfWorld
if len(*f) > 0 {
if *p {
defer profile.Start(profile.ProfilePath(".")).Stop()
@ -36,14 +37,15 @@ func main() {
w, err := model.Parse(*f, nil)
if err != nil {
fmt.Println(err)
os.Exit(1)
log.Fatal(err)
}
runtime.GC()
server.StartServer(w, static)
world = w
}
} else {
server.StartServer(nil, static)
err := server.StartServer(world, static)
if err != nil {
log.Fatal(err)
}
}

View file

@ -4,6 +4,7 @@ import (
"fmt"
"html/template"
"regexp"
"strconv"
"strings"
)
@ -23,6 +24,21 @@ var LinkMusicalForm = func(w *DfWorld, id int) template.HTML { return template.H
var LinkPoeticForm = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).poeticForm(id)) }
var LinkWrittenContent = func(w *DfWorld, id int) template.HTML { return template.HTML((&Context{World: w}).writtenContent(id)) }
var AddMapSite = func(w *DfWorld, id int) template.HTML {
if site, ok := w.Sites[id]; ok {
coords := strings.Split(site.Rectangle, ":")
c1 := strings.Split(coords[0], ",")
x1, _ := strconv.ParseFloat(c1[0], 32)
y1, _ := strconv.ParseFloat(c1[1], 32)
c2 := strings.Split(coords[1], ",")
x2, _ := strconv.ParseFloat(c2[0], 32)
y2, _ := strconv.ParseFloat(c2[1], 32)
return template.HTML(fmt.Sprintf(`<script>addSite("%s", %f, %f, %f, %f, "#FF0", "")</script>`, site.Name(), x1/16.0, y1/16.0-1, x2/16.0, y2/16.0-1))
} else {
return ""
}
}
func andList(list []string) string {
if len(list) > 1 {
return strings.Join(list[:len(list)-1], ", ") + " and " + list[len(list)-1]

94
backend/model/map.go Normal file
View file

@ -0,0 +1,94 @@
package model
import (
"bytes"
"fmt"
"image"
"image/png"
_ "image/png"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
_ "golang.org/x/image/bmp"
)
func (w *DfWorld) LoadMap() {
w.LoadDimensions()
path := ""
files, err := filepath.Glob(strings.ReplaceAll(w.FilePath, "-legends.xml", "-world_map.*"))
if err == nil && len(files) > 0 {
path = files[len(files)-1]
}
files, err = filepath.Glob(strings.ReplaceAll(w.FilePath, "-legends.xml", "-detailed.*"))
if err == nil && len(files) > 0 {
path = files[len(files)-1]
}
if path == "" {
return
}
mapImage, err := os.Open(path)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("Found Map", path)
img, format, err := image.Decode(mapImage)
if err != nil {
fmt.Println(err)
return
}
fmt.Println("loaded img", format)
buf := new(bytes.Buffer)
err = png.Encode(buf, img)
if err != nil {
fmt.Println(err)
return
}
w.MapData = buf.Bytes()
w.MapReady = true
}
func (w *DfWorld) LoadDimensions() {
files, err := filepath.Glob(filepath.Join(filepath.Dir(w.FilePath), "*-world_gen_param.txt"))
if err != nil {
fmt.Println(err)
return
}
path := ""
for _, f := range files {
prefix := filepath.Base(f)[:len(filepath.Base(f))-len("world_gen_param.txt")]
if strings.HasPrefix(filepath.Base(w.FilePath), prefix) {
path = f
break
}
}
if path == "" {
return
}
fmt.Println("Found Worldgen", path)
content, err := ioutil.ReadFile(path)
if err != nil {
return
}
text := string(content)
fmt.Println(text)
r := regexp.MustCompile(`\[DIM:(\d+):(\d+)\]`)
result := r.FindAllStringSubmatch(text, 1)
if result == nil {
return
}
w.Width, _ = strconv.Atoi(result[0][2])
w.Height, _ = strconv.Atoi(result[0][1])
}

View file

@ -1561,6 +1561,13 @@ type DfWorld struct {
UndergroundRegions map[int]*UndergroundRegion `json:"undergroundRegions" legend:"both"` // underground_regions
WorldConstructions map[int]*WorldConstruction `json:"worldConstructions" legend:"both"` // world_constructions
WrittenContents map[int]*WrittenContent `json:"writtenContents" legend:"both"` // written_contents
EndYear int `json:"endYear" legend:"add"` // EndYear
FilePath string `json:"filePath" legend:"add"` // FilePath
Height int `json:"height" legend:"add"` // Height
MapData []byte `json:"mapData" legend:"add"` // MapData
MapReady bool `json:"mapReady" legend:"add"` // MapReady
PlusFilePath string `json:"plusFilePath" legend:"add"` // PlusFilePath
Width int `json:"width" legend:"add"` // Width
}
func NewDfWorld() *DfWorld {
@ -1582,6 +1589,9 @@ func NewDfWorld() *DfWorld {
UndergroundRegions: make(map[int]*UndergroundRegion),
WorldConstructions: make(map[int]*WorldConstruction),
WrittenContents: make(map[int]*WrittenContent),
EndYear: -1,
Height: -1,
Width: -1,
}
}
func (x *DfWorld) Name() string { return x.Name_ }

View file

@ -2,10 +2,8 @@ package model
import (
"bufio"
"encoding/json"
"encoding/xml"
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
@ -98,6 +96,7 @@ BaseLoop:
}
}
}
world.FilePath = file
bar.Finish()
@ -133,15 +132,18 @@ BaseLoop:
}
}
}
world.PlusFilePath = file
bar.Finish()
}
same, err := json.MarshalIndent(exportSameFields(), "", " ")
if err != nil {
return world, err
}
ioutil.WriteFile("same.json", same, 0644)
// same, err := json.MarshalIndent(exportSameFields(), "", " ")
// if err != nil {
// return world, err
// }
// ioutil.WriteFile("same.json", same, 0644)
world.LoadMap()
world.process()

66
backend/server/config.go Normal file
View file

@ -0,0 +1,66 @@
package server
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
type Config struct {
LastPath string
LastFile string
}
func LoadConfig() (*Config, error) {
path, err := configPath()
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(path)
if err != nil {
fmt.Println("OPEN", err)
if os.IsNotExist(err) {
fmt.Println("EX", err)
home, err := os.UserHomeDir()
if err != nil {
return nil, err
}
return &Config{LastPath: home}, nil
} else {
return nil, err
}
}
c := &Config{}
json.Unmarshal(data, c)
return c, nil
}
func (c *Config) Save() error {
path, err := configPath()
if err != nil {
return err
}
file, err := json.MarshalIndent(c, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(path, file, 0644)
}
func configPath() (string, error) {
path, err := os.UserHomeDir()
if err != nil {
return "", err
}
path = filepath.Join(path, ".legendsbrowser")
os.MkdirAll(path, os.ModePerm)
return filepath.Join(path, "config.json"), nil
}

View file

@ -63,7 +63,7 @@ func (h loadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Current: path,
}
if p.Current == "" {
p.Current = "."
p.Current = h.server.context.config.LastPath
}
var err error
p.Current, err = filepath.Abs(p.Current)
@ -74,6 +74,9 @@ func (h loadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if f, err := os.Stat(p.Current); err == nil {
if f.IsDir() {
h.server.context.config.LastPath = p.Current
h.server.context.config.Save()
p.List, err = ioutil.ReadDir(p.Current)
if err != nil {
httpError(w, err)

View file

@ -3,9 +3,7 @@ package server
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
"sort"
"strconv"
@ -16,6 +14,7 @@ import (
)
type DfServerContext struct {
config *Config
world *model.DfWorld
isLoading bool
progress *model.LoadProgress
@ -28,10 +27,16 @@ type DfServer struct {
context *DfServerContext
}
func StartServer(world *model.DfWorld, static embed.FS) {
func StartServer(world *model.DfWorld, static embed.FS) error {
config, err := LoadConfig()
if err != nil {
return err
}
srv := &DfServer{
router: mux.NewRouter().StrictSlash(true),
context: &DfServerContext{
config: config,
world: world,
isLoading: false,
progress: &model.LoadProgress{},
@ -99,6 +104,12 @@ func StartServer(world *model.DfWorld, static embed.FS) {
}
})
srv.router.HandleFunc("/map", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/png")
w.WriteHeader(http.StatusOK)
w.Write(srv.loader.server.context.world.MapData)
})
srv.router.PathPrefix("/search").Handler(searchHandler{server: srv})
srv.router.PathPrefix("/load").Handler(srv.loader)
@ -110,6 +121,7 @@ func StartServer(world *model.DfWorld, static embed.FS) {
fmt.Println("Serving at :8080")
http.ListenAndServe(":8080", srv.router)
return nil
}
func (srv *DfServer) findStructure(p Parms) any {
@ -127,53 +139,6 @@ func (srv *DfServer) findStructure(p Parms) any {
return nil
}
type spaHandler struct {
server *DfServer
staticFS embed.FS
staticPath string
indexPath string
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path := r.URL.Path
// if err != nil {
// // if we failed to get the absolute path respond with a 400 bad request and stop
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
// prepend the path with the path to the static directory
path = h.staticPath + path
_, err := h.staticFS.Open(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
fmt.Println(path)
index, err := h.staticFS.ReadFile(h.staticPath + "/" + h.indexPath)
if err != nil {
h.server.notFound(w)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusAccepted)
w.Write(index)
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// get the subdirectory of the static dir
statics, err := fs.Sub(h.staticFS, h.staticPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.FS(statics)).ServeHTTP(w, r)
}
func (srv *DfServer) notFound(w http.ResponseWriter) {
err := srv.templates.Render(w, "notFound.html", nil)
if err != nil {

56
backend/server/static.go Normal file
View file

@ -0,0 +1,56 @@
package server
import (
"embed"
"fmt"
"io/fs"
"net/http"
"os"
)
type spaHandler struct {
server *DfServer
staticFS embed.FS
staticPath string
indexPath string
}
func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// get the absolute path to prevent directory traversal
path := r.URL.Path
// if err != nil {
// // if we failed to get the absolute path respond with a 400 bad request and stop
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
// prepend the path with the path to the static directory
path = h.staticPath + path
_, err := h.staticFS.Open(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
fmt.Println(path)
index, err := h.staticFS.ReadFile(h.staticPath + "/" + h.indexPath)
if err != nil {
h.server.notFound(w)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusAccepted)
w.Write(index)
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// get the subdirectory of the static dir
statics, err := fs.Sub(h.staticFS, h.staticPath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// otherwise, use http.FileServer to serve the static dir
http.FileServer(http.FS(statics)).ServeHTTP(w, r)
}

View file

@ -21,13 +21,19 @@ func (srv *DfServer) LoadTemplates() {
}
return nil
},
"title": util.Title,
"kebab": func(s string) string { return strcase.ToKebab(s) },
"title": util.Title,
"kebab": func(s string) string { return strcase.ToKebab(s) },
"world": func() *model.DfWorld { return srv.context.world },
"initMap": func() template.HTML {
return template.HTML(fmt.Sprintf(`<script>var worldWidth = %d, worldHeight = %d;</script><script src="/js/map.js"></script>`,
srv.context.world.Width, srv.context.world.Height))
},
"hf": func(id int) template.HTML { return model.LinkHf(srv.context.world, id) },
"getHf": func(id int) *model.HistoricalFigure { return srv.context.world.HistoricalFigures[id] },
"entity": func(id int) template.HTML { return model.LinkEntity(srv.context.world, id) },
"getEntity": func(id int) *model.Entity { return srv.context.world.Entities[id] },
"site": func(id int) template.HTML { return model.LinkSite(srv.context.world, id) },
"addSite": func(id int) template.HTML { return model.AddMapSite(srv.context.world, id) },
"getSite": func(id int) *model.Site { return srv.context.world.Sites[id] },
"structure": func(siteId, id int) template.HTML { return model.LinkStructure(srv.context.world, siteId, id) },
"region": func(id int) template.HTML { return model.LinkRegion(srv.context.world, id) },

198
backend/static/js/map.js Normal file
View file

@ -0,0 +1,198 @@
var sitesLayer = L.layerGroup();
var constructionsLayer = L.layerGroup();
var landmassesLayer = L.layerGroup();
var regionsLayer = L.layerGroup();
var mountainsLayer = L.layerGroup();
var evilnessLayer = L.layerGroup();
var map = L.map('map', {
maxZoom: 6,
minZoom: 0,
crs: L.CRS.Simple,
layers: [sitesLayer, constructionsLayer, mountainsLayer, evilnessLayer]
});
map.getBoundsZoom = function (bounds, inside, padding) { // (LatLngBounds[, Boolean, Point]) -> Number
bounds = L.latLngBounds(bounds);
var zoom = this.getMinZoom() - (inside ? 1 : 0),
maxZoom = this.getMaxZoom(),
size = this.getSize(),
nw = bounds.getNorthWest(),
se = bounds.getSouthEast(),
zoomNotFound = true,
boundsSize;
padding = L.point(padding || [0, 0]);
var incement = 0.02;
do {
zoom += incement;
boundsSize = this.project(se, zoom).subtract(this.project(nw, zoom)).add(padding).floor();
zoomNotFound = !inside ? size.contains(boundsSize) : boundsSize.x < size.x || boundsSize.y < size.y;
} while (zoomNotFound && zoom <= maxZoom);
if (zoomNotFound && inside) {
return null;
}
return inside ? zoom : zoom - incement;
}
var bounds = new L.LatLngBounds([0, 0], [worldWidth,
worldHeight]);
map.setMaxBounds(bounds);
map.fitBounds(bounds);
map.options.minZoom = map.getZoom();
var imageUrl = '/map'
var imageBounds = [[0, 0],
[worldWidth, worldHeight]];
var overlayMaps = {
"Sites": sitesLayer,
"World Constructions": constructionsLayer,
"Mountain Peaks": mountainsLayer,
"Landmasses": landmassesLayer,
"Regions": regionsLayer,
"Evilness": evilnessLayer,
};
L.control.layers(null, overlayMaps).addTo(map);
var imageLayer = L.imageOverlay(imageUrl, imageBounds, { opacity: 0.5 });
imageLayer.addTo(map);
// var opacitySlider = new L.Control.opacitySlider();
// map.addControl(opacitySlider);
// opacitySlider.setOpacityLayer(imageLayer);
var minx = 1000, miny = 1000, maxx = 0, maxy = 0;
function zoomTo(y, x, zoom) {
x = worldWidth - x - 1;
map.setView([x, y], zoom);
}
function zoom() {
var sw = L.latLng(minx, miny), ne = L.latLng(maxx + 1, maxy + 1);
var bounds = new L.LatLngBounds(sw, ne);
console.log(sw, ne, bounds);
map.fitBounds(bounds);
}
var siteOffset = 0.1;
var structureOffset = 0.35;
var battleOffset = 0.2;
var mountainOffset = -0.2;
function coordO(y, x, yo, xo) {
var c = coord(y, x);
return [c[0] + yo, c[1] + xo]
}
function square(y, x, o) {
return [coordO(y, x, o, o), coordO(y, x, 1 - o, o), coordO(y, x, 1 - o, 1 - o), coordO(y, x, o, 1 - o)];
}
function attachTooltip(layer, tip) {
layer.bindTooltip(tip, { direction: 'top' }).bindPopup(tip);
}
function addSite(name, y1, x1, y2, x2, color, glyph) {
/* resize tiny sites like lairs */
var MIN_SIZE = .3;
if (y2 - y1 < MIN_SIZE) {
y1 = (y1 + y2) / 2 - MIN_SIZE / 2;
y2 = y1 + MIN_SIZE;
}
if (x2 - x1 < MIN_SIZE) {
x1 = (x1 + x2) / 2 - MIN_SIZE / 2;
x2 = x1 + MIN_SIZE;
}
/* TODO: use glyph of the site instead of a polygon? */
var polygon = L.polygon(
[coord(y1, x1), coord(y2, x1), coord(y2, x2), coord(y1, x2)], {
color: color,
opacity: 1, fillOpacity: 0.7,
weight: 3
}).addTo(sitesLayer);
attachTooltip(polygon, name);
}
function addWc(name, y, x, color) {
var polygon = L.polygon(square(y, x, structureOffset), {
color: color,
opacity: 1, fillOpacity: 0.7,
weight: 3
}).addTo(constructionsLayer);
attachTooltip(polygon, name);
}
function addRegion(name, y1, x1, y2, x2, color) {
x1--; y2++;
var polygon = L.polygon(
[coord(y1, x1), coord(y2, x1), coord(y2, x2), coord(y1, x2)], {
color: color,
opacity: 0.5, fillOpacity: 0.3,
weight: 1
}).addTo(landmassesLayer);
attachTooltip(polygon, name);
}
function addMountain(name, y, x, color) {
x = worldWidth - x - 1;
var polygon = L.polygon(
[[x + mountainOffset / 2, y + mountainOffset], [x + mountainOffset / 2, y + 1 - mountainOffset], [x + 1 - mountainOffset, y + 0.5]], {
color: color,
opacity: 1, fillOpacity: 0.7,
weight: 3
}).addTo(mountainsLayer);
attachTooltip(polygon, name);
minx = Math.min(x, minx);
miny = Math.min(y, miny);
maxx = Math.max(x, maxx);
maxy = Math.max(y, maxy);
}
function coord(y, x) {
x = worldWidth - x - 1;
minx = Math.min(x, minx);
miny = Math.min(y, miny);
maxx = Math.max(x, maxx);
maxy = Math.max(y, maxy);
return [x, y];
}
function addBattle(name, y, x) {
x = worldWidth - x - 1;
var polygon = L.polygon(
[[x + 0.5, y + battleOffset],
[x + battleOffset, y + 0.5],
[x + 0.5, y + 1 - battleOffset],
[x + 1 - battleOffset, y + 0.5]], {
color: '#f00',
opacity: 1, fillOpacity: 0.7,
weight: 3
}).addTo(map);
attachTooltip(polygon, name);
minx = Math.min(x, minx);
miny = Math.min(y, miny);
maxx = Math.max(x, maxx);
maxy = Math.max(y, maxy);
}

View file

@ -17,15 +17,13 @@
</ul>
<div id="map" style="height: 400px"></div>
<script>
var map = L.map('map').setView([5, 5], 13);
{{initMap}}
{{- range $race, $civs := .Civilizations }}
{{- range $civs }}
{{- range .Sites }}
{{ addSite . }}
{{- end }}
{{- end }}
{{- end }}
var imageUrl = '$suburi/map'
var imageBounds = [[0, 0],
[10, 10]];
var imageLayer = L.imageOverlay(imageUrl, imageBounds, { opacity: 0.5 });
imageLayer.addTo(map);
</script>
{{- end }}

View file

@ -88,7 +88,6 @@
<script>
var hash = document.location.hash;
console.log("hah", hash)
if (hash && hash.startsWith("#nav-")) {
var hashPieces = hash.split('?');
activeTab = $('.nav-link[data-bs-target="' + hashPieces[0] + '"]');