+21
LICENSE
+21
LICENSE
···
1
+
MIT License
2
+
3
+
Copyright (c) 2025 Brook Jeynes
4
+
5
+
Permission is hereby granted, free of charge, to any person obtaining a copy
6
+
of this software and associated documentation files (the "Software"), to deal
7
+
in the Software without restriction, including without limitation the rights
8
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+
copies of the Software, and to permit persons to whom the Software is
10
+
furnished to do so, subject to the following conditions:
11
+
12
+
The above copyright notice and this permission notice shall be included in all
13
+
copies or substantial portions of the Software.
14
+
15
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+
SOFTWARE.
+6
README.md
+6
README.md
···
1
+
# Yōten
2
+
3
+
A social platform for tracking the essential points of your language learning.
4
+
Log your study sessions, visualize your progress, and share your milestones
5
+
with a community of fellow learners on the [AT Protocol](https://atproto.com).
6
+
Find your focus and turn daily practice into visible results.
+20
cmd/web/main.go
+20
cmd/web/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"net/http"
7
+
8
+
"yoten.app/internal/web"
9
+
)
10
+
11
+
func main() {
12
+
router := web.NewRouter()
13
+
14
+
log.Println("Starting server on :8080")
15
+
16
+
err := http.ListenAndServe(":8080", router)
17
+
if err != nil {
18
+
log.Fatal(fmt.Sprintf("failed to start server: %v", err))
19
+
}
20
+
}
+5
go.mod
+5
go.mod
+2
go.sum
+2
go.sum
+76
internal/web/handlers.go
+76
internal/web/handlers.go
···
1
+
package web
2
+
3
+
import (
4
+
"fmt"
5
+
"html/template"
6
+
"log"
7
+
"net/http"
8
+
)
9
+
10
+
func Index(w http.ResponseWriter, r *http.Request) {
11
+
data := struct{}{}
12
+
13
+
files := []string{
14
+
"./ui/templates/layouts/base.html",
15
+
"./ui/templates/index.html",
16
+
}
17
+
18
+
tmpl, err := template.ParseFiles(files...)
19
+
if err != nil {
20
+
log.Printf("template parsing error: %v", err)
21
+
http.Error(w, "Internal Server Error", 500)
22
+
return
23
+
}
24
+
25
+
err = tmpl.ExecuteTemplate(w, "layouts/base", data)
26
+
if err != nil {
27
+
log.Printf("template execution error: %v", err)
28
+
http.Error(w, "Internal Server Error", 500)
29
+
}
30
+
}
31
+
32
+
func Login(w http.ResponseWriter, r *http.Request) {
33
+
switch r.Method {
34
+
case http.MethodGet:
35
+
data := struct{}{}
36
+
37
+
files := []string{
38
+
"./ui/templates/layouts/base.html",
39
+
"./ui/templates/login.html",
40
+
}
41
+
42
+
tmpl, err := template.ParseFiles(files...)
43
+
if err != nil {
44
+
log.Printf("template parsing error: %v", err)
45
+
http.Error(w, "Internal Server Error", 500)
46
+
return
47
+
}
48
+
49
+
err = tmpl.ExecuteTemplate(w, "layouts/base", data)
50
+
if err != nil {
51
+
log.Printf("template execution error: %v", err)
52
+
http.Error(w, "Internal Server Error", 500)
53
+
}
54
+
case http.MethodPost:
55
+
err := r.ParseForm()
56
+
if err != nil {
57
+
http.Error(w, "Bad Request", http.StatusBadRequest)
58
+
return
59
+
}
60
+
61
+
username := r.FormValue("username")
62
+
password := r.FormValue("password")
63
+
64
+
if username != "admin" && password != "password" {
65
+
html := fmt.Sprintf(`<span id="%s" hx-swap-oob="innerHTML">%s</span>`, "login-msg", "Incorrect username or password")
66
+
67
+
w.Header().Set("Content-Type", "text/html")
68
+
w.WriteHeader(http.StatusOK)
69
+
w.Write([]byte(html))
70
+
return
71
+
}
72
+
73
+
w.Header().Set("HX-Redirect", "/")
74
+
w.WriteHeader(http.StatusOK)
75
+
}
76
+
}
+18
internal/web/routes.go
+18
internal/web/routes.go
+5
ui/templates/index.html
+5
ui/templates/index.html
+17
ui/templates/layouts/base.html
+17
ui/templates/layouts/base.html
···
1
+
{{ define "layouts/base" }}
2
+
<!DOCTYPE html>
3
+
<html lang="en">
4
+
5
+
<head>
6
+
<meta charset="UTF-8">
7
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+
<title>{{ block "title" . }}{{ end }} - Yōten</title>
9
+
</head>
10
+
11
+
<body>
12
+
<main>{{ block "content" . }}{{ end }}</main>
13
+
</body>
14
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
15
+
16
+
</html>
17
+
{{ end }}
+18
ui/templates/login.html
+18
ui/templates/login.html
···
1
+
{{ define "title" }}login{{ end }}
2
+
3
+
{{define "content"}}
4
+
<h2>Login Page</h2>
5
+
<p>Please enter your credentials.</p>
6
+
<form hx-post="/login" hx-swap="none">
7
+
<div>
8
+
<label for="username">Username</label>
9
+
<input type="text" id="username" name="username">
10
+
</div>
11
+
<div>
12
+
<label for="password">Password</label>
13
+
<input type="password" id="password" name="password">
14
+
</div>
15
+
<button type="submit">Log In</button>
16
+
</form>
17
+
<p id="login-msg"></p>
18
+
{{end}}