46 files changed, 1732 insertions, 681 deletions
diff --git a/config/config.go b/config/config.go index 7438fe1..f57b5cd 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,7 @@ import ( type Config struct { LogLevel slog.Level `json:"LogLevel" yaml:"logLevel" validate:"required"` BlogPages BlogPagesConfig `json:"BlogPages" yaml:"blogPages" validate:"required"` + FactGiver FactGiverConfig `json:"FactGiver" yaml:"factGiver" validate:"required"` LocalePath string `json:"LocalePath" yaml:"localePath" validate:"required,filepath"` AvailableLanguages []AvailableLanguageConfig `json:"AvailableLanguages" yaml:"availableLanguages" validate:"required"` Auth AuthConfig `json:"Auth" yaml:"auth" validate:"required"` @@ -33,6 +34,11 @@ type B2Config struct { ApplicationKey string `json:"ApplicationKey" yaml:"applicationKey"` } +type FactGiverConfig struct { + Storage StorageConfig `json:"Storage" yaml:"storage" validate:"required"` + FactsFileName string `json:"FactsFileName" yaml:"factsFileName" validate:"required"` +} + type AvailableLanguageConfig struct { Name string `json:"Name" yaml:"name" validate:"required"` Alt string `json:"Alt" yaml:"alt"` diff --git a/config/config.local.yaml b/config/config.local.yaml index 0b48e47..6d0abf3 100644 --- a/config/config.local.yaml +++ b/config/config.local.yaml @@ -8,6 +8,16 @@ blogPages: prefix: 'stage-' keyID: '${B2_KEY_ID}' applicationKey: '${B2_APPLICATION_KEY}' +factGiver: + storage: + type: b2 + config: + bucketName: sayana-pages + region: eu-central-003 + prefix: '' + keyID: '${B2_KEY_ID}' + applicationKey: '${B2_APPLICATION_KEY}' + factsFileName: 'facts-*.txt' localePath: ./locale/ availableLanguages: - name: ru diff --git a/config/config.prod.yaml b/config/config.prod.yaml index e1294c0..38981a8 100644 --- a/config/config.prod.yaml +++ b/config/config.prod.yaml @@ -8,6 +8,16 @@ blogPages: prefix: 'prod-' keyID: '${B2_KEY_ID}' applicationKey: '${B2_APPLICATION_KEY}' +factGiver: + storage: + type: b2 + config: + bucketName: sayana-pages + region: eu-central-003 + prefix: '' + keyID: '${B2_KEY_ID}' + applicationKey: '${B2_APPLICATION_KEY}' + factsFileName: 'facts-*.txt' localePath: ./locale/ availableLanguages: - name: ru diff --git a/config/config.stage.yaml b/config/config.stage.yaml index 30233c1..b05edaf 100644 --- a/config/config.stage.yaml +++ b/config/config.stage.yaml @@ -8,6 +8,16 @@ blogPages: prefix: 'stage-' keyID: '${B2_KEY_ID}' applicationKey: '${B2_APPLICATION_KEY}' +factGiver: + storage: + type: b2 + config: + bucketName: sayana-pages + region: eu-central-003 + prefix: '' + keyID: '${B2_KEY_ID}' + applicationKey: '${B2_APPLICATION_KEY}' + factsFileName: 'facts-*.txt' localePath: ./locale/ availableLanguages: - name: ru @@ -15,6 +15,9 @@ require ( require ( github.com/andybalholm/brotli v1.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -2,9 +2,15 @@ github.com/Backblaze/blazer v0.7.2 h1:UWNHMLB+Nf+UmbO2qkVvgriODLEMz4kIyr2Hm+DVXQ github.com/Backblaze/blazer v0.7.2/go.mod h1:T4y3EYa9IQ5J0PKc/C/J8/CEnSd3qa/lgNw938wZg10= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk= +github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= diff --git a/internal/factgiver/factgiver.go b/internal/factgiver/factgiver.go new file mode 100644 index 0000000..6f77403 --- /dev/null +++ b/internal/factgiver/factgiver.go @@ -0,0 +1,67 @@ +package factgiver + +import ( + "fmt" + "math/rand" + "regexp" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/config" + "github.com/SayaAndy/saya-today-web/internal/b2" +) + +type FactGiver struct { + b2Client *b2.B2Client + cache map[string][]string + langs []string + factsFileName string + nlRe *regexp.Regexp + randGen *rand.Rand +} + +func NewFactGiver(cfg *config.FactGiverConfig, langs []string) (*FactGiver, error) { + b2Client, err := b2.NewB2Client(&cfg.Storage.Config) + if err != nil { + return nil, fmt.Errorf("fail to init b2 client for a new fact giver: %s", err.Error()) + } + + factGiver := &FactGiver{ + b2Client: b2Client, + cache: make(map[string][]string, len(langs)), + langs: langs, + factsFileName: cfg.FactsFileName, + nlRe: regexp.MustCompile(`\r?\n`), + randGen: rand.New(rand.NewSource(time.Now().UnixNano())), + } + if err = factGiver.initCache(); err != nil { + return nil, fmt.Errorf("fail to init cache for a new fact giver: %s", err.Error()) + } + + return factGiver, nil +} + +func (g *FactGiver) Give(lang string) [3]string { + factSlice := make([]string, len(g.cache[lang])) + copy(factSlice, g.cache[lang]) + g.randGen.Shuffle(len(factSlice), func(i, j int) { + factSlice[i], factSlice[j] = factSlice[j], factSlice[i] + }) + return [3]string{factSlice[0], factSlice[1], factSlice[2]} +} + +func (g *FactGiver) initCache() error { + for _, lang := range g.langs { + localFacts := strings.Replace(g.factsFileName, "*", lang, 1) + factsContentBytes, err := g.b2Client.ReadAll(localFacts) + if err != nil { + return fmt.Errorf("fail to read '%s' facts file: %s", lang, err.Error()) + } + factsContent := string(factsContentBytes) + g.cache[lang] = g.nlRe.Split(factsContent, -1) + if g.cache[lang][len(g.cache[lang])-1] == "" { + g.cache[lang] = g.cache[lang][:len(g.cache[lang])-1] + } + } + return nil +} diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go index 1a521c0..83312dc 100644 --- a/internal/frontmatter/parser.go +++ b/internal/frontmatter/parser.go @@ -16,6 +16,8 @@ type Metadata struct { Thumbnail string `yaml:"thumbnail"` Tags []string `yaml:"tags"` Geolocation string `yaml:"geolocation"` + Medley string `yaml:"medley"` + MedleyPart int `yaml:"medleyPart"` } func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) { diff --git a/internal/glightbox/block.go b/internal/glightbox/block.go new file mode 100644 index 0000000..e53e67b --- /dev/null +++ b/internal/glightbox/block.go @@ -0,0 +1,32 @@ +package glightbox + +import ( + "time" + + "github.com/yuin/goldmark/ast" +) + +// GLightboxBlock represents a light gallery block in the AST +type GLightboxBlock struct { + ast.BaseBlock + Images []GLightboxImage + Location *time.Location +} + +type GLightboxImage struct { + URL string + Tags []string + Caption []byte +} + +var KindGLightboxBlock = ast.NewNodeKind("GLightboxBlock") + +// Dump implements ast.Node.Dump +func (n *GLightboxBlock) Dump(source []byte, level int) { + ast.DumpHelper(n, source, level, nil, nil) +} + +// Kind implements ast.Node.Kind +func (n *GLightboxBlock) Kind() ast.NodeKind { + return KindGLightboxBlock +} diff --git a/internal/lightgallery/extension.go b/internal/glightbox/extension.go index 761dc05..7fd17b1 100644 --- a/internal/lightgallery/extension.go +++ b/internal/glightbox/extension.go @@ -1,4 +1,4 @@ -package lightgallery +package glightbox import ( "github.com/yuin/goldmark" @@ -8,21 +8,21 @@ import ( ) // Extension that combines parser and renderer -type LightGalleryExtension struct{} +type GLightboxExtension struct{} -func NewLightGalleryExtension() goldmark.Extender { - return &LightGalleryExtension{} +func NewGLightboxExtension() goldmark.Extender { + return &GLightboxExtension{} } -func (e *LightGalleryExtension) Extend(m goldmark.Markdown) { +func (e *GLightboxExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions( parser.WithBlockParsers( - util.Prioritized(NewLightGalleryParser(), 500), + util.Prioritized(NewGLightboxParser(), 500), ), ) m.Renderer().AddOptions( renderer.WithNodeRenderers( - util.Prioritized(NewLightGalleryHTMLRenderer(), 500), + util.Prioritized(NewGLightboxHTMLRenderer(), 500), ), ) } diff --git a/internal/glightbox/html_renderer.go b/internal/glightbox/html_renderer.go new file mode 100644 index 0000000..44fbd4c --- /dev/null +++ b/internal/glightbox/html_renderer.go @@ -0,0 +1,188 @@ +package glightbox + +import ( + "bytes" + "fmt" + "math/rand" + "regexp" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/internal/tailwind" + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +type GLightboxHTMLRenderer struct { + html.Config + md goldmark.Markdown + anchorMatchRe *regexp.Regexp +} + +func NewGLightboxHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &GLightboxHTMLRenderer{ + Config: html.NewConfig(), + md: goldmark.New( + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + parser.WithAttribute(), + ), + goldmark.WithRenderer( + renderer.NewRenderer( + renderer.WithNodeRenderers( + util.Prioritized(tailwind.NewCustomLinkRenderer( + html.WithUnsafe(), html.WithHardWraps(), html.WithXHTML(), + ), 50), + util.Prioritized(html.NewRenderer( + html.WithUnsafe(), html.WithHardWraps(), html.WithXHTML(), + ), 100), + ), + ), + ), + ), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + r.anchorMatchRe = regexp.MustCompile(`(?s)<\s*a(\s+[^<]*)(href\s*=\s*["'].*?["'])\s*([^<]*)>(.*?)<\s*\/\s*a\s*>`) + return r +} + +func (r *GLightboxHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(KindGLightboxBlock, r.renderGLightbox) +} + +func (r *GLightboxHTMLRenderer) renderGLightbox(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + gallery := n.(*GLightboxBlock) + + if len(gallery.Images) == 0 { + return ast.WalkContinue, nil + } + + galleryID := generateDivId(8) + + var elements []string + for i, img := range gallery.Images { + imageUrlSegments := strings.Split(img.URL, ".") + imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") + imageUrlParts := strings.Split(imageUrlWithoutExt, "/") + imageNameParts := strings.Split(imageUrlParts[len(imageUrlParts)-1], "-") + + var captionBuf bytes.Buffer + captionHTML := img.Caption + if err := r.md.Convert(img.Caption, &captionBuf); err == nil { + captionHTML = captionBuf.Bytes() + captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) + captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) + } + + dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) + dayDate = dayDate.In(gallery.Location) + + glightboxDescId := "" + if len(captionHTML) != 0 { + glightboxDescId = fmt.Sprintf("glightbox-desc-%s-%d", galleryID, i) + } + + dataDescriptionAttribute := "" + if glightboxDescId != "" { + dataDescriptionAttribute = fmt.Sprintf("data-description=\".%s\"", glightboxDescId) + } + + tagClassList := make([]string, 0, len(img.Tags)) + for _, tag := range img.Tags { + switch tag { + case "2x": + tagClassList = append(tagClassList, "grid-item-2x") + } + } + + if len(img.Caption) > 0 { + tagClassList = append(tagClassList, "grid-tooltip") + } + + anchorlessCaptionHTML := r.anchorMatchRe.ReplaceAll(captionHTML, []byte("<span class=\"linklike\" $1 $3>$4</span>")) + + elements = append(elements, fmt.Sprintf(` + <a href="https://f003.backblazeb2.com/file/sayana-photos/full/%s" class="glightbox-%s grid-item %s grid-item-%s p-1" + data-gallery="gallery-%s" data-title="%s" %s> + <picture> + <source media="(width < 640px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp" /> + <source media="(width < 1120px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp" /> + <source media="(width < 1600px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp" /> + <source media="(width < 2400px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp" /> + <source media="(width >= 2400px)" srcset="https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp" /> + <img src="https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp" /> + </picture> + <span class="grid-tooltip-text"><p>%s</p></span> + <span class="grid-item-index">%d</span> + </a>`, img.URL, galleryID, strings.Join(tagClassList, " "), galleryID, galleryID, dayDate.Format("2006-01-02 15:04:05 -07:00"), dataDescriptionAttribute, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, anchorlessCaptionHTML, i+1)) + + if glightboxDescId != "" { + elements = append(elements, fmt.Sprintf(` + <div class="glightbox-desc rounded-4 %s"> + <p>%s</p> + </div>`, glightboxDescId, captionHTML)) + } + } + + w.WriteString(fmt.Sprintf(` +<div class="justify-content-center display-block m-1"> + <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%%] ml-auto mr-auto"> + <div class="grid masonry-grid-%s mx-auto"> + <div class="grid-sizer grid-sizer-%s"></div> + %s + </div> + <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%%] ml-auto mr-auto"> +</div>`, galleryID, galleryID, strings.Join(elements, "\n"))) + + w.WriteString(fmt.Sprintf(` +<script> + var lightbox_%s = GLightbox({ + selector: '.glightbox-%s', + moreLength: 0 + }); + + var msnry_%s = new Masonry('.masonry-grid-%s', { + itemSelector: '.grid-item-%s', + columnWidth: '.grid-sizer-%s', + percentPosition: true, + horizontalOrder: true + }); + + var imgLoad_%s_timer; + var imgLoad_%s = imagesLoaded('.masonry-grid-%s'); + + function initMasonryLayout_%s() { + clearTimeout(imgLoad_%s_timer); + imgLoad_%s_timer = setTimeout(() => msnry_%s.layout(), 500); + } + + imgLoad_%s.on('progress', initMasonryLayout_%s); + + document.addEventListener('popout', (e) => { + imgLoad_%s.off('progress', initMasonryLayout_%s); + initMasonryLayout_%s = null; + imgLoad_%s = null; + }); +</script>`, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID, galleryID)) + } + + return ast.WalkContinue, nil +} + +func generateDivId(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz" + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time + b := make([]byte, length) + for i := range b { + b[i] = charset[seededRand.Intn(len(charset))] + } + return string(b) +} diff --git a/internal/lightgallery/parser.go b/internal/glightbox/parser.go index daec797..6780090 100644 --- a/internal/lightgallery/parser.go +++ b/internal/glightbox/parser.go @@ -1,9 +1,10 @@ -package lightgallery +package glightbox import ( "bytes" "log/slog" "regexp" + "strings" "time" "github.com/yuin/goldmark/ast" @@ -11,17 +12,17 @@ import ( "github.com/yuin/goldmark/text" ) -type LightGalleryParser struct{} +type GLightboxParser struct{} -func NewLightGalleryParser() parser.BlockParser { - return &LightGalleryParser{} +func NewGLightboxParser() parser.BlockParser { + return &GLightboxParser{} } -func (p *LightGalleryParser) Trigger() []byte { +func (p *GLightboxParser) Trigger() []byte { return []byte{'{'} } -func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { +func (p *GLightboxParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, _ := reader.PeekLine() if !bytes.HasPrefix(line, []byte("{Gallery:")) { @@ -43,10 +44,10 @@ func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser return nil, parser.NoChildren } - return &LightGalleryBlock{Location: loc}, parser.NoChildren + return &GLightboxBlock{Location: loc}, parser.NoChildren } -func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { +func (p *GLightboxParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() if len(line) == 0 || segment.Len() == 0 { return parser.Continue | parser.NoChildren @@ -58,31 +59,42 @@ func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc pars return parser.Close } - gallery := node.(*LightGalleryBlock) + gallery := node.(*GLightboxBlock) - parts := bytes.SplitN(trimmed, []byte{'|'}, 2) + parts := bytes.SplitN(trimmed, []byte{'|'}, 3) url := bytes.TrimSpace(parts[0]) caption := make([]byte, 0) - if len(parts) > 1 { + tagsRaw := "" + if len(parts) == 2 { caption = bytes.TrimSpace(parts[1]) } + if len(parts) >= 3 { + tagsRaw = string(parts[1]) + caption = bytes.TrimSpace(parts[2]) + } + + tags := strings.Split(tagsRaw, ",") + for i := range tags { + tags[i] = strings.TrimSpace(tags[i]) + } - gallery.Images = append(gallery.Images, LightGalleryImage{ + gallery.Images = append(gallery.Images, GLightboxImage{ URL: string(url), + Tags: tags, Caption: caption, }) return parser.Continue | parser.NoChildren } -func (p *LightGalleryParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { +func (p *GLightboxParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { } -func (p *LightGalleryParser) CanInterruptParagraph() bool { +func (p *GLightboxParser) CanInterruptParagraph() bool { return true } -func (p *LightGalleryParser) CanAcceptIndentedLine() bool { +func (p *GLightboxParser) CanAcceptIndentedLine() bool { return false } diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go deleted file mode 100644 index 8eb7c9e..0000000 --- a/internal/lightgallery/block.go +++ /dev/null @@ -1,31 +0,0 @@ -package lightgallery - -import ( - "time" - - "github.com/yuin/goldmark/ast" -) - -// LightGalleryBlock represents a light gallery block in the AST -type LightGalleryBlock struct { - ast.BaseBlock - Images []LightGalleryImage - Location *time.Location -} - -type LightGalleryImage struct { - URL string - Caption []byte -} - -var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock") - -// Dump implements ast.Node.Dump -func (n *LightGalleryBlock) Dump(source []byte, level int) { - ast.DumpHelper(n, source, level, nil, nil) -} - -// Kind implements ast.Node.Kind -func (n *LightGalleryBlock) Kind() ast.NodeKind { - return KindLightGalleryBlock -} diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go deleted file mode 100644 index 17a6f2b..0000000 --- a/internal/lightgallery/html_renderer.go +++ /dev/null @@ -1,137 +0,0 @@ -package lightgallery - -import ( - "bytes" - "fmt" - "math/rand" - "strings" - "time" - - "github.com/yuin/goldmark" - "github.com/yuin/goldmark/ast" - "github.com/yuin/goldmark/renderer" - "github.com/yuin/goldmark/renderer/html" - "github.com/yuin/goldmark/util" -) - -type LightGalleryHTMLRenderer struct { - html.Config - md goldmark.Markdown -} - -func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { - r := &LightGalleryHTMLRenderer{ - Config: html.NewConfig(), - md: goldmark.New( - goldmark.WithRendererOptions( - html.WithHardWraps(), - html.WithXHTML(), - ), - ), - } - for _, opt := range opts { - opt.SetHTMLOption(&r.Config) - } - return r -} - -func (r *LightGalleryHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { - reg.Register(KindLightGalleryBlock, r.renderLightGallery) -} - -func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { - if entering { - gallery := n.(*LightGalleryBlock) - - if len(gallery.Images) == 0 { - return ast.WalkContinue, nil - } - - galleryID := generateDivId(8) - - w.WriteString(fmt.Sprintf(` -<div class="justify-content-center display-block m-2"> - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%%] ml-auto mr-auto"> - <div id="lg-%s" class="inline-gallery-container relative ml-auto mr-auto"></div> - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%%] ml-auto mr-auto"> -</div>`, galleryID)) - - var dynamicElements []string - for _, img := range gallery.Images { - imageUrlSegments := strings.Split(img.URL, ".") - imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") - imageNameParts := strings.Split(imageUrlWithoutExt, "-") - - var captionBuf bytes.Buffer - captionHTML := img.Caption - if err := r.md.Convert(img.Caption, &captionBuf); err == nil { - captionHTML = captionBuf.Bytes() - captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) - } - - dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) - dayDate = dayDate.In(gallery.Location) - dynamicElements = append(dynamicElements, fmt.Sprintf(`{ - src: - "https://f003.backblazeb2.com/file/sayana-photos/full/%s", - downloadUrl: - "https://f003.backblazeb2.com/file/sayana-photos/full/%s", - alt: "%s", - responsive: "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp 384, https://f003.backblazeb2.com/file/sayana-photos/webp-560p/%s.webp 672, https://f003.backblazeb2.com/file/sayana-photos/webp-800p/%s.webp 960, https://f003.backblazeb2.com/file/sayana-photos/webp-1200p/%s.webp 1440, https://f003.backblazeb2.com/file/sayana-photos/webp-1600p/%s.webp 1920", - thumb: - "https://f003.backblazeb2.com/file/sayana-photos/webp-320p/%s.webp", - subHtml: `+"`"+`<div class="flex flex-row light-gallery-captions"> - <p class="grow !text-[1vmax]/[1] text-left font-spectral text-main-dark">%s</p> - <p class="!text-[1vmax]/[1] text-right font-spectral text-secondary">%s</p> - </div>`+"`"+` - }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00"))) - } - - w.WriteString(fmt.Sprintf(` -<script> -function createLightGallery%s() { - const $lgContainer = document.getElementById('lg-%s'); - const config = { - container: $lgContainer, - dynamic: true, - dynamicEl: [%s], - width: "100%%", - height: "50vmin", - hash: false, - closable: false, - showMaximizeIcon: true, - appendSubHtmlTo: ".lg-sub-html", - isMobile: () => false, - slideDelay: 0, - plugins: [lgZoom, lgThumbnail], - thumbWidth: calculateVmin(10), - thumbHeight: "10vmin", - thumbMargin: 4 - }; - const inlineGallery = lightGallery($lgContainer, config); - - setTimeout(() => { - inlineGallery.openGallery(); - }, 200); - - galleryMap.set(inlineGallery, createLightGallery%s); -} - -document.addEventListener('DOMContentLoaded', createLightGallery%s); -</script>`, galleryID, galleryID, strings.Join(dynamicElements, ","), galleryID, galleryID)) - } - - return ast.WalkContinue, nil -} - -func generateDivId(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyz" - seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time - b := make([]byte, length) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - return string(b) -} diff --git a/internal/router/api-v1-blog-search.go b/internal/router/api-v1-blog-search.go index e22fabe..69be651 100644 --- a/internal/router/api-v1-blog-search.go +++ b/internal/router/api-v1-blog-search.go @@ -1,6 +1,7 @@ package router import ( + "encoding/json" "fmt" "log/slog" "net/url" @@ -35,10 +36,18 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie slog.Warn("unable to parse a client timezone, defaulting to UTC", slog.String("error", err.Error()), slog.String("tz", tz)) } - pages, err := b2Client.Scan(lang + "/") - if err != nil { - c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) - return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err)) + cacheKey := "blog-search." + lang + ".pages-list" + var pages []*b2.BlogPage + if pagesBytes, ok := PCache.Get(cacheKey); pagesBytes != nil || ok { + json.Unmarshal(pagesBytes, &pages) + } else { + pages, err = b2Client.Scan(lang + "/") + if err != nil { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages for '%s' lang: %v", lang, err)) + } + pagesBytes, _ := json.Marshal(pages) + PCache.SetWithTTL(cacheKey, pagesBytes, int64(len(pagesBytes)), 5*time.Minute) } encodedQuery := c.Request().URI().QueryString() @@ -70,6 +79,7 @@ func Api_V1_BlogSearch(l map[string]*locale.LocaleConfig, langs []string, b2Clie "Thumbnail": page.Metadata.Thumbnail, "Tags": page.Metadata.Tags, "LikeCount": CCache.GetLikeCount(page.FileName), + "ViewCount": CCache.GetViewCount(page.FileName), }) break } diff --git a/internal/router/api-v1-general-page-body.go b/internal/router/api-v1-general-page-body.go new file mode 100644 index 0000000..c1a4a9f --- /dev/null +++ b/internal/router/api-v1-general-page-body.go @@ -0,0 +1,164 @@ +package router + +import ( + "fmt" + "html/template" + "log/slog" + "math/rand" + "net/url" + "regexp" + "slices" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/internal/factgiver" + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" + "github.com/yuin/goldmark" +) + +var FactGiver *factgiver.FactGiver + +func init() { + tm.Add("general-page-body", "views/partials/general-page-body.html") +} + +func Api_V1_GeneralPage_Body(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client, md goldmark.Markdown) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + + cacheKey := fmt.Sprintf("body.%s", path) + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + querySort := c.Query("sort") + if querySort == "" { + querySort = "publicationDateDesc" + } + + encodedQuery := c.Request().URI().QueryString() + re, err := regexp.Compile(`tags\[\]=([\w]+)`) + if err != nil { + slog.Warn("failed to generate regex for tags gathering", slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate regex for tags gathering") + } + decodedQuery, _ := url.QueryUnescape(string(encodedQuery)) + matches := re.FindAllStringSubmatch(decodedQuery, -1) + + queryTags := make([]string, 0, len(matches)) + for _, match := range matches { + queryTags = append(queryTags, string(match[1])) + } + + pages, err := b2Client.Scan(lang + "/") + if err != nil { + slog.Warn("failed to scan pages via b2", slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString(fmt.Sprintf("failed to scan pages via b2: %s", slog.String("error", err.Error()))) + } + + tagsMap := make(map[string]int) + for _, page := range pages { + for _, tag := range page.Metadata.Tags { + tagsMap[tag]++ + } + } + slog.Debug("enlist pages for catalogue", slog.Int("tag_count", len(tagsMap)), slog.Int("page_count", len(pages)), slog.String("path", c.Path())) + + type Tag struct { + Name string `json:"Name" yaml:"name"` + Count int `json:"Count" yaml:"count"` + } + + tagsArray := make([]Tag, 0, len(tagsMap)) + for tag, count := range tagsMap { + tagsArray = append(tagsArray, Tag{tag, count}) + } + slices.SortFunc(tagsArray, func(a Tag, b Tag) int { + return strings.Compare(a.Name, b.Name) + }) + + values["Tags"] = tagsArray + values["QuerySort"] = querySort + values["QueryTags"] = strings.Join(queryTags, ",") + values["Title"] = l[lang].BlogSearch.Header + + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + metadata, parsedMarkdown, err := readBlogPost(md, b2Client, lang+"/"+pathParts[2]) + if err != nil { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", pathParts[2])) + } + + geolocationParts := strings.Split(metadata.Geolocation, " ") + var x, y, areaError string + if len(geolocationParts) >= 2 { + x = geolocationParts[0] + y = geolocationParts[1] + } + if len(geolocationParts) >= 3 { + areaError = geolocationParts[2] + } + + values["MapLocationX"] = x + values["MapLocationY"] = y + values["MapLocationAreaMeters"] = areaError + values["Title"] = metadata.Title + values["ParsedMarkdown"] = template.HTML(parsedMarkdown) + values["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") + values["ActionDate"] = metadata.ActionDate + + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + + go CCache.View(c.IP(), pathParts[2]) + } else if len(pathParts) == 1 { + values["Title"] = l[lang].HomePage.Header + values["FilledHeartCount"] = uint(40) + values["OutlineHeartCount"] = uint(40) + values["GifName"] = fmt.Sprintf("otter-%d.gif", rand.Int()%3+1) + values["FunFacts"] = FactGiver.Give(lang) + additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") + } + + content, err := tm.Render("general-page-body", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page-bottom-embeds.go b/internal/router/api-v1-general-page-bottom-embeds.go new file mode 100644 index 0000000..c4d9543 --- /dev/null +++ b/internal/router/api-v1-general-page-bottom-embeds.go @@ -0,0 +1,75 @@ +package router + +import ( + "fmt" + "log/slog" + "net/url" + "slices" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/internal/b2" + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("general-page-bottom-embeds", "views/partials/general-page-bottom-embeds.html") +} + +func Api_V1_GeneralPage_BottomEmbeds(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + cacheKey := fmt.Sprintf("bottom-embeds.%s", path) + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } else if len(pathParts) == 1 { + additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") + } + + content, err := tm.Render("general-page-bottom-embeds", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page-footer.go b/internal/router/api-v1-general-page-footer.go new file mode 100644 index 0000000..003f54c --- /dev/null +++ b/internal/router/api-v1-general-page-footer.go @@ -0,0 +1,75 @@ +package router + +import ( + "fmt" + "log/slog" + "net/url" + "slices" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("general-page-footer", "views/partials/general-page-footer.html") +} + +func Api_V1_GeneralPage_Footer(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + + cacheKey := fmt.Sprintf("footer.%s", path) + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } else if len(pathParts) == 1 { + additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") + } + + content, err := tm.Render("general-page-footer", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page-header.go b/internal/router/api-v1-general-page-header.go index 8ac7c03..80228f7 100644 --- a/internal/router/api-v1-general-page-header.go +++ b/internal/router/api-v1-general-page-header.go @@ -2,15 +2,22 @@ package router import ( "fmt" + "log/slog" "net/url" "slices" "strings" + "time" + "github.com/SayaAndy/saya-today-web/internal/b2" "github.com/SayaAndy/saya-today-web/locale" "github.com/gofiber/fiber/v2" ) -func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { +func init() { + tm.Add("general-page-header", "views/partials/general-page-header.html") +} + +func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B2Client) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) @@ -24,8 +31,15 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string } path := urlStruct.EscapedPath() - pathParts := strings.SplitN(strings.Trim(path, "/"), "/", 2) - if len(pathParts) < 2 { + + cacheKey := fmt.Sprintf("header.%s", path) + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") } @@ -34,6 +48,38 @@ func Api_V1_GeneralPage_Header(l map[string]*locale.LocaleConfig, langs []string return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) } - return nil + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + values["Title"] = l[lang].BlogSearch.Header + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + metadata, _, err := b2Client.ReadFrontmatter(lang + "/" + pathParts[2] + ".md") + if err != nil { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("could not read '%s' for content", path)) + } + values["Title"] = metadata.Title + values["PublishedDate"] = metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00") + values["ActionDate"] = metadata.ActionDate + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } else if len(pathParts) == 1 { + values["Title"] = l[lang].HomePage.Header + additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") + } + + content, err := tm.Render("general-page-header", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) } } diff --git a/internal/router/api-v1-general-page-top-embeds.go b/internal/router/api-v1-general-page-top-embeds.go new file mode 100644 index 0000000..78021b2 --- /dev/null +++ b/internal/router/api-v1-general-page-top-embeds.go @@ -0,0 +1,75 @@ +package router + +import ( + "fmt" + "log/slog" + "net/url" + "slices" + "strings" + "time" + + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("general-page-top-embeds", "views/partials/general-page-top-embeds.html") +} + +func Api_V1_GeneralPage_TopEmbeds(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + + referer := c.Get("Referer", "") + if referer == "" { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty") + } + urlStruct, err := url.ParseRequestURI(referer) + if err != nil { + return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error())) + } + + path := urlStruct.EscapedPath() + + cacheKey := fmt.Sprintf("top-embeds.%s", path) + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { + return c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + values := fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + } + var additionalTemplates []string + + if len(pathParts) == 2 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-catalogue.html") + } else if len(pathParts) == 3 && pathParts[1] == "blog" { + additionalTemplates = append(additionalTemplates, "views/pages/blog-page.html") + } else if len(pathParts) == 1 { + additionalTemplates = append(additionalTemplates, "views/pages/home-page.html") + } + + content, err := tm.Render("general-page-top-embeds", values, additionalTemplates...) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.SetWithTTL(cacheKey, content, int64(len(content)), 5*time.Minute) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/api-v1-general-page.go b/internal/router/api-v1-general-page.go new file mode 100644 index 0000000..d0ea559 --- /dev/null +++ b/internal/router/api-v1-general-page.go @@ -0,0 +1,52 @@ +package router + +import ( + "fmt" + "log/slog" + "slices" + "strings" + + "github.com/SayaAndy/saya-today-web/locale" + "github.com/gofiber/fiber/v2" +) + +func init() { + tm.Add("general-page", "views/layouts/general-page.html") +} + +func Api_V1_GeneralPage(l map[string]*locale.LocaleConfig, langs []string) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + path := c.Path() + + pathParts := strings.Split(strings.Trim(path, "/"), "/") + if len(pathParts) == 0 { + return c.Status(fiber.ErrBadRequest.Code).SendString("url path is invalid: expect format '/{lang}/...'") + } + + lang := pathParts[0] + if !slices.Contains(langs, lang) { + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + cacheKey := "general-page." + lang + if val, ok := PCache.Get(cacheKey); val != nil && ok { + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(val) + } + + content, err := tm.Render("general-page", fiber.Map{ + "L": l[lang], + "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), + }) + if err != nil { + slog.Warn("failed to generate div", slog.String("path", path), slog.String("error", err.Error())) + return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div") + } + + go PCache.Set(cacheKey, content, int64(len(content))) + c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8) + return c.Status(fiber.StatusOK).Type("html").Send(content) + } +} diff --git a/internal/router/client-cache.go b/internal/router/client-cache.go index 81d7641..d607cdd 100644 --- a/internal/router/client-cache.go +++ b/internal/router/client-cache.go @@ -11,16 +11,12 @@ import ( "golang.org/x/crypto/argon2" ) -type PageLike struct { - PageRef string - UserId string -} - type ClientCache struct { hashMap map[string]string hashMapMutex sync.RWMutex likePageMap map[string]map[string]struct{} + viewPageMap map[string]map[string]struct{} pageMutexMap map[string]*sync.RWMutex pageMutexMapMutex sync.Mutex @@ -43,6 +39,7 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { } likePageMap := make(map[string]map[string]struct{}) + viewPageMap := make(map[string]map[string]struct{}) pageMutexMap := make(map[string]*sync.RWMutex) for rows.Next() { @@ -55,9 +52,32 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { userIdString := base64.RawStdEncoding.EncodeToString(userId) if _, ok := likePageMap[pageRef]; !ok { likePageMap[pageRef] = make(map[string]struct{}) + viewPageMap[pageRef] = make(map[string]struct{}) pageMutexMap[pageRef] = &sync.RWMutex{} } likePageMap[pageRef][userIdString] = struct{}{} + viewPageMap[pageRef][userIdString] = struct{}{} + } + + rows, err = tx.Query("select * from blog_views;") + if err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail to query db for blog_views to fill cache: %w", err) + } + + for rows.Next() { + var pageRef string + var userId []byte + if err = rows.Scan(&pageRef, &userId); err != nil { + tx.Rollback() + return nil, fmt.Errorf("fail scanning blog_views to fill cache: %w", err) + } + userIdString := base64.RawStdEncoding.EncodeToString(userId) + if _, ok := viewPageMap[pageRef]; !ok { + viewPageMap[pageRef] = make(map[string]struct{}) + pageMutexMap[pageRef] = &sync.RWMutex{} + } + viewPageMap[pageRef][userIdString] = struct{}{} } if err = tx.Commit(); err != nil { @@ -67,6 +87,7 @@ func NewClientCache(db *sql.DB, salt []byte) (*ClientCache, error) { return &ClientCache{ hashMap: make(map[string]string), likePageMap: likePageMap, + viewPageMap: viewPageMap, pageMutexMap: pageMutexMap, salt: salt, db: db, @@ -79,51 +100,14 @@ func (c *ClientCache) Close() error { return fmt.Errorf("fail to init transaction with db to dump cache: %w", err) } - if _, err = tx.Exec("delete from blog_likes;"); err != nil { + if err = batchSave(tx, "blog_likes", c.likePageMap); err != nil { tx.Rollback() - return fmt.Errorf("fail to truncate table blog_likes: %w", err) + return fmt.Errorf("fail to save blog_likes: %s", err) } - userIdBytes := make(map[string][]byte) - - sqlStatement := fmt.Sprintf(` - INSERT OR IGNORE INTO blog_likes (page_ref, user_id) - VALUES %s(?, ?); - `, strings.Repeat("(?, ?), ", 99)) - sqlStatementVars := make([]any, 0, 200) - - for pageRef, userSet := range c.likePageMap { - for userId := range userSet { - if _, ok := userIdBytes[userId]; !ok { - userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) - if err != nil { - slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) - continue - } - } - - sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) - if len(sqlStatementVars) < 200 { - continue - } - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } - - sqlStatementVars = make([]any, 0, 200) - } - } - - if len(sqlStatementVars) > 0 { - sqlStatement = fmt.Sprintf(` - INSERT OR IGNORE INTO blog_likes (page_ref, user_id) - VALUES %s(?, ?); - `, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) - - if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { - slog.Warn("couldn't insert blog like pairs into db", slog.String("error", err.Error())) - } + if err = batchSave(tx, "blog_views", c.viewPageMap); err != nil { + tx.Rollback() + return fmt.Errorf("fail to save blog_views: %s", err) } return tx.Commit() @@ -227,3 +211,94 @@ func (c *ClientCache) LikeOff(id string, page string) (alreadyUnliked bool) { delete(c.likePageMap[page], hash) return false } + +func (c *ClientCache) GetViewStatus(id string, page string) bool { + page = strings.Clone(page) + + mutex := c.getPageMutex(page) + mutex.RLock() + defer mutex.RUnlock() + + if _, ok := c.viewPageMap[page]; !ok { + return false + } + _, ok := c.viewPageMap[page][c.GetHash(id)] + return ok +} + +func (c *ClientCache) GetViewCount(page string) int { + page = strings.Clone(page) + + mutex := c.getPageMutex(page) + mutex.RLock() + defer mutex.RUnlock() + + if userSet, ok := c.viewPageMap[page]; ok { + return len(userSet) + } + return 0 +} + +func (c *ClientCache) View(id string, page string) { + page = strings.Clone(page) + hash := c.GetHash(id) + + mutex := c.getPageMutex(page) + mutex.Lock() + defer mutex.Unlock() + + if _, ok := c.viewPageMap[page]; !ok { + c.viewPageMap[page] = make(map[string]struct{}) + } + c.viewPageMap[page][hash] = struct{}{} +} + +func batchSave(tx *sql.Tx, table string, pageMap map[string]map[string]struct{}) (err error) { + if _, err = tx.Exec(fmt.Sprintf("delete from %s;", table)); err != nil { + return fmt.Errorf("fail to truncate table %s: %w", table, err) + } + + userIdBytes := make(map[string][]byte) + + sqlStatement := fmt.Sprintf(` + INSERT OR IGNORE INTO %s (page_ref, user_id) + VALUES %s(?, ?); + `, table, strings.Repeat("(?, ?), ", 99)) + sqlStatementVars := make([]any, 0, 200) + + for pageRef, userSet := range pageMap { + for userId := range userSet { + if _, ok := userIdBytes[userId]; !ok { + userIdBytes[userId], err = base64.RawStdEncoding.DecodeString(userId) + if err != nil { + slog.Warn("couldn't parse one of user hashes into bytes back", slog.String("hash", userId), slog.String("error", err.Error())) + continue + } + } + + sqlStatementVars = append(sqlStatementVars, any(pageRef), any(userIdBytes[userId])) + if len(sqlStatementVars) < 200 { + continue + } + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) + } + + sqlStatementVars = make([]any, 0, 200) + } + } + + if len(sqlStatementVars) > 0 { + sqlStatement = fmt.Sprintf(` + INSERT OR IGNORE INTO %s (page_ref, user_id) + VALUES %s(?, ?); + `, table, strings.Repeat("(?, ?), ", len(sqlStatementVars)/2-1)) + + if _, err := tx.Exec(sqlStatement, sqlStatementVars...); err != nil { + slog.Warn("couldn't insert blog stat pairs into db", slog.String("table", table), slog.String("error", err.Error())) + } + } + + return nil +} diff --git a/internal/router/lang-blog-title.go b/internal/router/lang-blog-title.go index bafb0d2..d6ca917 100644 --- a/internal/router/lang-blog-title.go +++ b/internal/router/lang-blog-title.go @@ -56,6 +56,7 @@ func Lang_Blog_Title(l map[string]*locale.LocaleConfig, langs []string, b2Client "MapLocationY": y, "MapLocationAreaMeters": areaError, "Lang": lang, + "QueryString": string(c.Request().URI().QueryString()), }) if err != nil { slog.Warn("failed to generate page", slog.String("page", "/"+lang+"/blog/"+c.Params("title")), slog.String("error", err.Error())) diff --git a/internal/router/lang-blog.go b/internal/router/lang-blog.go index dd3f079..38a6597 100644 --- a/internal/router/lang-blog.go +++ b/internal/router/lang-blog.go @@ -76,6 +76,7 @@ func Lang_Blog(l map[string]*locale.LocaleConfig, langs []string, b2Client *b2.B content, err := tm.Render("blog-catalogue", fiber.Map{ "QuerySort": querySort, "QueryTags": strings.Join(queryTags, ","), + "QueryString": string(c.Request().URI().QueryString()), "Tags": tagsArray, "Lang": lang, "L": l[lang], diff --git a/internal/router/page-cache.go b/internal/router/page-cache.go new file mode 100644 index 0000000..01e4792 --- /dev/null +++ b/internal/router/page-cache.go @@ -0,0 +1,5 @@ +package router + +import "github.com/dgraph-io/ristretto/v2" + +var PCache *ristretto.Cache[string, []byte] diff --git a/internal/tailwind/link_renderer.go b/internal/tailwind/link_renderer.go new file mode 100644 index 0000000..ff3d437 --- /dev/null +++ b/internal/tailwind/link_renderer.go @@ -0,0 +1,49 @@ +package tailwind + +import ( + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" +) + +type CustomLinkRenderer struct { + html.Config +} + +func NewCustomLinkRenderer(opts ...html.Option) renderer.NodeRenderer { + r := &CustomLinkRenderer{ + Config: html.NewConfig(), + } + for _, opt := range opts { + opt.SetHTMLOption(&r.Config) + } + return r +} + +func (r *CustomLinkRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { + n := node.(*ast.Link) + if entering { + _, _ = w.WriteString("<a href=\"") + if r.Unsafe || !html.IsDangerousURL(n.Destination) { + _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true))) + } + _, _ = w.WriteString(`"`) + if n.Title != nil { + _, _ = w.WriteString(` title="`) + _, _ = w.Write(util.EscapeHTML(n.Title)) + _, _ = w.WriteString(`"`) + } + if n.Attributes() != nil { + html.RenderAttributes(w, n, nil) + } + _, _ = w.WriteString(">") + } else { + _, _ = w.WriteString("</a>") + } + return ast.WalkContinue, nil +} + +func (r *CustomLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { + reg.Register(ast.KindLink, r.renderLink) +} diff --git a/internal/tailwind/transformer.go b/internal/tailwind/transformer.go index 2f052b9..23ba1a3 100644 --- a/internal/tailwind/transformer.go +++ b/internal/tailwind/transformer.go @@ -1,6 +1,9 @@ package tailwind import ( + "bytes" + "fmt" + "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/text" @@ -17,44 +20,48 @@ func (t *TailwindTransformer) Transform(node *ast.Document, reader text.Reader, switch node := n.(type) { case *ast.Heading: classes := map[int]string{ - 1: "font-patua text-[2.5vmax] font-bold text-main-dark mb-[1.2vmin] tracking-[0.04vmin]", - 2: "font-spectral text-[2vmax] font-bold text-main-dark mb-[0.8vmin] tracking-[0.04vmin]", - 3: "font-spectral text-[1.5vmax] font-medium text-main-dark mb-[0.6vmin] tracking-[0.04vmin]", - 4: "font-spectral text-[1.2vmax] font-medium text-main-medium mb-[0.4vmin] tracking-[0.04vmin]", - 5: "font-spectral text-[1vmax] font-medium text-main-medium mb-[0.4vmin] italic tracking-[0.04vmin]", - 6: "font-spectral text-[1vmax] font-medium text-secondary mb-[0.4vmin]", + 1: "font-patua text-4xl font-bold text-main-dark mb-3 tracking-[.0125rem]", + 2: "font-spectral text-3xl font-bold text-main-dark mb-1 tracking-[.0125rem]", + 3: "font-spectral text-2xl font-medium text-main-dark mb-0.8 tracking-[.0125rem]", + 4: "font-spectral text-xl font-medium text-main-medium mb-0.5 tracking-[.0125rem]", + 5: "font-spectral text-base font-medium text-main-medium mb-0.5 italic tracking-[.0125rem]", + 6: "font-spectral text-base font-medium text-secondary mb-0.5", } if class, ok := classes[node.Level]; ok { node.SetAttribute([]byte("class"), []byte(class)) } case *ast.Paragraph: - node.SetAttribute([]byte("class"), []byte("text-[1vmax]/[2] font-spectral tracking-[0.04vmin] -indent-[2vmax] ml-[2vmax] mb-[1.6vmin]")) + node.SetAttribute([]byte("class"), []byte("text-base/[2] font-spectral tracking-[.0125rem] -indent-8 ml-4 mb-8")) case *ast.List: if node.IsOrdered() { - node.SetAttribute([]byte("class"), []byte("list-decimal list-inside space-y-[0.8vmin] mb-[1.6vmin] pl-[2vmax]")) + node.SetAttribute([]byte("class"), []byte("list-decimal list-inside space-y-2 mb-4 pl-8")) } else { - node.SetAttribute([]byte("class"), []byte("list-disc list-inside space-y-[0.8vmin] mb-[1.6vmin] pl-[2vmax]")) + node.SetAttribute([]byte("class"), []byte("list-disc list-inside space-y-2 mb-4 pl-8")) } case *ast.ListItem: - node.SetAttribute([]byte("class"), []byte("text-[1vmax]/[2] font-spectral tracking-[0.04vmin]")) + node.SetAttribute([]byte("class"), []byte("text-base/[2] font-spectral tracking-[.0125rem]")) case *ast.Blockquote: - node.SetAttribute([]byte("class"), []byte("border-l-[0.4vmin] border-main-medium bg-background-dark p-[0.8vmin] mb-[0.8vmin] italic")) + node.SetAttribute([]byte("class"), []byte("border-l-2 border-main-medium bg-background-dark p-1 mb-2 italic")) case *ast.CodeSpan: node.SetAttribute([]byte("class"), []byte("bg-background-dark")) case *ast.CodeBlock, *ast.FencedCodeBlock: - node.SetAttribute([]byte("class"), []byte("bg-background-dark p-[0.8vmin] rounded-lg overflow-x-auto mb-[1.6vmin]")) + node.SetAttribute([]byte("class"), []byte("bg-background-dark p-1 rounded-lg overflow-x-auto mb-4")) case *ast.Link: + if bytes.HasPrefix(node.Destination, []byte{'.', '/'}) { + onclickAttr := fmt.Appendf(make([]byte, 0, 21+len(node.Destination)), "return changeUrl('%s');", node.Destination) + node.SetAttribute([]byte("onclick"), onclickAttr) + } node.SetAttribute([]byte("class"), []byte("text-secondary hover:text-main-dark underline")) case *ast.Image: - node.SetAttribute([]byte("class"), []byte("max-w-full h-auto rounded-lg shadow-lg mb-[1.6vmin]")) + node.SetAttribute([]byte("class"), []byte("max-w-full h-auto rounded-lg shadow-lg mb-4")) case *ast.Emphasis: switch node.Level { diff --git a/internal/templatemanager/templatemanager.go b/internal/templatemanager/templatemanager.go index 640a706..eebc297 100644 --- a/internal/templatemanager/templatemanager.go +++ b/internal/templatemanager/templatemanager.go @@ -22,13 +22,22 @@ type TemplateManagerTemplates struct { Files []string } +var templateFuncMap = template.FuncMap{ + "contains": strings.Contains, + "iterate": func(count uint) []uint { + items := make([]uint, count) + for i := range count { + items[i] = i + } + return items + }, +} + func NewTemplateManager(templates ...TemplateManagerTemplates) (*TemplateManager, error) { templateMap := make(map[string]templateManagerRender) for _, tmplStruct := range templates { - tmpl := template.New("").Funcs(template.FuncMap{ - "contains": strings.Contains, - }) + tmpl := template.New(tmplStruct.Name).Funcs(templateFuncMap) tmpl, err := tmpl.ParseFiles(tmplStruct.Files...) if err != nil { return nil, err @@ -55,7 +64,11 @@ func (tm *TemplateManager) Render(name string, data any, files ...string) ([]byt if len(files) == 0 { tempTmpl = tmpl.Tmpl } else { - tempTmpl, err = tmpl.Tmpl.ParseFiles(files...) + tempTmpl, err = tmpl.Tmpl.Clone() + if err != nil { + return nil, fmt.Errorf("couldn't clone existing template for rendering: %w", err) + } + tempTmpl, err = tempTmpl.ParseFiles(files...) if err != nil { return nil, fmt.Errorf("couldn't include additional files in template rendering: %w", err) } @@ -71,9 +84,7 @@ func (tm *TemplateManager) Add(name string, files ...string) error { return fmt.Errorf("you can't add template without any files") } - tmpl := template.New("").Funcs(template.FuncMap{ - "contains": strings.Contains, - }) + tmpl := template.New(name).Funcs(templateFuncMap) tmpl, err := tmpl.ParseFiles(files...) if err != nil { diff --git a/locale/localization.en.yaml b/locale/localization.en.yaml index 48ef9da..e93ab00 100644 --- a/locale/localization.en.yaml +++ b/locale/localization.en.yaml @@ -9,4 +9,19 @@ BlogSearch: ChooseAllTags: 'Choose All' GlobalMap: Header: 'Global Map' -LikeButton: 'Like!' +HomePage: + Header: 'Home Page' + Contacts: 'Contacts' + DidYouKnowThat: 'Did you know, that...' + SidebarDescription: 'Describing the Sidebar' + HomePageDescription: 'Home Page -- where we are right now!' + BlogSearchDescription: 'Blog Search. You can find my travel notes there and, maybe, something else!' + MarkerMapDescription: 'Map with markers of my travel notes -- a convinient way to find an interesting region and find related posts!' + ThemeSwitchDescription: 'Theme Palette lets you change the theme of the site -- there are 2 dark and 2 bright themes currently!' + Hymn1: 'We welcome you to Sayasite!' + Hymn2: "We've been expecting you!" + Hymn3: 'You bring such joy in Sayasite!' + Hymn4: 'No matter where you roam, know our love is true!~' +Metadata: + Published: 'Published' + Action: 'Took place on' diff --git a/locale/localization.go b/locale/localization.go index b557e08..7dbb554 100644 --- a/locale/localization.go +++ b/locale/localization.go @@ -11,7 +11,8 @@ type LocaleConfig struct { TagsLabel string `yaml:"TagsLabel" json:"TagsLabel"` BlogSearch BlogSearchConfig `yaml:"BlogSearch" json:"BlogSearch"` GlobalMap GlobalMapConfig `yaml:"GlobalMap" json:"GlobalMap"` - LikeButton string `yaml:"LikeButton" json:"LikeButton"` + HomePage HomePageConfig `yaml:"HomePage" json:"HomePage"` + Metadata MetadataConfig `yaml:"Metadata" json:"Metadata"` } type BlogSearchConfig struct { @@ -28,6 +29,26 @@ type GlobalMapConfig struct { Header string `yaml:"Header" json:"Header"` } +type HomePageConfig struct { + Header string `yaml:"Header" json:"Header"` + Contacts string `yaml:"Contacts" json:"Contacts"` + DidYouKnowThat string `yaml:"DidYouKnowThat" json:"DidYouKnowThat"` + SidebarDescription string `yaml:"SidebarDescription" json:"SidebarDescription"` + HomePageDescription string `yaml:"HomePageDescription" json:"HomePageDescription"` + BlogSearchDescription string `yaml:"BlogSearchDescription" json:"BlogSearchDescription"` + MarkerMapDescription string `yaml:"MarkerMapDescription" json:"MarkerMapDescription"` + ThemeSwitchDescription string `yaml:"ThemeSwitchDescription" json:"ThemeSwitchDescription"` + Hymn1 string `yaml:"Hymn1" json:"Hymn1"` + Hymn2 string `yaml:"Hymn2" json:"Hymn2"` + Hymn3 string `yaml:"Hymn3" json:"Hymn3"` + Hymn4 string `yaml:"Hymn4" json:"Hymn4"` +} + +type MetadataConfig struct { + Action string `yaml:"Action" json:"Action"` + Published string `yaml:"Published" json:"Published"` +} + func LoadConfig(path string, config *LocaleConfig) error { fileBytes, err := os.ReadFile(path) if err != nil { diff --git a/locale/localization.ru.yaml b/locale/localization.ru.yaml index d6e9fe4..a1a9a6c 100644 --- a/locale/localization.ru.yaml +++ b/locale/localization.ru.yaml @@ -9,4 +9,19 @@ BlogSearch: ChooseAllTags: 'Выбрать все' GlobalMap: Header: 'Глобальная карта' -LikeButton: 'Нраица!' +HomePage: + Header: 'Домашняя страница' + Contacts: 'Контакты' + DidYouKnowThat: 'А вы знали, что...' + SidebarDescription: 'Поясняем за сайдбар' + HomePageDescription: 'Домашняя страница -- где мы сейчас и находимся!' + BlogSearchDescription: 'Поиск по блогу. Там размещены мои путевые заметки и, может, что-то ещё!' + MarkerMapDescription: 'Карта с маркерами моих путевых заметок, -- удобный способ найти интересный регион и перейти на заинтересовавшие посты!' + ThemeSwitchDescription: 'Позволяет поменять тему сайта -- есть 2 светлые и 2 тёмные темы!' + Hymn1: 'Приветствуем Вас на Саясайте!' + Hymn2: 'Мы Вас столько ждали!' + Hymn3: 'Столько радости от Вас на Саясайте!' + Hymn4: 'Где бы Вы не были, знайте, -- наша любовь крепка!' +Metadata: + Published: 'Опубликовано' + Action: 'Время действия' @@ -11,17 +11,21 @@ import ( "github.com/SayaAndy/saya-today-web/config" "github.com/SayaAndy/saya-today-web/internal/b2" - "github.com/SayaAndy/saya-today-web/internal/lightgallery" + "github.com/SayaAndy/saya-today-web/internal/factgiver" + "github.com/SayaAndy/saya-today-web/internal/glightbox" "github.com/SayaAndy/saya-today-web/internal/router" "github.com/SayaAndy/saya-today-web/internal/tailwind" "github.com/SayaAndy/saya-today-web/locale" + "github.com/dgraph-io/ristretto/v2" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/redirect" + "github.com/gofiber/fiber/v2/middleware/cors" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" - gmhtml "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/renderer" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/util" _ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/mattn/go-sqlite3" @@ -30,14 +34,20 @@ import ( var ( md = goldmark.New( goldmark.WithExtensions( - lightgallery.NewLightGalleryExtension(), + glightbox.NewGLightboxExtension(), tailwind.NewTailwindExtension(), ), goldmark.WithParserOptions( parser.WithAutoHeadingID(), + parser.WithAttribute(), ), - goldmark.WithRendererOptions( - gmhtml.WithXHTML(), + goldmark.WithRenderer( + renderer.NewRenderer( + renderer.WithNodeRenderers( + util.Prioritized(tailwind.NewCustomLinkRenderer(html.WithUnsafe(), html.WithXHTML()), 50), + util.Prioritized(html.NewRenderer(html.WithXHTML()), 100), + ), + ), ), ) b2Client *b2.B2Client @@ -106,28 +116,49 @@ func main() { ProxyHeader: "X-Forwarded-For", }) - app.Use(redirect.New(redirect.Config{ - Rules: map[string]string{ - "/ru": "/", - "/en": "/", - }, - StatusCode: 301, + app.Use(cors.New(cors.Config{ + AllowOrigins: "https://f003.backblazeb2.com", + AllowMethods: "GET,POST,OPTIONS", + AllowHeaders: "Origin, Content-Type, Accept", })) router.CCache, err = router.NewClientCache(db, []byte(cfg.Auth.Salt)) if err != nil { - slog.Error("fail to initialize cache", slog.String("error", err.Error())) + slog.Error("fail to initialize client cache", slog.String("error", err.Error())) + os.Exit(1) + } + + router.PCache, err = ristretto.NewCache(&ristretto.Config[string, []byte]{ + NumCounters: 1e6, // 1,000,000 + MaxCost: 1 << 29, // 512 MB + BufferItems: 64, // number of keys per Get buffer. + }) + if err != nil { + slog.Error("fail to initialize page cache", slog.String("error", err.Error())) + os.Exit(1) + } + + router.FactGiver, err = factgiver.NewFactGiver(&cfg.FactGiver, availableLanguages) + if err != nil { + slog.Error("fail to initialize fact giver", slog.String("error", err.Error())) os.Exit(1) } app.Get("/", router.Root(cfg.AvailableLanguages)) + app.Get("/:lang<len(2)>", router.Api_V1_GeneralPage(localization, availableLanguages)) app.Get("/:lang/map", router.Lang_Map(localization, availableLanguages, b2Client)) - app.Get("/:lang/blog", router.Lang_Blog(localization, availableLanguages, b2Client)) - app.Get("/:lang/blog/:title", router.Lang_Blog_Title(localization, availableLanguages, b2Client, md)) + app.Get("/:lang/blog", router.Api_V1_GeneralPage(localization, availableLanguages)) + app.Get("/:lang/blog/:title", router.Api_V1_GeneralPage(localization, availableLanguages)) + app.Get("/api/v1/tz", router.Api_V1_TZ()) app.Get("/api/v1/blog-search", router.Api_V1_BlogSearch(localization, availableLanguages, b2Client)) app.Get("/api/v1/like", router.Api_V1_Like_Get(localization, b2Client)) app.Put("/api/v1/like", router.Api_V1_Like_Put(localization, b2Client)) + app.Get("/api/v1/general-page/header", router.Api_V1_GeneralPage_Header(localization, availableLanguages, b2Client)) + app.Get("/api/v1/general-page/body", router.Api_V1_GeneralPage_Body(localization, availableLanguages, b2Client, md)) + app.Get("/api/v1/general-page/footer", router.Api_V1_GeneralPage_Footer(localization, availableLanguages)) + app.Get("/api/v1/general-page/top-embeds", router.Api_V1_GeneralPage_TopEmbeds(localization, availableLanguages)) + app.Get("/api/v1/general-page/bottom-embeds", router.Api_V1_GeneralPage_BottomEmbeds(localization, availableLanguages, b2Client)) app.Static("/", "./static") @@ -152,5 +183,6 @@ func main() { if err = db.Close(); err != nil { slog.Error("fail to close db connection", slog.String("error", err.Error())) } + router.PCache.Close() db.Close() } diff --git a/static/input.css b/static/input.css index a64c59d..d03764b 100644 --- a/static/input.css +++ b/static/input.css @@ -3,6 +3,35 @@ @theme { --font-spectral: Spectral, serif; --font-patua: "Patua One", cursive; + --breakpoint-xs: 32rem; +} + +html { + font-size: 12px; +} + +@media screen and (min-width: 640px) and (min-height: 640px) { + html { + font-size: 14px; + } +} + +@media screen and (min-width: 900px) and (min-height: 900px) { + html { + font-size: 16px; + } +} + +@media screen and (min-width: 1400px) and (min-height: 1400px) { + html { + font-size: 20px; + } +} + +@media screen and (min-width: 1920px) and (min-height: 1920px) { + html { + font-size: 24px; + } } [data-theme="olive"] { @@ -10,6 +39,7 @@ --main-medium-color: #8a9d8a; --main-light-color: #a5b8a5; --background-dark-color: #ffffdd; + --background-medium-color: var(--background-dark-color); --background-light-color: #fffff0; --sidebar-text-color: var(--background-dark-color); --sidebar-stroke-color: var(--main-dark-color); @@ -19,7 +49,7 @@ --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23ffffdb%22%2F%3E%3Cpath%20fill%3D%22%23777e63%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23a0a182%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; - --interlocked-hexagons-background-size: 10vh; + --interlocked-hexagons-background-size: 10dvh; --squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%234a5139%22%2F%3E%3Cpath%20fill%3D%22%23b2b285%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E"); --squares-and-triangles-background-repeat: repeat; --squares-and-triangles-background-width-coef: 10; @@ -31,6 +61,7 @@ --main-medium-color: #9bb665; --main-light-color: #bbd095; --background-dark-color: #f5eee2; + --background-medium-color: var(--background-dark-color); --background-light-color: #fff9ef; --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-dark-color); @@ -40,7 +71,7 @@ --leaflet-link-hover-color: var(--main-light-color); --interlocked-hexagons-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23f5ede1%22%2F%3E%3Cpath%20fill%3D%22%239ab663%22%20d%3D%22m33.64%200-.17.25h-33L4.3%206h33.07l4-6zm8.33%200-4.33%206.5h-33l3.83%205.75h33.06L49.7%200zm8.34%200-8.5%2012.75h-33l3.83%205.75H45.7L58.03%200zm8.33%200L45.97%2019h-33l3.83%205.75h33.06L66.36%200zm-8.5%2025.25L33.63%2050l3.86%205.8L54.03%2031h33l-3.83-5.75zm4.16%206.25L37.8%2056.25l3.87%205.8%2016.53-24.8h33l-3.84-5.75zm4.17%206.25L41.97%2062.5l3.86%205.8%2016.54-24.8h33l-3.84-5.75zM62.63%2044l-16.5%2024.75%203.87%205.8%2016.53-24.8h33L95.7%2044zM100%2050.45l-3.87%205.8%203.87%205.81v-11.6zM0%2050.46v11.6L12.5%2080.8l3.86-5.8zm95.83%206.25-3.87%205.8L100%2074.55v-11.6zm-4.17%206.24-3.86%205.8%2012.2%2018.3v-11.6zM0%2062.96v11.6l8.33%2012.49%203.86-5.8zm87.5%206.24L83.63%2075%20100%2099.54V87.96zM50%2075.45%2033.64%20100h7.72l12.5-18.75zm-50%200v11.6l4.16%206.24%203.87-5.8zm54.17%206.25L41.97%20100h7.73l8.33-12.5zm4.17%206.25L50.3%20100h7.73l4.17-6.25zM0%2087.95v11.59l3.86-5.8zm62.5%206.25-3.87%205.8h7.73z%22%2F%3E%3Cpath%20fill%3D%22%23bbd095%22%20d%3D%22M66.8.25%2062.97%206h33l4.03%206.05V.45l-.14-.2zM0%20.45v11.61L12.5%2030.8l3.86-5.8zM62.63%206.5l-3.83%205.75h33l8.2%2012.3v-11.6L95.7%206.5zm-4.16%206.25-3.84%205.75h33L100%2037.06v-11.6l-8.47-12.71zM0%2012.96v11.6l8.33%2012.49%203.86-5.8zM54.3%2019l-3.84%205.75h33L100%2049.55l.01-.01V37.96L87.36%2019zm-4.77%206.25H41.8L25.3%2050l16.66%2025-16.5%2024.75h7.74L49.7%2075%2033.03%2050zm-8.33%200h-7.74L16.96%2050l16.67%2025-16.5%2024.75h7.73L41.36%2075%2024.7%2050Zm-8.34%200h-7.73L8.63%2050%2025.3%2075%208.8%2099.75h7.73L33.03%2075%2016.36%2050zm-16.07%200L.3%2050l16.67%2025L.46%2099.75H8.2L24.7%2075%208.02%2050l16.5-24.75zM0%2025.46v11.6l4.16%206.24%203.87-5.8zm0%2012.5v11.58l3.86-5.79zm66.8%2012.29L50.3%2074.99l3.87%205.81%204.16%206.25%204.17%206.25%204.3%206.45h7.73L58.03%2075l16.5-24.75zm16.06%200h-7.73L58.63%2075l16.5%2024.75h7.73L66.36%2075zm.6%200L66.96%2075l16.5%2024.75h7.74L74.7%2075l16.5-24.75zm16.07%200H91.8L75.3%2075l16.5%2024.75h7.73L83.03%2075z%22%2F%3E%3C%2Fsvg%3E"); --interlocked-hexagons-background-repeat: repeat; - --interlocked-hexagons-background-size: 10vh; + --interlocked-hexagons-background-size: 10dvh; --squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%22%20height%3D%22100%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%23bbd095%22%2F%3E%3Cpath%20fill%3D%22%23f5ede1%22%20d%3D%22M0%200v6.77L6.72%200zm8.91%200L0%208.96v3.86L12.83%200zm6.11%200L0%2015.06v3.87L18.9%200zm6.11%200L0%2021.12v3.92L25%200zM25%200l25%2024.99v-3.87L28.87%200zm25%2024.99%2025%2024.98L100%2025%2075%200zm-50%20.05v3.87l21.13%2021.12H25l-25-25zm25%2024.99h3.87L50%2028.9v-3.87zm0%200L0%2075l25%2025%2025-24.99zM50%2075l25-24.98h-3.87L50%2071.14v3.87zm0%200v3.87L71.13%20100H75L50%2075.01zm25%2025h3.87L100%2078.88v-3.87zm25-24.99v-3.87L78.87%2050.03H75L100%2075zM31.1%200%2050%2018.93v-3.87L34.98%200h-3.87zm6.07%200L50%2012.82V8.96L41.04%200zm6.1%200L50%206.77V0h-6.72zM25%202.19%202.14%2025.04H25zm0%2022.85v22.8l22.81-22.8zM75%203.92l21.13%2021.12L75%2046.16%2053.87%2025.04zm0%202.19L56.06%2025.04l18.89%2018.88%2018.94-18.88L75%206.1zm0%203.92%2015.07%2015L75%2040.06%2059.98%2025.04zm0%202.18L62.17%2025.04%2075%2037.86l12.83-12.82L75%2012.2zm0%203.82%208.96%209L75%2033.95%2066.04%2025zm0%202.24-6.72%206.77L75%2031.76l6.77-6.72h-.05zM0%2031.09v3.87l15.02%2015.07h3.87zm50%200L31.1%2050.03h3.88L50%2034.96V31.1zM0%2037.2v3.87l8.91%208.96h3.92zm50%200L37.17%2050.03h3.87L50%2041.07zM0%2043.3v6.73h6.72zm50%200-6.72%206.73H50zm0%206.73v6.71l6.72-6.71zm8.96%200L50%2058.93v3.92l12.83-12.82zm6.06%200L50%2065.03v3.88l18.94-18.88zm16.09%200L100%2068.9v-3.87l-15.02-15zm6.11%200L100%2062.85v-3.92l-8.91-8.9zm6.06%200%206.72%206.71v-6.71zM75%2052.2%2052.19%2075H75zM75%2075v22.8L97.81%2075zM25%2053.9l21.13%2021.12L25%2096.13%203.87%2075.01zm0%202.2L6.1%2074.95l18.95%2018.88%2018.9-18.88L25%2056.08zm0%203.9%2015.02%2015.01L25%2090.03%209.98%2075zm0%202.19L12.17%2075%2025%2087.84%2037.83%2075%2025%2062.2zm0%203.87L33.96%2075%2025%2083.97%2016.04%2075zm0%202.23-6.72%206.72L25%2081.73l6.72-6.72L25%2068.3zm25%2012.83v3.87L65.02%20100h3.87zm50%200L81.1%20100h3.88L100%2084.99zm-50%206.06v3.86l8.96%208.96h3.87zm50%200L87.22%20100h3.87l8.91-8.96zm-50%206.1V100h6.72zm50%200L93.28%20100H100z%22%2F%3E%3C%2Fsvg%3E"); --squares-and-triangles-background-repeat: repeat; --squares-and-triangles-background-width-coef: 10; @@ -52,6 +83,7 @@ --main-medium-color: var(--color-sky-200); --main-light-color: var(--color-sky-300); --background-dark-color: #13131b; + --background-medium-color: var(--background-dark-color); --background-light-color: #222230; --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-light-color); @@ -61,7 +93,7 @@ --leaflet-link-hover-color: var(--background-light-color); --interlocked-hexagons-background: radial-gradient(33.5% 25% at 100% 82%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(54% 25% at 55% 100%, rgba(190, 227, 248, 0.2) 0%, rgba(197, 179, 246, 0.13) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(37% 19.5% at 20% 100%, rgba(176, 220, 248, 0.3) 0%, rgba(197, 172, 247, 0.2) 33.33%, rgba(255, 0, 0, 0) 100%), radial-gradient(63% 34.5% at 66% 77%, rgba(190, 227, 248, 0.15) 0%, rgba(197, 179, 246, 0.1) 33.33%, rgba(255, 0, 0, 0) 100%), linear-gradient(0deg, #2f5883 0%, #2f4e7a 4.75%, #2e4472 9.5%, #2a3065 19%, #272e57 26.5%, #242b4b 34%, #1f2437 49%, #1c2030 57.75%, #1a1b29 66.5%, #13131b 84%); --interlocked-hexagons-background-repeat: no-repeat; - --interlocked-hexagons-background-size: 100vw 100vh; + --interlocked-hexagons-background-size: 100dvw 100dvh; --squares-and-triangles-background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2260%22%20height%3D%2260%22%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20fill%3D%22%2313131b%22%2F%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%23363645%22%20stroke-width%3D%22.5%22%20d%3D%22M10%2050H0m60-40H50m0%2040v10M10%200v10m21%2030v20m0-60v20m9%2010h20M0%2030h20m0%2010h20V20H20v10zM10%2010h40v40H10zM0%200h60v60H0z%22%2F%3E%3C%2Fsvg%3E"); --squares-and-triangles-background-repeat: repeat; --squares-and-triangles-background-width-coef: 5; @@ -73,6 +105,7 @@ --main-medium-color: #f6e4c5; --main-light-color: #e8d4b8; --background-dark-color: #413b34; + --background-medium-color: #62584f; --background-light-color: #6d6258; --sidebar-text-color: var(--main-dark-color); --sidebar-stroke-color: var(--background-light-color); @@ -89,12 +122,12 @@ --squares-and-triangles-background-height-coef: 15; } -a { +a, .linklike { color: var(--main-medium-color) !important; text-decoration: underline; } -a:hover { +a:hover, .linklike:hover { color: var(--main-light-color) !important; } @@ -203,74 +236,34 @@ a:hover { border-style: outset; } -.desktop-sidebar-custom { - width: 5vw; - background-size: calc(var(--squares-and-triangles-background-size) * 1vw); -} - .bg-sidebar { background-image: var(--squares-and-triangles-background); background-repeat: var(--squares-and-triangles-background-repeat); - background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw); -} - -.mobile-topbar-custom { - width: 90vw; - height: 8%; - background-size: calc(var(--squares-and-triangles-background-size) * 1.6vh); - margin-left: 5vw; -} - -body:has(.lg-container:not([class~="lg-inline"])) #sidebar { - display: none !important; + background-size: calc(var(--squares-and-triangles-background-width-coef) * 0.8rem) calc(var(--squares-and-triangles-background-height-coef) * 0.8rem); } .logo-custom { - width: 3.5vw; - height: 20vw; - font-size: 2.5vw; - -webkit-text-stroke: 0.1vw var(--sidebar-stroke-color); + width: 3.5rem; + height: 20rem; + font-size: 2.5rem; + -webkit-text-stroke: 0.1rem var(--sidebar-stroke-color); writing-mode: sideways-lr; text-orientation: mixed; - display: block; -} - -.nav-icon-custom { - font-size: 3vw !important; - -webkit-text-stroke: 0.1vw var(--sidebar-stroke-color); - width: 5vw; - height: 5vw; - align-content: center; - text-align: center; -} - -.nav-icon-custom:hover { - font-size: 4vw; -} - -.border-left-custom { - width: 0.5vw; - border-right: 0.3vh dashed var(--main-dark-color); -} - -.border-right-custom { - width: 0.5vw; - border-left: 0.5vh dotted var(--main-dark-color); } .inset-shadow { - box-shadow: inset 0vh 1vh 1vh -1vh rgba(75, 81, 58, 0.5), - inset 1vh 0vh 1vh -1vh rgba(75, 81, 58, 0.5), - inset -1vh 0vh 1vh -1vh rgba(75, 81, 58, 0.5), - inset 0vh -0.5vh rgba(75, 81, 58, 0.5); + box-shadow: inset 0dvh 1dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), + inset 1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), + inset -1dvh 0dvh 1dvh -1dvh rgba(75, 81, 58, 0.5), + inset 0dvh -0.5dvh rgba(75, 81, 58, 0.5); } .text-stroke-\(--sidebar-stroke-color\) { -webkit-text-stroke-color: var(--sidebar-stroke-color); } -.text-stroke-\[0\.1vw\] { - -webkit-text-stroke-width: 0.1vw; +.text-stroke-\[0\.1dvw\] { + -webkit-text-stroke-width: 0.1dvw; } .theme-wheel { @@ -297,15 +290,15 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { .theme-icon { position: absolute; - width: 4vmax; - height: 4vmax; + width: 4rem; + height: 4rem; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; - border: 0.1vmax inset var(--background-light-color); + border: 0.1rem inset var(--background-light-color); background: var(--interlocked-hexagons-background); background-size: contain; } @@ -316,7 +309,7 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { .theme-icon.active { transform: scale(1.2); - border: 0.3vmax outset var(--main-light-color); + border: 0.3rem outset var(--main-light-color); } .custom-svg-marker { @@ -333,143 +326,148 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { transform: scale(1.1); } -.lg-backdrop { - background-color: var(--background-light-color); - opacity: 0.8 !important; -} - -.lg-container:not([class~="lg-inline"]) .lg-outer { - width: 90%; - height: 90%; - margin: 5vh 5vw; +.grid-tooltip { + position: relative; + display: inline-block; + box-shadow: inset 0rem 0rem 0.5rem var(--main-light-color); + transition: box-shadow 0.5s ease-in-out; } -.lg-thumb-outer { - background-color: var(--background-light-color) !important; - opacity: 0.8 !important; +.grid-tooltip:hover { + box-shadow: none; } -.lg-prev { - background-color: var(--background-light-color); +.grid-tooltip-text { + visibility: hidden; + opacity: 0; + transition: opacity 0.5s; + width: 100%; + top: 100%; + background-color: var(--background-dark-color); color: var(--main-dark-color); + text-align: center; + padding: 0.25rem 0.25rem; + margin-top: 0.25rem; + border-radius: 0.5rem; + position: absolute; + z-index: 15; } -.lg-next { - background-color: var(--background-light-color); - color: var(--main-dark-color); +.grid-tooltip:hover .grid-tooltip-text { + visibility: visible; + opacity: 1; } -.lg-counter { - color: var(--main-dark-color); - font-family: 'Spectral'; - font-size: 1vmax; - padding-top: 0; - height: 1vmax; +.grid-tooltip-text::after { + content: " "; + position: absolute; + bottom: 100%; + left: 50%; + margin-left: -0.25rem; + border-width: 0.25rem; + border-style: solid; + border-color: transparent transparent var(--background-dark-color) transparent; } -.lg-container:not([class~="lg-inline"]) .lg-counter { - font-size: 2vmax; - height: 2vmax; +.grid-item, .grid-sizer { + position: relative; + display: inline-block; + width: 50%; } -.lg-content { - top: 2.5vmax !important; - bottom: calc(10vmin + 5vmax) !important; +.grid-item-index { + visibility: visible; + opacity: 1; + transition: opacity 0.5s; + top: 0%; + left: 0%; + background-color: var(--main-light-color); + color: var(--background-dark-color); + text-align: center; + padding: 0.25rem 0.25rem; + position: absolute; + z-index: 14; } -.lg-outer[class~="lg-single-item"] .lg-content { - bottom: 5vmax !important; +.grid-item:hover .grid-item-index { + visibility: hidden; + opacity: 0; } -.lg-thumb-item { - width: 10vmin !important; - height: 10vmin !important; +.grid-item-2x { + width: 100%; } -.lg-container:not([class~="lg-inline"]) .lg-content { - top: 4vmax !important; -} +@container main (width >= 32rem) { + .grid-item, .grid-sizer { + width: 33%; + } -.lg-toolbar .lg-icon, -.lg-content .lg-icon { - padding: 0 0.7vmax; - font-size: 1.5vmax !important; - height: 1.5vmax; - width: auto; - color: var(--main-dark-color); + .grid-item-2x { + width: 66%; + } } -.lg-container:not([class~="lg-inline"]) .lg-toolbar .lg-icon, -.lg-container:not([class~="lg-inline"]) .lg-content .lg-icon { - font-size: 3vmax !important; - height: 3vmax; -} +@container main (width >= 48rem) { + .grid-item, .grid-sizer { + width: 25%; + } -.lg-prev { - left: 0 !important; + .grid-item-2x { + width: 50%; + } } -.lg-next { - right: 0 !important; -} +@container main (width >= 64rem) { + .grid-item, .grid-sizer { + width: 20%; + } -.lg-toolbar .lg-icon:hover { - color: var(--main-medium-color); + .grid-item-2x { + width: 40%; + } } -.lg-toolbar .lg-icon { - font-size: 1.5vmax !important; - color: var(--main-dark-color); +.goverlay { + background: color-mix(in srgb, var(--background-light-color) 50%, transparent) !important; + backdrop-filter: blur(1rem); } -.lg-content .lg-icon:hover { - color: var(--main-medium-color); +.glightbox-clean .gslide-description { + background: color-mix(in srgb, var(--background-dark-color) 50%, transparent) !important; + backdrop-filter: blur(1rem); } -.lg-sub-html { - padding: 0.7vmin 1.25vmin !important; +.glightbox-clean .gslide-title { + font-family: var(--font-spectral) !important; + font-weight: 600 !important; + color: var(--main-light-color) !important; + padding-bottom: 1rem !important; + margin-bottom: 0 !important; } -.inline-gallery-container { - width: 80%; - height: 70vmin; +.glightbox-clean .gslide-desc { + font-size: 1rem !important; + font-family: var(--font-spectral) !important; + font-weight: 400; + color: var(--main-medium-color) !important; } -.light-gallery-captions { - p { - transform: translate3d(0, 0, 0px); - transition-delay: 500ms; - opacity: 1; - } +.glightbox-mobile .glightbox-container .gslide-description { + padding: 0 !important; } -.lg-slide-progress { - .light-gallery-captions { - h4 { - transform: translate3d(-60px, 0, 0px); - } - - p { - transform: translate3d(60px, 0, 0px); - } - - h4, - p { - opacity: 0; - } - } +.glightbox-clean .gdesc-inner { + padding: 1rem 1rem !important; } -.lg-current { - &.lg-slide-progress { - .light-gallery-captions { +.glightbox-container .ginner-container { + height: 100dvh !important; +} - h4, - p { - transition-delay: 0ms; - } - } - } +.gslide-description { + flex-grow: 0; + flex-shrink: 1; } .map-container .leaflet-container { @@ -483,28 +481,29 @@ body[data-theme~="ram"] .leaflet-tile { } @media (min-aspect-ratio: 0.8/1) { - .ar-gt-0\.8\:mr-\[1\.2vmin\] { - margin-right: 1.2vmin; + .ar-gt-0\.8\:mr-6 { + margin-right: 1.5rem; } .ar-gt-0\.8\:w-fit { width: fit-content; } - .ar-gt-0\.8\:min-w-fit { - min-width: fit-content; + .ar-gt-0\.8\:w-\[100dvh\] { + width: 100dvh; + flex-basis: 100dvh; } - .ar-gt-0\.8\:min-w-\[10vh\] { - min-width: 10vh; + .ar-gt-0\.8\:min-w-fit { + min-width: fit-content; } - .ar-gt-0\.8\:max-w-\[20vh\] { - max-width: 20vh; + .ar-gt-0\.8\:min-w-\[10dvh\] { + min-width: 10dvh; } - .ar-gt-0\.8\:max-w-\[100vh\] { - max-width: 100vh; + .ar-gt-0\.8\:max-w-\[20dvh\] { + max-width: 20dvh; } .ar-gt-0\.8\:items-center { @@ -514,32 +513,15 @@ body[data-theme~="ram"] .leaflet-tile { @media (max-aspect-ratio: 0.8/1) { .bg-sidebar { - background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6vh) calc(var(--squares-and-triangles-background-height-coef) * 1.6vh); + background-size: calc(var(--squares-and-triangles-background-width-coef) * 1.6dvh) calc(var(--squares-and-triangles-background-height-coef) * 1.6dvh); } - .inline-gallery-container { - width: 100%; - } - - .logo-custom { - display: none; - } - - .nav-icon-custom { - font-size: 5vh !important; - -webkit-text-stroke: 0.1vh var(--sidebar-stroke-color); - width: 8vh; - height: 8vh; - align-content: center; - text-align: center; - } - - .nav-icon-custom:hover { - font-size: 7vh; + .ar-lt-0\.8\:bg-background-light { + background-color: var(--background-light-color); } .bg-pattern-body { - background-size: 10vh; + background-size: 10dvh; } .ar-lt-0\.8\:grid { @@ -554,56 +536,52 @@ body[data-theme~="ram"] .leaflet-tile { flex-direction: row; } - .ar-lt-0\.8\:h-\[8vh\] { - height: 8vh; + .ar-lt-0\.8\:h-\[8dvh\] { + height: 8dvh; } - .ar-lt-0\.8\:h-\[10vh\] { - height: 10vh; + .ar-lt-0\.8\:h-\[10dvh\] { + height: 10dvh; } - .ar-lt-0\.8\:h-\[30vh\] { - height: 30vh; + .ar-lt-0\.8\:h-\[30dvh\] { + height: 30dvh; } - .ar-lt-0\.8\:h-\[90vh\] { - height: 90vh; + .ar-lt-0\.8\:h-\[90dvh\] { + height: 90dvh; } - .ar-lt-0\.8\:w-\[8vh\] { - width: 8vh; + .ar-lt-0\.8\:w-\[8dvh\] { + width: 8dvh; } .ar-lt-0\.8\:w-\[80\%\] { width: 80%; } - .ar-lt-0\.8\:w-\[90vw\] { - width: 90vw; + .ar-lt-0\.8\:w-\[90dvw\] { + width: 90dvw; } - .ar-lt-0\.8\:w-screen { - width: 100vw; + .ar-lt-0\.8\:w-\[100dvw\] { + width: 100dvw; } - .ar-lt-0\.8\:max-h-\[25\%\] { - max-height: 25%; + .ar-lt-0\.8\:max-h-\[30\%\] { + max-height: 30%; } .ar-lt-0\.8\:max-h-\[80\%\] { max-height: 80%; } - .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] { - max-height: calc(100% - 25vh); - } - - .ar-lt-0\.8\:text-\[5vh\] { - font-size: 5vh; + .ar-lt-0\.8\:max-h-\[92dvh\] { + max-height: 92dvh; } - .ar-lt-0\.8\:ml-\[5vw\] { - margin-left: 5vw; + .ar-lt-0\.8\:text-\[5dvh\] { + font-size: 5dvh; } .ar-lt-0\.8\:hidden { @@ -611,16 +589,30 @@ body[data-theme~="ram"] .leaflet-tile { } } -@media (max-aspect-ratio: 0.4/1) { - .ar-lt-0\.4\:w-\[100vw\] { - width: 100vw; +@media (min-aspect-ratio: 0.6/1) { + .ar-gt-0\.6\:flex-3\/5 { + flex: 60%; } - .ar-lt-0\.4\:ml-0 { - margin-left: 0; + .ar-gt-0\.6\:flex-2\/5 { + flex: 40%; } } +@media (max-aspect-ratio: 0.6/1) { + .ar-lt-0\.6\:flex-col { + flex-direction: column; + } +} + +.home-page-heart { + position: absolute; + color: var(--color-pink-700); + user-select: none; + pointer-events: none; + font-family: serif; +} + .night-star { color: var(--color-yellow-100); animation-name: blink; diff --git a/views/index.html b/views/index.html index 2b8c937..2633894 100644 --- a/views/index.html +++ b/views/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>SAYA TODAY // Заглавная</title> + <title>Choose Language // SAYA TODAY</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> @@ -12,8 +12,8 @@ <style> .menu-outer-body { border-radius: 10%; - width: 40vh; - height: 30vh; + width: 40dvh; + height: 30dvh; } .menu-inner-body { @@ -35,8 +35,8 @@ } .menu-outer-body { - width: min(40vh, 100vw); - height: 40vh; + width: min(40dvh, 100dvw); + height: 40dvh; border-radius: 0; } @@ -56,7 +56,7 @@ </style> </head> -<body data-theme="night" class="font-spectral text-main-dark text-[1.5vmax] overflow-hidden bg-interlocked-hexagons h-screen flex flex-col"> +<body data-theme="night" class="font-spectral text-main-dark text-2xl overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-col"> <div class="absolute star z-0">*</div> <div class="absolute star z-0">*</div> @@ -102,14 +102,14 @@ switchTheme(savedTheme); </script> - <div class="menu-outer-body p-4 z-1 bg-background-dark flex flex-col mx-auto my-auto opacity-70"> - <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[0.8vmin]">saya.today</p> + <div class="menu-outer-body p-2 z-1 bg-background-dark flex flex-col mx-auto my-auto opacity-70"> + <p class="font-patua mx-auto text-4xl mt-1 mb-2">saya.today</p> <div class="menu-inner-body grow bg-background-light flex flex-adaptive mx-auto opacity-100"> {{- range .AvailableLanguages }} - <img onclick="location.href='/{{ .Name }}/blog';" class="object-scale-down cursor-pointer flex-1 rounded-[20%] language-icon mx-auto my-auto relative" src="{{ .Flag }}" alt="{{ .Alt }}"> + <img onclick="location.href='/{{ .Name }}';" class="object-scale-down cursor-pointer flex-1 rounded-[20%] language-icon mx-auto my-auto relative" src="{{ .Flag }}" alt="{{ .Alt }}"> {{- end }} </div> - <p class="font-spectral mx-auto text-[1.5vmax] text-secondary mt-[0.8vmin] mb-[0.8vmin]">Choose your language</p> + <p class="font-spectral mx-auto text-2xl text-secondary mt-1 mb-2">Choose your language</p> </div> <script> diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index 675b6e9..ce5698b 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -3,22 +3,37 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>SAYA TODAY // {{ .Title }}</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,200;1,300;1,400;1,500;1,600;1,700;1,800&family=Patua+One&display=swap" rel="stylesheet"> - {{ block "top-embeds" . }}{{ end }} - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lightgallery.min.css"> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-zoom.min.css"> - <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightgallery/2.8.3/css/lg-thumbnail.min.css"> - {{- if .MapLocationX }} <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.css"> - {{- end }} + <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/glightbox/3.3.0/css/glightbox.min.css"> <link href="/output.css" rel="stylesheet"> </head> -<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> +<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-row ar-lt-0.8:flex-col"> + + <script> + let map = null; + </script> + + <script> + function changeUrl(path, toAppend = false) { + const urlParams = new URLSearchParams(window.location.search); + const newPathname = location.pathname.endsWith('/') ? location.pathname.slice(0, -1) : location.pathname; + const newPath = toAppend ? `${newPathname}${path}` : `${path}`; + + history.pushState({}, '', `${newPath}${urlParams.entries.length == 0 ? '' : '?' + urlParams.toString()}`); + if (location.pathname == "" || location.pathname == "/") { + return true; + } + window.dispatchEvent(new Event('popstate')); + return false; + }; + </script> + + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/glightbox/3.3.0/js/glightbox.min.js"></script> <script> function sfc32(a, b, c, d) { @@ -146,23 +161,33 @@ switchTheme(savedTheme); </script> - <script> - const galleryMap = new Map(); - </script> + <div id="general-page-top-embeds" class="flex" + hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML"> + </div> + <div id="general-page-top-embeds-trigger" hx-get="/api/v1/general-page/top-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-top-embeds" hx-swap="innerHTML"></div> <script> const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; </script> - <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> - <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> + <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5rem] ar-lt-0.8:w-[100dvw] h-[100dvh] ar-lt-0.8:h-[8dvh]"> + <div class="logo-custom block ar-lt-0.8:hidden [@media(max-height:32rem)]:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> saya.today </div> - <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> + <div class="text-[3rem] ar-lt-0.8:text-[5dvh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> + <a href="/{{ .Lang }}" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i onclick="return changeUrl('/{{ .Lang }}');" class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <a href="/{{ .Lang }}/blog" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i onclick="return changeUrl('/{{ .Lang }}/blog');" class="fas fa-search flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <a href="/{{ .Lang }}/map" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook flex w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i> + <a href="./" class="hidden max-xs:block [@media(max-height:32rem)]:block w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i onclick="return changeUrl('/../', true);" class="fas fa-circle-left flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> </div> <div class="theme-wheel" id="theme-wheel"> <div class="theme-icon" data-theme="olive"></div> @@ -172,47 +197,42 @@ </div> </div> - <div class="bg-background-dark flex z-10 ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> - <div class="pl-[0.5vw] bg-background-dark border-r-[0.4vmin] border-dashed border-main-dark"></div> + <div class="bg-background-dark @container/main flex z-10 ar-lt-0.8:w-[100dvw] w-[90dvw] xs:w-[85dvw] sm:w-[80dvw] md:w-[75dvw] lg:w-[70dvw] xl:w-[65dvw] 2xl:w-[60dvw] grow-0 h-[100dvh] ar-lt-0.8:max-h-[92dvh]"> + <div class="bg-background-dark border-r-2 border-dashed border-main-dark"></div> - <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative pt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> - <div id="general-page-header" class="flex flex-col"> - <div {{ if .PublishedDate }} hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="#published-date" hx-swap="innerHTML" hx-trigger="load" {{ end }} - class="flex flex-row mb-[0.4vmin]"> - <i onclick="location.pathname += location.pathname.endsWith('/') ? '../' : '/../';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> - <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> - <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p> - <p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> - </div> - {{ block "header" . }}{{ end }} + <div class="max-h-100% flex flex-col grow bg-background-dark relative pt-4 ml-8 pr-4"> + <div id="general-page-header" class="max-xs:hidden [@media(max-height:32rem)]:hidden flex flex-col basis-8" + hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML"> </div> + <div id="general-page-header-trigger" hx-get="/api/v1/general-page/header{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-header" hx-swap="innerHTML"></div> - <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + <hr class="max-xs:hidden [@media(max-height:32rem)]:hidden border-t-4 border-dotted border-main-dark mb-2"> - <div id="general-page-body" class="flex flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] grow"> - {{ block "body" . }}{{ end }} + <div id="general-page-body" class="flex flex-col grow overflow-y-auto" + hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML"> </div> + <div id="general-page-body-trigger" hx-get="/api/v1/general-page/body{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-body" hx-swap="innerHTML"></div> - <div id="general-page-footer" class="flex flex-row mt-[0.8vmin] mb-[0.4vmin]"> - {{ block "footer" . }}{{ end }} - <p class="grow content-center text-[1vmax]/[1] font-spectral italic text-right text-secondary mr-2"> - All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }} - </p> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC"> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY"> - <img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA"> + <div id="general-page-footer" class="flex flex-row basis-4 my-1" + hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML"> </div> + <div id="general-page-footer-trigger" hx-get="/api/v1/general-page/footer{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-footer" hx-swap="innerHTML"></div> - <hr class="border-t-4 border-dotted border-main-dark mb-[0.8vmin]"> + <hr class="border-t-4 border-dotted border-main-dark mb-2"> </div> - <div class="pr-[0.5vw] bg-background-dark shadow-2xl border-l-[0.4vmin] border-dotted border-main-dark"></div> + <div class="pr-[0.5dvw] bg-background-dark shadow-2xl border-l-2 border-dotted border-main-dark"></div> </div> <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/lightgallery.min.js"></script> - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-zoom.min.js"></script> - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/lightgallery/2.8.3/plugins/lg-thumbnail.min.js"></script> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/imagesloaded/5.0.0/imagesloaded.pkgd.min.js"></script> + <script src="https://f003.backblazeb2.com/file/sayana-static/libs/masonry-layout/4.2.2/masonry.pkgd.min.js"></script> + + <div id="general-page-bottom-embeds" class="flex" + hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="load" hx-target="this" hx-swap="innerHTML"> + </div> + <div id="general-page-bottom-embeds-trigger" hx-get="/api/v1/general-page/bottom-embeds{{ if .QueryString }}?{{ .QueryString }}{{ end }}" hx-trigger="popstate from:window" hx-target="#general-page-bottom-embeds" hx-swap="innerHTML"></div> <script> const themeWheel = document.getElementById('theme-wheel'); @@ -333,58 +353,5 @@ }); </script> - {{- if .MapLocationX }} - <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> - - <script> - var map; - - function initLocationMap(id, x, y, relativeErrorMeters = 0) { - map = L.map(id).setView([x, y], 8); - - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' - }).addTo(map); - - if (relativeErrorMeters != 0) { - var circle = L.circle([x, y], { - color: '#8a9d8a', - fillColor: '#8a9d8a', - fillOpacity: 0.15, - radius: relativeErrorMeters - }).addTo(map); - } - - var marker = L.marker([x, y], { - title: '{{ .Title }}', - }).addTo(map) - .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>') - .openPopup(); - } - - document.addEventListener('DOMContentLoaded', () => {initLocationMap( - 'map-container', - {{ .MapLocationX }}, - {{ .MapLocationY }}, - {{ .MapLocationAreaMeters }} - )}); - - let galleryResizeTimeout; - window.addEventListener('resize', function() { - clearTimeout(galleryResizeTimeout); - galleryResizeTimeout = setTimeout(() => { - map.invalidateSize(); - var oldGalleryMap = new Map(galleryMap); - oldGalleryMap.forEach((createFunc, g, map) => { - galleryMap.delete(g); - g.destroy(); - createFunc(); - }); - }, 300); - }); - </script> - {{- end }} - </body> </html> diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index ac2fb7d..3845275 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -1,59 +1,59 @@ {{ define "body" }} -<div class="flex flex-row ar-lt-0.8:flex-col text-[1vmax] h-[100%]"> +<div class="flex flex-row ar-lt-0.8:flex-col text-base h-[100%]"> <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" - class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[25%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]"> + class="tags-list ar-gt-0.8:min-w-[10dvh] ar-gt-0.8:max-w-[20dvh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] ar-lt-0.8:max-h-[30%] flex shrink-0 flex-col ar-gt-0.8:mr-6 bg-background-dark ar-lt-0.8:bg-background-light inset-shadow"> <div class="flex flex-col overflow-y-auto"> <fieldset> <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend> - <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense"> - <div class="m-[0.4vmin]"> + <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 xs:grid-cols-4 sm:grid-cols-5 grid-flow-row-dense"> + <div class="m-0.5"> <label class="font-bold"><input type="checkbox" id="tagsAllCheckbox" onclick="selectAll();"> {{ .L.BlogSearch.ChooseAllTags }}</label> </div> {{- range .Tags }} - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label> </div> {{- end }} </div> </fieldset> - <fieldset class="mt-[0.8vmin]"> + <fieldset class="mt-1"> <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.OrderByHeader }}</legend> <div class="flex flex-col ar-lt-0.8:grid grid-cols-3 grid-rows-2 grid-flow-col"> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortTitleAsc" name="sort" value="titleAsc" {{ if eq .QuerySort "titleAsc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-a-z"></i></label> </div> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortTitleDesc" name="sort" value="titleDesc" {{ if eq .QuerySort "titleDesc" }}checked{{ end }}> {{ .L.BlogSearch.TitleOrdered }} <i class="fas fa-arrow-down-z-a"></i></label> </div> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortActionDateAsc" name="sort" value="actionDateAsc" {{ if eq .QuerySort "actionDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label> </div> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortActionDateDesc" name="sort" value="actionDateDesc" {{ if eq .QuerySort "actionDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.ActionDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label> </div> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortPublicationDateAsc" name="sort" value="publicationDateAsc" {{ if eq .QuerySort "publicationDateAsc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-1-9"></i></label> </div> - <div class="m-[0.4vmin]"> + <div class="m-0.5"> <label><input type="radio" id="sortPublicationDateDesc" name="sort" value="publicationDateDesc" {{ if eq .QuerySort "publicationDateDesc" }}checked{{ end }}> {{ .L.BlogSearch.PublicationDateOrdered }} <i class="fas fa-arrow-down-9-1"></i></label> </div> </div> </fieldset> </div> - <hr class="border-t-[0.2vmax] border-dotted border-main-dark my-[0.4vmin]"> - <div class="flex flex-row grow bg-background-light my-[2vmin] p-[0.4vmin] rounded-[0.4vmin] justify-items-center"> - <i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i> - <div class="w-[0.5vmin] bg-background-dark grow-0"></div> + <hr class="border-t-2 border-dotted border-main-dark my-0.5"> + <div class="flex flex-row bg-background-dark my-2 p-0.5 rounded-1 justify-items-center"> + <i onclick="defaultSearchParams(); cleanHrefParams();" class="fas fa-rotate-left flex-1 text-center text-2xl hover:bg-background-light cursor-pointer transition-colors duration-300"></i> + <div class="w-1 bg-background-light grow-0"></div> <button class="flex-1" type="submit"> - <i onclick="setHrefParams();" class="fas fa-magnifying-glass text-center text-[1.5vmax] hover:bg-background-dark cursor-pointer transition-colors duration-300"></i> + <i onclick="setHrefParams();" class="fas fa-magnifying-glass text-center text-2xl hover:bg-background-light cursor-pointer transition-colors duration-300"></i> </button> </div> </form> - <div class="w-[0.5vh] border-r-[0.3vh] border-dotted border-main-dark bg-background-dark"></div> + <div class="w-[0.5dvh] border-r-2 border-dotted border-main-dark bg-background-dark"></div> <div hx-get="/api/v1/blog-search" hx-vals='js:{lang: "{{ .Lang }}", tz: clientTimeZone}' hx-target="this" hx-swap="innerHTML" hx-trigger="load" hx-include=".tags-list" - class="blog-cards grow flex flex-col overflow-y-auto bg-background-light inset-shadow p-[2.4vmin]"> + class="blog-cards grow flex flex-col overflow-y-auto bg-background-light inset-shadow p-6"> Loading... </div> </div> @@ -98,6 +98,7 @@ function cleanHrefParams() { const url = new URL(window.location.href); url.search = ''; + window.history.pushState({}, '', url.toString()); } @@ -137,22 +138,27 @@ window.history.pushState({}, '', url.toString()); } - document.addEventListener('DOMContentLoaded', contextAll); -</script> + document.addEventListener('htmx:afterRequest', (e) => { + if (e.detail.xhr.status == 404 || e.detail.successful != true) { + return console.error(e); + } + if (e.detail.target.id == 'general-page-body') { + contextAll(); -<script> - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - anyChecked = false; - checkboxes.forEach(checkbox => { - anyChecked = checkbox.checked ? true : anyChecked; - }); - if (!anyChecked) { - const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); - selectAllCheckbox.checked = true; - selectAll(); + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + anyChecked = false; + checkboxes.forEach(checkbox => { + anyChecked = checkbox.checked ? true : anyChecked; + }); + if (!anyChecked) { + const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); + selectAllCheckbox.checked = true; + selectAll(); + } + }); } - }); + }, {once: true}); </script> {{ end }} diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html index d8fb118..c9c9e91 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -1,10 +1,52 @@ {{ define "body" }} -<div class="bg-background-light flex flex-col inset-shadow p-[2.4vmin] overflow-y-auto"> +<div class="bg-background-light flex flex-col inset-shadow px-8 py-4 overflow-y-auto"> + <p class="max-xs:block [@media(max-height:32rem)]:block hidden font-spectral text-2xl font-bold italic text-main-dark tracking-[.0125rem] mb-1">{{ .Title }}</p> + <p class="max-xs:block [@media(max-height:32rem)]:block hidden grow text-lg font-spectral text-secondary">{{ .L.Metadata.Published }}: {{ .PublishedDate }}</p> + <p class="max-xs:block [@media(max-height:32rem)]:block hidden grow text-lg font-spectral text-secondary">{{ .L.Metadata.Action }}: {{ .ActionDate }}</p> + <hr class="max-xs:block [@media(max-height:32rem)]:block hidden border-t-2 border-dotted border-main-dark mt-1 mb-2 w-full ml-auto mr-auto"> {{ .ParsedMarkdown }} {{- if .MapLocationX }} - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> - <div id="map-container" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div> - <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> + <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%] ml-auto mr-auto"> + <div id="map-container" class="relative p-1 flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40dvh] ar-lt-0.8:h-[30dvh]"></div> + <hr class="border-t-3 border-dotted border-main-dark mt-1 mb-2 w-[80%] ml-auto mr-auto"> + + <script> + function initLocationMap(id, x, y, relativeErrorMeters = 0) { + map = L.map(id).setView([x, y], 8); + + L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18, + attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' + }).addTo(map); + + if (relativeErrorMeters != 0) { + var circle = L.circle([x, y], { + color: '#8a9d8a', + fillColor: '#8a9d8a', + fillOpacity: 0.15, + radius: relativeErrorMeters + }).addTo(map); + } + + var marker = L.marker([x, y], { + title: '{{ .Title }}', + }).addTo(map) + .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>') + .openPopup(); + } + + if (map) { + map.remove(); + map = null; + } + + setTimeout(() => initLocationMap( + 'map-container', + {{ .MapLocationX }}, + {{ .MapLocationY }}, + {{ .MapLocationAreaMeters }} + ), 1000); + </script> {{- end }} </div> {{ end }} diff --git a/views/pages/global-map.html b/views/pages/global-map.html index 569166f..83078c9 100644 --- a/views/pages/global-map.html +++ b/views/pages/global-map.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>SAYA TODAY // {{ .L.GlobalMap.Header }}</title> + <title>{{ .L.GlobalMap.Header }} // SAYA TODAY</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css"> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> @@ -15,7 +15,7 @@ <link rel="stylesheet" href="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet.markercluster/1.4.1/MarkerCluster.Default.css"> </head> -<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-screen flex flex-row ar-lt-0.8:flex-col"> +<body data-theme="ram" class="font-spectral text-main-dark overflow-hidden bg-interlocked-hexagons h-[100dvh] flex flex-row ar-lt-0.8:flex-col"> <script> function switchTheme(theme) { @@ -40,15 +40,21 @@ const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; </script> - <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[8vh]"> + <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5rem] ar-lt-0.8:w-[100dvw] h-[100dvh] ar-lt-0.8:h-[8dvh]"> <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> saya.today </div> - <div class="text-[3vw] ar-lt-0.8:text-[5vh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1vw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/blog';" class="fas fa-search w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="location.href='/{{ .Lang }}/map';" class="fas fa-map w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> - <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook w-[5vw] ar-lt-0.8:w-[8vh] h-[5vw] ar-lt-0.8:h-[8vh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex rounded-lg"></i> + <div class="text-[3rem] ar-lt-0.8:text-[5dvh] text-stroke-(--sidebar-stroke-color) text-stroke-[0.1dvw] flex flex-col ar-lt-0.8:flex-row gap-0 relative z-20"> + <a href="/{{ .Lang }}" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i class="fas fa-home flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <a href="/{{ .Lang }}/blog" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i class="fas fa-search flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <a href="/{{ .Lang }}/map" class="w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh]"> + <i class="fas fa-map flex w-full h-full content-center text-center text-sidebar hover:bg-background-dark transition-colors duration-300 rounded-lg"></i> + </a> + <i onclick="toggleThemeWheel();" id="theme-button" class="fas fa-swatchbook flex w-[5rem] ar-lt-0.8:w-[8dvh] h-[5rem] ar-lt-0.8:h-[8dvh] content-center text-center text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 rounded-lg"></i> </div> <div class="theme-wheel" id="theme-wheel"> <div class="theme-icon" data-theme="olive"></div> @@ -58,7 +64,7 @@ </div> </div> - <div id="map-container" class="bg-background-dark flex z-10 w-[95vw] ar-lt-0.8:w-screen h-screen ar-lt-0.8:h-[92vh]"></div> + <div id="map-container" class="bg-background-dark flex z-10 w-[95dvw] ar-lt-0.8:w-[100dvw] h-[100dvh] ar-lt-0.8:h-[92dvh]"></div> <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> <script src="https://f003.backblazeb2.com/file/sayana-static/libs/leaflet/1.9.4/leaflet.js"></script> @@ -242,7 +248,7 @@ title: title, }).bindPopup(` <div class="flex flex-row justify-center justify-items-center"> - <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[3vmax] h-[3vmax] mr-[0.5vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/${thumbnail}.webp"> + <img onclick="location.href='${link}';" class="cursor-pointer object-cover rounded-[10%] select-none w-12 h-12 mr-1" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/${thumbnail}.webp"> <span><b><a href="${link}">${title}</a></b><br><a href="https://www.openstreetmap.org/#map=13/${x}/${y}">${x}, ${y}</a></span> </div>`); diff --git a/views/pages/home-page.html b/views/pages/home-page.html new file mode 100644 index 0000000..95093a0 --- /dev/null +++ b/views/pages/home-page.html @@ -0,0 +1,121 @@ +{{ define "body" }} +<div class="bg-background-light flex flex-row ar-lt-0.6:flex-col w-full h-full inset-shadow overflow-y-auto text-base/[2] tracking-wide"> + <div class="ar-gt-0.6:flex-3/5 justify-center content-start p-8"> + <div class="flex flex-col justify-start items-start bg-background-dark mb-8"> + <p class="text-3xl/[2] sm:text-4xl/[2] lg:text-5xl/[2] ml-4 tracking-widest uppercase w-full font-spectral italic text-left + border-l-1.5 border-solid border-(--background-dark-color) + bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.Hymn1 }}</p> + <hr class="w-full border-t-2 border-dotted border-main-dark mb-1"> + <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn2 }}</p> + <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn3 }}</p> + <p class="text-xl/[2] sm:text-2xl/[2] lg:text-3xl/[2] -indent-8 ml-12 p-1">{{ .L.HomePage.Hymn4 }}</p> + </div> + + <div class="flex flex-col justify-center items-center"> + <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-spectral italic text-left + border-l-1.5 border-solid border-(--background-dark-color) + bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.DidYouKnowThat }}</p> + <hr class="w-full border-t-2 border-dotted border-main-dark mb-1"> + <ul class="w-full text-left list-[circle]"> + <li class="ml-4 mb-1">{{ index .FunFacts 0 }}</li> + <li class="ml-4 mb-1">{{ index .FunFacts 1 }}</li> + <li class="ml-4">{{ index .FunFacts 2 }}</li> + </ul> + </div> + </div> + + <div class="ar-gt-0.6:flex-2/5 justify-center content-start p-8"> + <div class="flex flex-col justify-center items-start mb-4"> + <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-spectral italic text-left + border-l-1.5 border-solid border-(--background-dark-color) + bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.Contacts }}</p> + <a href="https://t.me/EarlInisMona"> + <i class="fa-brands fa-telegram w-4 h-4 mr-1"></i> + <span class="font-bold">Telegram:</span> + <span>@EarlInisMona</span> + </a> + <a href="https://discord.com/users/356057270951346176"> + <i class="fa-brands fa-discord w-4 h-4 mr-1"></i> + <span class="font-bold">Discord:</span> + <span>montferrat</span> + </a> + <a href="mailto:saya.andy@posteo.com"> + <i class="fas fa-envelope w-4 h-4 mr-1"></i> + <span class="font-bold">E-mail:</span> + <span>saya.andy@posteo.com</span> + </a> + </div> + + <div class="flex relative aspect-square justify-center content-center mb-4"> + {{- range $_ := iterate .FilledHeartCount }} + <div class="home-page-heart z-50">♥</div> + {{- end }} + {{- range $_ := iterate .OutlineHeartCount }} + <div class="home-page-heart z-50">♡</div> + {{- end }} + <img src="https://f003.backblazeb2.com/file/sayana-static/home-page-gifs/{{ .GifName }}" + class="home-page-gif z-49 -rotate-12 max-w-[70%] max-h-[70%] border-4 border-(--background-dark-color) border-inset"> + </div> + + <script> + function shuffleHearts() { + vmin = calculateVmin(1); + document.querySelectorAll('.home-page-heart').forEach((e) => { + let top, left; + do { + top = Math.floor(rand() * 90) + 5; + left = Math.floor(rand() * 90) + 5; + } while (top > 30 && top < 70 && left > 30 && left < 70); + const opacity = rand() * 0.7 + 0.2; + const size = rand() * 3.2 + 0.8; + e.style["top"] = `calc(${top}% - ${size}vmin / 2)`; + e.style["left"] = `calc(${left}% - ${size}vmin / 2)`; + e.style["opacity"] = `${opacity}`; + e.style["font-size"] = `${size}vmin`; + }); + } + + let shuffleHeartsResizeTimeout; + function shuffleHeartsResizeCb() { + clearTimeout(shuffleHeartsResizeTimeout); + shuffleHeartsResizeTimeout = setTimeout(shuffleHearts, 300); + } + window.addEventListener('resize', shuffleHeartsResizeCb); + + document.addEventListener('popout', (e) => { + window.removeEventListener('resize', shuffleHeartsResizeCb); + shuffleHeartsResizeCb = null; + }); + </script> + + <script> + document.addEventListener('htmx:afterRequest', (e) => { + if (e.detail.xhr.status == 404 || e.detail.successful != true) { + return console.error(e); + } + if (e.detail.target.id == 'general-page-body') { + shuffleHearts(); + } + }, {once: true}); + </script> + + <div class="flex flex-col justify-center items-center"> + <p class="text-xl sm:text-2xl lg:text-3xl p-1 w-full font-spectral italic text-left + border-l-1.5 border-solid border-(--background-dark-color) + bg-gradient-to-r from-(--background-dark-color) to-(--background-light-color)">{{ .L.HomePage.SidebarDescription }}</p> + <hr class="w-full border-t-2 border-dotted border-main-dark mb-1"> + <div class="w-full grid grid-cols-[1fr_4fr] content-center items-center + [&>*:nth-child(4n+2)]:bg-(--background-medium-color) [&>*:nth-child(4n+3)]:bg-(--background-medium-color)" style="container-type: inline-size;"> + <i class="fas fa-home w-full h-full text-[10cqi] content-center text-center text-main-dark"></i> + <p class="p-1">{{ .L.HomePage.HomePageDescription }}</p> + <i class="fas fa-search w-full h-full text-[10cqi] content-center text-center text-main-dark"></i> + <p class="p-1">{{ .L.HomePage.BlogSearchDescription }}</p> + <i class="fas fa-map w-full h-full text-[10cqi] content-center text-center text-main-dark"></i> + <p class="p-1">{{ .L.HomePage.MarkerMapDescription }}</p> + <i class="fas fa-swatchbook w-full h-full text-[10cqi] content-center text-center text-main-dark"></i> + <p class="p-1">{{ .L.HomePage.ThemeSwitchDescription }}</p> + </div> + </div> + </div> +</div> +{{ end }} diff --git a/views/partials/blog-page-like-button.html b/views/partials/blog-page-like-button.html index afc3027..f23ef9d 100644 --- a/views/partials/blog-page-like-button.html +++ b/views/partials/blog-page-like-button.html @@ -1,7 +1,6 @@ <button hx-put="/api/v1/like" hx-vals='{"like": {{ not .Liked }}}' hx-target="this" hx-swap="outerHTML" hx-trigger="click" - class="text-[0.8vmax]/[0.9] font-spectral text-left px-[0.4vmax] py-[0.2vmax] {{ if .Liked }}bg-main-light hover:bg-main-medium text-background-dark border-inset{{ else }}bg-main-dark hover:bg-main-medium text-background-light border-outset{{ end }} cursor-pointer border-[0.2vmax] border-background-dark"> - <i class="fas fa-thumbs-up w-[0.8vmax] h-[0.8vmax] mr-[0.4vmax]"></i> - <span>{{ .L.LikeButton }}</span> - <span class="italic ml-[0.2vmax] text-background-light">({{ .LikedCount }})</span> + class="text-sm/[0.9] shrink-0 font-spectral text-left px-1 py-0.5 {{ if .Liked }}bg-main-light hover:bg-main-medium text-background-dark border-inset{{ else }}bg-main-dark hover:bg-main-medium text-background-light border-outset{{ end }} cursor-pointer border-2 border-background-dark"> + <i class="fas fa-thumbs-up w-3 h-3"></i> + <span class="italic ml-0.5 text-background-light">({{ .LikedCount }})</span> </button>
\ No newline at end of file diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index 35c7ad7..5c3bb95 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -1,21 +1,27 @@ {{- range .BlogPages }} -<hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto"> -<div class="m-[0.4vmin] flex flex-row justify-center justify-items-center"> - <img onclick="location.href='{{ .ArticleLink }}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp"> - <div class="grow my-[0.4vmin] mx-[0.8vmin] flex flex-col justify-center"> - <div class="flex flex-row gap-[5%] max-w-[100%]"> - <a href="{{ .ArticleLink }}" class="flex-9/12 grow-0 block text-[1vmax] cursor-pointer"> +<hr class="first-of-type:hidden my-1 border-t-2 border-dotted border-main-dark"> +<div class="m-1 flex flex-row justify-center justify-items-center"> + <a href="{{ .ArticleLink }}" class="w-24 h-24 basis-24 shrink-0 my-2.5 mr-2"> + <img onclick="return changeUrl('{{ .ArticleLink }}');" class="w-full h-full aspect-square cursor-pointer object-cover rounded-lg select-none" src="https://f003.backblazeb2.com/file/sayana-photos/webp-320p/{{ .Thumbnail }}.webp"> + </a> + <div class="grow my-0.5 mx-1 flex flex-col justify-center"> + <div class="flex flex-row gap-2 max-w-[100%]"> + <a href="{{ .ArticleLink }}" onclick="return changeUrl('{{ .ArticleLink }}');" class="grow text-base cursor-pointer"> <span class="font-extrabold">{{ .Title }}</span> <span class="font-extrabold select-none text-secondary">//</span> <span class="italic text-secondary">{{ .ActionDate }}</span> - <span class="font-extrabold select-none text-secondary">//</span> - <i class="fas fa-thumbs-up w-[1vmax] h-[1vmax]"></i> - <span class="italic text-secondary">{{ .LikeCount }}</span> </a> - <p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p> + <p class="basis-56 text-base italic text-secondary text-right">{{ .PublishedTime }}</p> </div> - <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p> - <p class="text-[1vmax] text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> + <p class="text-base text-main-dark">{{ .ShortDescription }}</p> + <p class="text-base text-secondary italic"> + <i class="fas fa-thumbs-up w-4 h-4"></i> + <span>{{ .LikeCount }}</span> + <span class="font-extrabold select-none">//</span> + <i class="fas fa-eye w-4 h-4"></i> + <span>{{ .ViewCount }}</span> + </p> + <p class="text-base text-secondary">{{ $.L.TagsLabel }} {{ template "catalogue-blog-card-tags.html" . }}</p> </div> </div> {{- end }}
\ No newline at end of file diff --git a/views/partials/general-page-body.html b/views/partials/general-page-body.html index d697c14..c3aa3eb 100644 --- a/views/partials/general-page-body.html +++ b/views/partials/general-page-body.html @@ -1 +1,2 @@ +<title>{{ .Title }} // SAYA TODAY</title> {{ block "body" . }}{{ end }}
\ No newline at end of file diff --git a/views/partials/general-page-bottom-embeds.html b/views/partials/general-page-bottom-embeds.html new file mode 100644 index 0000000..4c36b26 --- /dev/null +++ b/views/partials/general-page-bottom-embeds.html @@ -0,0 +1 @@ +{{ block "bottom-embeds" . }}{{ end }}
\ No newline at end of file diff --git a/views/partials/general-page-footer.html b/views/partials/general-page-footer.html index af6aec9..8979423 100644 --- a/views/partials/general-page-footer.html +++ b/views/partials/general-page-footer.html @@ -1,7 +1,7 @@ {{ block "footer" . }}{{ end }} -<p class="grow content-center text-[1vmax]/[1] font-spectral italic text-right text-secondary mr-2"> +<p class="grow content-center text-base/[1] font-spectral italic text-right text-secondary mr-1"> All the photos on <a href="https://saya.today/">saya.today</a> are licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a> by <a href="https://t.me/EarlInisMona">Saya Andy</a> © {{ .PublishedYear }} </p> -<img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC"> -<img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY"> -<img class="max-w-[1vmax] max-h-[1vmax] self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA">
\ No newline at end of file +<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="CC"> +<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="BY"> +<img class="max-w-4 max-h-4 self-center" src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="SA">
\ No newline at end of file diff --git a/views/partials/general-page-header.html b/views/partials/general-page-header.html index 66cfecf..e5488b4 100644 --- a/views/partials/general-page-header.html +++ b/views/partials/general-page-header.html @@ -1,8 +1,10 @@ <div {{ if .PublishedDate }} hx-get="/api/v1/tz" hx-vals='js:{timestamp: "{{ .PublishedDate }}", tz: clientTimeZone}' hx-target="#published-date" hx-swap="innerHTML" hx-trigger="load" {{ end }} - class="flex flex-row mb-[0.4vmin]"> - <i onclick="location.pathname += location.pathname.endsWith('/') ? '../' : '/../';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> - <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> - <p class="text-[2vmax] font-spectral italic text-left mt-auto">{{ .Title }}</p> - <p id="published-date" class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> + class="flex flex-row mb-2"> + <a href="./" class="w-(--text-4xl) h-(--text-4xl) mx-1 my-auto"> + <i onclick="return changeUrl('/../', true);" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-4xl"></i> + </a> + <p class="text-4xl font-spectral italic font-extrabold select-none my-auto pl-3 pr-3">//</p> + <p class="text-4xl font-spectral italic text-left mt-auto">{{ .Title }}</p> + <p id="published-date" class="grow text-2xl font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> </div> {{ block "header" . }}{{ end }} diff --git a/views/partials/general-page-top-embeds.html b/views/partials/general-page-top-embeds.html new file mode 100644 index 0000000..1407b50 --- /dev/null +++ b/views/partials/general-page-top-embeds.html @@ -0,0 +1 @@ +{{ block "top-embeds" . }}{{ end }}
\ No newline at end of file |