Yōten: A social tracker for your language learning journey built on the atproto.

chore(init): initial commit

This commit adds the basic project structure and a test landing and
login page.

Changed files
+188
cmd
web
internal
ui
templates
+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
··· 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
··· 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
··· 1 + module yoten.app 2 + 3 + go 1.23.2 4 + 5 + require github.com/go-chi/chi/v5 v5.2.1 // indirect
+2
go.sum
··· 1 + github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 2 + github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+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
··· 1 + package web 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + ) 8 + 9 + func NewRouter() http.Handler { 10 + r := chi.NewRouter() 11 + 12 + r.Get("/", Index) 13 + 14 + r.Get("/login", Login) 15 + r.Post("/login", Login) 16 + 17 + return r 18 + }
+5
ui/templates/index.html
··· 1 + {{ define "title" }}home{{ end }} 2 + 3 + {{ define "content" }} 4 + <span>Hello, world</span> 5 + {{ end }}
+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
··· 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}}