Latest changes from my twitter for coders project brought back.

master
Zed A. Shaw 7 days ago
parent 0f063c4a4d
commit 32cd1ff495
  1. 23
      data/models.go
  2. 1
      go.mod
  3. 2
      go.sum
  4. 6
      migrations/20250802154952_init.sql
  5. 2
      pages/index.md
  6. 55
      pages/layouts/main.html
  7. 1
      static/icons/arrow-big-up.svg
  8. 1
      static/icons/bookmark.svg
  9. 1
      static/icons/heart.svg
  10. 1
      static/icons/log-in.svg
  11. 1
      static/icons/log-out.svg
  12. 1
      static/icons/message-square.svg
  13. 1
      static/icons/refresh-cw.svg
  14. 1
      static/icons/reply.svg
  15. 1
      static/icons/rss.svg
  16. 1
      static/icons/settings.svg
  17. 1
      static/icons/user-plus.svg
  18. 21
      static/input_style.css
  19. 1
      static/js/alpine-intersect.js
  20. 49
      static/js/code.js
  21. 54
      static/style.css
  22. 6
      tools/pragmas.sql
  23. 16
      views/admin/table/new.html
  24. 8
      views/admin/table/view.html
  25. 80
      views/layouts/main.html

@ -1,6 +1,9 @@
package data
import "reflect"
import (
"reflect"
"github.com/guregu/null/v6"
)
type Login struct {
Username string `db:"username" validate:"required,max=30"`
@ -14,8 +17,26 @@ type User struct {
Password string `db:"password" validate:"required,min=8,max=64"`
}
type Message struct {
Id int `db:"id" json:"id" validate:"numeric"`
Text string `db:"text" json:"text" validate:"required,max=512"`
UserId int `db:"user_id" json:"user_id" validate:"numeric"`
CreatedAt string `db:"created_at" json:"created_at"`
Likes int `db:"likes" json:"likes" validate:"numeric"`
Bookmarks int `db:"bookmarks" json:"bookmarks" validate:"numeric"`
ReplyingTo null.Int `db:"replying_to" json:"replying_to" validate:"omitempty,numeric"`
}
type Bookmark struct {
MessageId int `db:"message_id" json:"message_id" validate:"required,numeric"`
UserId int `db:"user_id" json:"user_id" validate:"required,numeric"`
}
func Models() map[string]reflect.Type {
return map[string]reflect.Type{
"user": reflect.TypeFor[User](),
"message": reflect.TypeFor[Message](),
"bookmark": reflect.TypeFor[Bookmark](),
}
}

@ -9,6 +9,7 @@ require (
github.com/go-playground/validator/v10 v10.26.0
github.com/gofiber/fiber/v2 v2.52.8
github.com/gofiber/template/html/v2 v2.1.3
github.com/guregu/null/v6 v6.0.0
github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.28
github.com/stretchr/testify v1.10.0

@ -231,6 +231,8 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=

@ -1,12 +1,6 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE user (id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE, password TEXT NOT NULL);
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA mmap_size = 134217728; -- 128 megabytes
PRAGMA journal_size_limig = 67108864; -- 64 megabytes
PRAGMA cache_size = 2000;
-- +goose StatementEnd
-- +goose Down

@ -68,6 +68,8 @@ Once you have that you can make it your own:
git init .
go mod tidy
cp config_example.toml config.toml
make migrate
sqlite3 .\db.sqlite3 ".read tools/pragmas.sql"
make dev
```

@ -8,11 +8,14 @@
<meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/style.css">
<!-- all alpine plugins come first to register into alpine:init event -->
<script defer src="/js/alpine-intersect.js"></script>
<!-- then alpine runs, triggers init, and tada you get no-build plugins -->
<script defer src="/js/alpine.js"></script>
<script src="/js/code.js"></script>
<title>ZedShaw.games</title>
<title>Go Web Dev Starter Kit</title>
</head>
<body data-testid="{{.PageId}}">
<body id="top" data-testid="{{.PageId}}">
<header>
<nav>
<a id="home" href="/">
@ -23,8 +26,22 @@
<use href="/icons/home.svg#home" />
</svg>
</a>
<a id="register" href="/register/">Register</a>
<a id="login" href="/login/">Login</a>
<a id="register" href="/register/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/user-plus.svg#img" />
</svg>
</a>
<a id="login" href="/login/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/log-in.svg#img" />
</svg>
</a>
</nav>
</header>
@ -32,17 +49,25 @@
{{embed}}
</main>
<footer>
<div class="flex-1">
<img class="size-12 shrink-0" src="/logo.png" />
<div>
<p>Blah blah about me.</p>
</div>
</div>
<div class="flex-1">
<h3 class="text-3xl">Other Projects</h3>
<p>Some other links to stuff.</p>
</div>
<footer class="sticky-bottom">
<nav>
<a href="/feed/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/home.svg#home" />
</svg>
</a>
<a href="#top">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/arrow-big-up.svg#img" />
</svg>
</a>
</nav>
</footer>
</body>
</html>

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M9 21V10H5l7-7 7 7h-4v11z" />
</svg>

Before

Width:  |  Height:  |  Size: 249 B

After

Width:  |  Height:  |  Size: 260 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
</svg>

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 286 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z" />
</svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 365 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4" />
<polyline points="10 17 15 12 10 7" />

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 359 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />

Before

Width:  |  Height:  |  Size: 346 B

After

Width:  |  Height:  |  Size: 357 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 296 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M21 2v6h-6" />
<path d="M3 12a9 9 0 0 1 15-6.7L21 8" />

Before

Width:  |  Height:  |  Size: 347 B

After

Width:  |  Height:  |  Size: 358 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<polyline points="9 17 4 12 9 7" />
<path d="M20 18v-2a4 4 0 0 0-4-4H4" />

Before

Width:  |  Height:  |  Size: 287 B

After

Width:  |  Height:  |  Size: 298 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M4 11a9 9 0 0 1 9 9" />
<path d="M4 4a16 16 0 0 1 16 16" />

Before

Width:  |  Height:  |  Size: 315 B

After

Width:  |  Height:  |  Size: 326 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="settings"
>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 840 B

@ -8,6 +8,7 @@
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
id="img"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 394 B

@ -13,8 +13,20 @@ header {
@apply flex flex-col justify-stretch;
}
footer {
@apply bg-gray-950 text-gray-50 text-lg flex p-1;
}
@utility sticky-bottom {
@apply w-full sticky bottom-0 left-0;
}
nav {
@apply flex bg-gray-950 *:text-gray-50 *:flex-1 *:text-xl p-6;
@apply flex justify-center items-center bg-gray-950 *:text-gray-50 *:flex-1 *:text-xl w-full justify-evenly;
}
nav > a {
@apply flex justify-center items-center pt-1;
}
code {
@ -23,16 +35,13 @@ code {
}
pre {
@apply bg-gray-950 rounded-lg border-1 border-gray-300 mb-4 p-1;
@apply bg-gray-950 border-1 border-gray-600 mb-4 p-1;
}
pre > code {
@apply !bg-gray-950 p-1;
}
footer {
@apply bg-gray-950 text-gray-50 text-lg flex p-6;
}
h1 {
@apply text-6xl mb-2 mt-4;
@ -175,7 +184,7 @@ shape.video {
}
block {
@apply flex flex-col p-4 mb-10 gap-4;
@apply flex flex-col p-4 gap-4;
}
bar {

@ -0,0 +1 @@
(()=>{function o(e){e.directive("intersect",e.skipDuringClone((t,{value:i,expression:l,modifiers:n},{evaluateLater:r,cleanup:c})=>{let s=r(l),a={rootMargin:x(n),threshold:f(n)},u=new IntersectionObserver(d=>{d.forEach(h=>{h.isIntersecting!==(i==="leave")&&(s(),n.includes("once")&&u.disconnect())})},a);u.observe(t),c(()=>{u.disconnect()})}))}function f(e){if(e.includes("full"))return .99;if(e.includes("half"))return .5;if(!e.includes("threshold"))return 0;let t=e[e.indexOf("threshold")+1];return t==="100"?1:t==="0"?0:Number(`.${t}`)}function p(e){let t=e.match(/^(-?[0-9]+)(px|%)?$/);return t?t[1]+(t[2]||"px"):void 0}function x(e){let t="margin",i="0px 0px 0px 0px",l=e.indexOf(t);if(l===-1)return i;let n=[];for(let r=1;r<5;r++)n.push(p(e[l+r]||""));return n=n.filter(r=>r!==void 0),n.length?n.join(" ").trim():i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(o)});})();

@ -4,7 +4,7 @@ class PaginateTable {
this.items = [];
this.url = url;
this.headers = [];
this.search_query=""
this.search_query="";
}
async contents() {
@ -31,19 +31,43 @@ class PaginateTable {
}
}
class GetJson {
class ForeverScroll {
constructor(url) {
this.item;
this.page = 0;
this.items = [];
this.url = url;
this.end = false;
}
async item() {
const resp = await fetch(`${this.url}`);
async init() {
const resp = await fetch(this.url);
console.assert(resp.status == 200, "failed to get it");
this.item = await resp.json();
return this.item;
const items = await resp.json();
if(items) this.items = items;
}
async load() {
this.page += 1
let url = `${this.url}?page=${this.page}`;
const resp = await fetch(url);
console.assert(resp.status == 200, "failed to get it");
const items = await resp.json();
if(items) {
this.items.push(...items);
} else {
this.end = true;
}
}
}
const GetJson = async (url) => {
const resp = await fetch(url);
console.assert(resp.status == 200, "failed to get it");
return await resp.json();
}
const ConfirmDelete = async (table, obj_id) => {
@ -55,3 +79,14 @@ const ConfirmDelete = async (table, obj_id) => {
return false;
}
}
const UrlId = () => {
let url = new URL(window.location.href);
let parts = url.pathname.split("/");
if(window.location.href.endsWith("/")) {
return parts[parts.length - 2];
} else {
return parts[parts.length - 1];
}
}

@ -217,6 +217,12 @@
.visible {
visibility: visible;
}
.sticky-bottom {
position: sticky;
bottom: calc(var(--spacing) * 0);
left: calc(var(--spacing) * 0);
width: 100%;
}
.static {
position: static;
}
@ -262,10 +268,6 @@
.aspect-video {
aspect-ratio: var(--aspect-video);
}
.size-12 {
width: calc(var(--spacing) * 12);
height: calc(var(--spacing) * 12);
}
.h-15 {
height: calc(var(--spacing) * 15);
}
@ -299,12 +301,6 @@
.min-w-md {
min-width: var(--container-md);
}
.flex-1 {
flex: 1;
}
.shrink-0 {
flex-shrink: 0;
}
.transform {
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
}
@ -463,9 +459,6 @@
.\!p-4 {
padding: calc(var(--spacing) * 4) !important;
}
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! {
padding: calc(var(--spacing) * 0) !important;
}
@ -518,10 +511,6 @@
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
.text-3xl {
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
.text-sm {
font-size: var(--text-sm);
line-height: var(--tw-leading, var(--text-sm--line-height));
@ -631,10 +620,21 @@ header {
flex-direction: column;
justify-content: stretch;
}
footer {
display: flex;
background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 1);
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
color: var(--color-gray-50);
}
nav {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
justify-content: space-evenly;
background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 6);
:is(& > *) {
flex: 1;
}
@ -646,6 +646,12 @@ nav {
color: var(--color-gray-50);
}
}
nav > a {
display: flex;
align-items: center;
justify-content: center;
padding-top: calc(var(--spacing) * 1);
}
code {
speak-as: literal-punctuation;
display: inline-block;
@ -654,10 +660,9 @@ code {
}
pre {
margin-bottom: calc(var(--spacing) * 4);
border-radius: var(--radius-lg);
border-style: var(--tw-border-style);
border-width: 1px;
border-color: var(--color-gray-300);
border-color: var(--color-gray-600);
background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 1);
}
@ -665,14 +670,6 @@ pre > code {
background-color: var(--color-gray-950) !important;
padding: calc(var(--spacing) * 1);
}
footer {
display: flex;
background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 6);
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
color: var(--color-gray-50);
}
h1 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
@ -894,7 +891,6 @@ shape.video {
width: 100%;
}
block {
margin-bottom: calc(var(--spacing) * 10);
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 4);

@ -0,0 +1,6 @@
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA mmap_size = 134217728; -- 128 megabytes
PRAGMA journal_size_limig = 67108864; -- 64 megabytes
PRAGMA cache_size = 2000;

@ -1,13 +1,12 @@
<script>
let Data = new GetJson("/api/admin/new/table/{{ .Table }}");
</script>
<h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1>
<block x-data="Data">
<block x-data="{item: {}}"
x-init="item = await GetJson('/api/admin/new/table/{{ .Table }}')">
<form method="POST" action="/api/admin/new/table/{{ .Table }}">
<card>
<top><h2>New {{ .Table }}</h2></top>
<middle>
<template x-for="(value, key) in item">
<div>
@ -16,10 +15,11 @@
</div>
</template>
</middle>
<bottom>
<button type="button"><a href="/admin/table/{{ .Table }}/">Back</a></button>
<button class="hover:btn-alert" type="button">Clear</button>
<button class="hover:btn-hover" type="submit">Insert</button>
<button type="button"><a href="/admin/table/{{ .Table }}/">Back</a></button>
<button class="hover:btn-alert" type="button">Clear</button>
<button class="hover:btn-hover" type="submit">Insert</button>
</bottom>
</card>
</form>

@ -1,10 +1,8 @@
<script>
let Data = new GetJson("/api/admin/table/{{ .Table }}/{{ .Id }}");
</script>
<h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1>
<block x-data="Data">
<block x-data="{item: {}}"
x-init="item = await GetJson('/api/admin/table/{{ .Table }}/{{ .Id }}')">
<form method="POST" action="/api/admin/table/{{ .Table }}/{{ .Id }}">
<card>
<top><h1>{{ .Table }} : {{ .Id }}</h1></top>

@ -8,42 +8,66 @@
<meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/style.css">
<!-- all alpine plugins come first to register into alpine:init event -->
<script defer src="/js/alpine-intersect.js"></script>
<!-- then alpine runs, triggers init, and tada you get no-build plugins -->
<script defer src="/js/alpine.js"></script>
<script src="/js/code.js"></script>
<title>ZedShaw.games</title>
<title>Go Web Dev Starter Kit</title>
</head>
<body data-testid="{{.PageId}}">
<body id="top" data-testid="{{.PageId}}">
<header>
<a id="home" href="/">
<svg xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24">
<use href="/icons/home.svg#home"><use>
</svg>
</a>
<a id="live" href="/live/">Live</a>
<a id="stream" href="/stream/">Streams</a>
<a id="game" href="/game/">Games</a>
<a id="register" href="/register/">Register</a>
<a id="login" href="/login/">Login</a>
<nav>
<a id="home" href="/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/home.svg#home" />
</svg>
</a>
<a id="register" href="/register/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/user-plus.svg#img" />
</svg>
</a>
<a id="login" href="/login/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/log-in.svg#img" />
</svg>
</a>
</nav>
</header>
<div class="p-0 min-h-screen dark:bg-gray-900">
<main>
{{embed}}
</div>
</main>
<footer>
<div class="flex-1">
<img class="size-12 shrink-0" src="/logo.png" />
<div>
<p>Blah blah about me.</p>
</div>
</div>
<div class="flex-1">
<h3 class="text-3xl">Other Projects</h3>
<p>Some other links to stuff.</p>
</div>
<footer class="sticky-bottom">
<nav>
<a href="/feed/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/home.svg#home" />
</svg>
</a>
<a href="#top">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/arrow-big-up.svg#img" />
</svg>
</a>
</nav>
</footer>
</body>
</html>

Loading…
Cancel
Save