| -rw-r--r-- | .github/workflows/build-and-deploy.yml | 11 | ||||
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | deploy/inventory.yml | 14 | ||||
| -rw-r--r-- | deploy/playbook.yml | 28 | ||||
| -rw-r--r-- | internal/b2/client.go | 4 | ||||
| -rw-r--r-- | main.go | 42 | ||||
| -rw-r--r-- | views/index.html | 60 | ||||
| -rw-r--r-- | views/layouts/blog-main.html | 147 | ||||
| -rw-r--r-- | views/layouts/blog-page.html | 8 |
9 files changed, 245 insertions, 73 deletions
diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index e3bacd2..ac5fa1f 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -61,7 +61,7 @@ jobs: - name: Test SSH connection run: | - ssh -o ConnectTimeout=10 -i ~/.ssh/id_rsa svc_github@pl.saya.today "echo 'SSH connection successful'" + ssh -o ConnectTimeout=10 -i ~/.ssh/id_ed25519 svc_github@pl.saya.today "echo 'SSH connection successful'" - name: Install Ansible shell: bash @@ -73,10 +73,5 @@ jobs: env: ANSIBLE_HOST_KEY_CHECKING: False working-directory: ./deploy - run: > - ansible-playbook -i inventory.yml -l stage playbook.yml - --private-key ~/.ssh/id_ed25519 - -u svc_github - -e saya_today_web.b2.key_id=${{ secrets.B2_KEY_ID }} - -e saya_today_web.b2.application_key=${{ secrets.B2_APPLICATION_KEY }} - -e saya_today_web.tag=${{ github.ref_name }} + run: | + ansible-playbook -i inventory.yml -l stage playbook.yml --private-key ~/.ssh/id_ed25519 -u svc_github -e saya_today_web_b2_key_id=${{ secrets.B2_KEY_ID }} -e saya_today_web_b2_application_key=${{ secrets.B2_APPLICATION_KEY }} -e saya_today_web_tag=${{ github.ref_name }} @@ -36,7 +36,9 @@ config*.json /main /sayana-demo -/static/input.css +/static/output.css /.vscode .envrc + +/.ansible diff --git a/deploy/inventory.yml b/deploy/inventory.yml index 5c88365..3c1220d 100644 --- a/deploy/inventory.yml +++ b/deploy/inventory.yml @@ -3,15 +3,13 @@ all: prod: hosts: pl.saya.today: - saya_today_web: - name: sayana-web - hostname: sayana-web - ipv4_address: 172.16.0.18 + saya_today_web_name: sayana-web + saya_today_web_hostname: sayana-web + saya_today_web_ipv4_address: 172.16.0.18 stage: hosts: pl.saya.today: - saya_today_web: - name: sayana-demo - hostname: sayana-demo - ipv4_address: 172.16.0.17 + saya_today_web_name: sayana-demo + saya_today_web_hostname: sayana-demo + saya_today_web_ipv4_address: 172.16.0.17 diff --git a/deploy/playbook.yml b/deploy/playbook.yml index c126f23..773bb61 100644 --- a/deploy/playbook.yml +++ b/deploy/playbook.yml @@ -4,18 +4,18 @@ remote_user: svc_github tasks: - - name: Deploy saya-today-web Docker Container - community.docker.docker_container: - name: "{{ saya_today_web.name }}" - hostname: "{{ saya_today_web.hostname }}" - image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web.tag }}" - env: - B2_KEY_ID: "{{ saya_today_web.b2.key_id }}" - B2_APPLICATION_KEY: "{{ saya_today_web.b2.application_key }}" - network_mode: caddy - networks: - - name: caddy - ipv4_address: "{{ saya_today_web.ipv4_address }}" - restart_policy: unless-stopped - no_log: true + - name: Deploy saya-today-web Docker Container + community.docker.docker_container: + name: "{{ saya_today_web_name }}" + hostname: "{{ saya_today_web_hostname }}" + image: "ghcr.io/sayaandy/saya-today-web:{{ saya_today_web_tag }}" + env: + B2_KEY_ID: "{{ saya_today_web_b2_key_id }}" + B2_APPLICATION_KEY: "{{ saya_today_web_b2_application_key }}" + network_mode: caddy + networks: + - name: caddy + ipv4_address: "{{ saya_today_web_ipv4_address }}" + restart_policy: unless-stopped + no_log: true ... diff --git a/internal/b2/client.go b/internal/b2/client.go index f2b6cad..1a97e5f 100644 --- a/internal/b2/client.go +++ b/internal/b2/client.go @@ -36,10 +36,10 @@ type BlogPage struct { Metadata *frontmatter.Metadata } -func (c *B2Client) Scan() ([]*BlogPage, error) { +func (c *B2Client) Scan(prefix string) ([]*BlogPage, error) { filePaths := []*BlogPage{} - iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix)) + iter := c.bucket.List(context.Background(), b2.ListPrefix(c.prefix+prefix)) for iter.Next() { obj := iter.Object() @@ -8,6 +8,7 @@ import ( "log" "log/slog" "os" + "slices" "strconv" "strings" @@ -17,6 +18,7 @@ import ( "github.com/SayaAndy/saya-today-web/internal/lightgallery" "github.com/SayaAndy/saya-today-web/internal/tailwind" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/redirect" "github.com/gofiber/template/html/v2" "github.com/yuin/goldmark" "github.com/yuin/goldmark/parser" @@ -36,8 +38,9 @@ var ( gmhtml.WithXHTML(), ), ) - b2Client *b2.B2Client - configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)") + b2Client *b2.B2Client + configPath = flag.String("c", "config.yaml", "Path to the configuration file (in YAML format)") + availableLanguages = make([]string, 0) ) func main() { @@ -60,14 +63,37 @@ func main() { os.Exit(1) } + for _, lang := range cfg.BlogPages.AvailableLanguages { + availableLanguages = append(availableLanguages, lang.Name) + } + engine := html.New("./views", ".html") app := fiber.New(fiber.Config{ Views: engine, }) + app.Use(redirect.New(redirect.Config{ + Rules: map[string]string{ + "/blog": "/", + }, + StatusCode: 301, + })) + app.Get("/", func(c *fiber.Ctx) error { - pages, err := b2Client.Scan() + return c.Status(fiber.StatusOK).Render("index", fiber.Map{ + "AvailableLanguages": cfg.BlogPages.AvailableLanguages, + }) + }) + + app.Get("/blog/:lang", func(c *fiber.Ctx) error { + lang := c.Params("lang") + if !slices.Contains(availableLanguages, lang) { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + pages, err := b2Client.Scan(lang + "/") status := fiber.StatusOK if err != nil { status = fiber.StatusPartialContent @@ -88,14 +114,20 @@ func main() { }) } - return c.Status(status).Render("index", fiber.Map{ + return c.Status(status).Render("layouts/blog-main", fiber.Map{ "PublishedYear": "2025", "BlogPages": pageMeta, }) }) app.Get("/blog/:lang/:title", func(c *fiber.Ctx) error { - metadata, parsedMarkdownDesktop, parsedMarkdownMobile, err := readBlogPost(c.Params("lang") + "/" + c.Params("title")) + lang := c.Params("lang") + if !slices.Contains(availableLanguages, lang) { + c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) + return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("server does not support '%s' language... yet??", lang)) + } + + metadata, parsedMarkdownDesktop, parsedMarkdownMobile, err := readBlogPost(lang + "/" + c.Params("title")) if err != nil { c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8) return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("failed to find '%s' post", c.Params("title"))) diff --git a/views/index.html b/views/index.html index 731def6..1c57bd1 100644 --- a/views/index.html +++ b/views/index.html @@ -12,33 +12,45 @@ <style> .menu-outer-body { border-radius: 10%; - width: 60vw; - height: 40vh; + width: 40vh; + height: 30vh; } .menu-inner-body { width: 85%; } - @media (max-aspect-ratio: 1.2/1) { - .menu-outer-body { - border-radius: 5%; - } + .flex-adaptive { + flex-direction: row; + } - .menu-inner-body { - width: 92.5%; - } + .language-icon { + max-width: calc(80% / {{ .AvailableLanguages | len }}); + max-height: 80%; } @media (max-aspect-ratio: 0.8/1) { .menu-outer-body { - width: 100vw; - height: 80vh; + border-radius: 5%; + } + + .menu-outer-body { + width: min(40vh, 100vw); + height: 40vh; border-radius: 0; } + .language-icon { + max-width: 40%; + max-height: calc(80% / {{ .AvailableLanguages | len }}); + } + .menu-inner-body { - width: 100%; + width: 92.5%; + } + + .flex-adaptive { + flex-direction: column; } } </style> @@ -91,27 +103,13 @@ </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-[1.6vmin]">saya.today</p> - <div class="menu-inner-body grow bg-background-light flex flex-col mx-auto mb-[1.6vmin] overflow-y-auto opacity-100"> - {{- range .BlogPages }} - <div class="m-2 flex flex-row justify-center justify-items-center gap-4"> - <img onclick="location.href='/blog/{{ .Link }}';" style="flex-grow: 0;" class="flex-1 cursor-pointer object-cover rounded-4xl select-none w-[4vmax] h-[4vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp"> - <div class="flex-5 justify-self-center flex-col border-r-1 border-dashed border-opacity-50 border-main-medium"> - <a href="/blog/{{ .Link }}" class="block font-extrabold">{{ .Title }}</a> - <a href="/blog/{{ .Link }}" class="block italic text-[1vmax] text-secondary">{{ .ActionDate }}</a> - <p>{{ .ShortDescription }}</p> - </div> - <div class="flex-3 justify-self-center flex flex-col text-right"> - <p class="grow text-[1vmax]"> - Тэги: {{ .Tags }} - </p> - <p class="italic text-secondary text-[1vmax]"> - {{ .PublishedTime }} - </p> - </div> - </div> + <p class="font-patua mx-auto text-[2.5vmax] mt-[0.8vmin] mb-[0.8vmin]">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='/blog/{{ .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> </div> <script> diff --git a/views/layouts/blog-main.html b/views/layouts/blog-main.html new file mode 100644 index 0000000..1b8e149 --- /dev/null +++ b/views/layouts/blog-main.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>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> + <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"> + <link href="/output.css" rel="stylesheet"> + <style> + .menu-outer-body { + border-radius: 10%; + width: 60vw; + height: 40vh; + } + + .menu-inner-body { + width: 85%; + } + + @media (max-aspect-ratio: 1.2/1) { + .menu-outer-body { + border-radius: 5%; + } + + .menu-inner-body { + width: 92.5%; + } + } + + @media (max-aspect-ratio: 0.8/1) { + .menu-outer-body { + width: 100vw; + height: 80vh; + border-radius: 0; + } + + .menu-inner-body { + width: 100%; + } + } + </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"> + + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + <div class="absolute star z-0">*</div> + + <script> + function switchTheme(theme) { + if (theme == "" || typeof theme == "undefined") { + const currentTheme = document.body.getAttribute('data-theme'); + switch (currentTheme) { + case "olive": theme = "lettuce"; break; + case "lettuce": theme = "night"; break; + case "night": theme = "olive"; break; + } + } + // localStorage.setItem('theme', theme); + document.body.setAttribute('data-theme', theme); + } + + const savedTheme = 'night'; + 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-[1.6vmin]">saya.today</p> + <div class="menu-inner-body grow bg-background-light flex flex-col mx-auto mb-[1.6vmin] overflow-y-auto opacity-100"> + {{- range .BlogPages }} + <div class="m-2 flex flex-row justify-center justify-items-center gap-4"> + <img onclick="location.href='/blog/{{ .Link }}';" style="flex-grow: 0;" class="flex-1 cursor-pointer object-cover rounded-4xl select-none w-[4vmax] h-[4vmax]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp"> + <div class="flex-5 justify-self-center flex-col border-r-1 border-dashed border-opacity-50 border-main-medium"> + <a href="/blog/{{ .Link }}" class="block font-extrabold">{{ .Title }}</a> + <a href="/blog/{{ .Link }}" class="block italic text-[1vmax] text-secondary">{{ .ActionDate }}</a> + <p>{{ .ShortDescription }}</p> + </div> + <div class="flex-3 justify-self-center flex flex-col text-right"> + <p class="grow text-[1vmax]"> + {{ .Tags }} + </p> + <p class="italic text-secondary text-[1vmax]"> + {{ .PublishedTime }} + </p> + </div> + </div> + {{- end }} + </div> + </div> + + <script> + function generateStars() { + var clWidth = document.documentElement.clientWidth; + var clHeight = document.documentElement.clientHeight; + + stars = document.getElementsByClassName("star"); + for (let starIndex = 0; starIndex < stars.length; starIndex++) { + const star = stars[starIndex]; + const x = Math.floor(Math.random() * clWidth); + const y = Math.floor(Math.random() * clHeight * 0.8); + const rot = Math.random() * 90; + const animDuration = (Math.random() * 10) + 3; + const sz = Math.floor(Math.random() * 8) + 12; + star.style.transform = `translateX(${x}px) translateY(${y}px) rotate(${rot}deg)`; + star.style.animationDuration = `${animDuration}s`; + star.style.fontSize = `${sz}px`; + } + } + + document.addEventListener('DOMContentLoaded', generateStars); + + let generateStarsTimer; + window.addEventListener("resize", () => { + clearTimeout(generateStarsTimer); + generateStarsTimer = setTimeout(() => { + generateStars(); + }, 1000); + }); + </script> +</body> +</html> diff --git a/views/layouts/blog-page.html b/views/layouts/blog-page.html index 08a6f93..46fe9f4 100644 --- a/views/layouts/blog-page.html +++ b/views/layouts/blog-page.html @@ -49,7 +49,7 @@ saya.today </div> <div class="flex flex-col gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i onclick="location.href+='/../';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> @@ -61,7 +61,7 @@ <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]"> <div class="flex flex-row mb-[0.4vmin]"> - <i onclick="location.href='/';" 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> + <i onclick="location.href+='/../';" 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 class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> @@ -94,7 +94,7 @@ <div class="mobile-topbar-custom fixed bg-squares-and-triangles bg-repeat flex-row items-center shadow-2xl w-[90vw] z-20 mobile"> <div class="flex flex-row gap-0 relative z-20"> - <i onclick="location.href='/';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> + <i onclick="location.href+='/../';" class="fas fa-home nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i class="fas fa-search nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i class="fas fa-images nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> <i onclick="switchTheme('')" class="fas fa-swatchbook nav-icon-custom text-sidebar hover:bg-background-dark cursor-pointer transition-colors duration-300 flex justify-center items-center rounded-lg"></i> @@ -109,7 +109,7 @@ <div class="content-square flex flex-col bg-background-dark relative mt-[1.6vmin] mx-[1.6vmin]"> <div class="flex flex-row mb-[0.4vmin]"> - <i onclick="location.href='/';" 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> + <i onclick="location.href+='/../';" 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 class="grow text-[1.5vmax] font-spectral text-secondary text-right mt-auto">{{ .PublishedDate }}</p> |