#109 switch svelte to SSR with go
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 46s

This commit is contained in:
Tim
2024-08-21 23:33:06 +02:00
parent 69b8efc8b9
commit 9c7d7e2cb3
53 changed files with 1796 additions and 6353 deletions

49
.air.toml Normal file
View File

@@ -0,0 +1,49 @@
root = "."
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/main"
cmd = "templ generate && go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["static", "migrations", "node_modules", "tmp"]
exclude_file = []
exclude_regex = ["_test.go", "_templ.go"]
exclude_unchanged = false
follow_symlink = false
include_dir = []
include_ext = ["go", "templ", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = false
keep_scroll = true

View File

@@ -11,11 +11,5 @@ jobs:
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- run: docker build api/ -t api-test - run: docker build . -t me-fit-test
- run: docker rmi api-test - run: docker rmi me-fit-test
- run: |
docker build \
--build-arg="PUBLIC_BASE_API_URL=${{ vars.PUBLIC_BASE_API_URL }}" \
-t view-test \
view/
- run: docker rmi view-test

View File

@@ -11,20 +11,10 @@ jobs:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }} - run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
- run: | - run: docker build . \
docker build \ -t git.wundenbergs.de/tim/me-fit:latest \
-t git.wundenbergs.de/tim/me-fit/api:latest \ -t git.wundenbergs.de/tim/me-fit:$GITHUB_SHA \
-t git.wundenbergs.de/tim/me-fit/api:$GITHUB_SHA \ - run: docker push git.wundenbergs.de/tim/me-fit:latest
api/ - run: docker push git.wundenbergs.de/tim/me-fit:$GITHUB_SHA
- run: docker push git.wundenbergs.de/tim/me-fit/api:latest - run: docker rmi git.wundenbergs.de/tim/me-fit:latest git.wundenbergs.de/tim/me-fit:$GITHUB_SHA
- run: docker push git.wundenbergs.de/tim/me-fit/api:$GITHUB_SHA
- run: docker rmi git.wundenbergs.de/tim/me-fit/api:latest git.wundenbergs.de/tim/me-fit/api:$GITHUB_SHA
- run: |
docker build \
--build-arg="PUBLIC_BASE_API_URL=${{ vars.PUBLIC_BASE_API_URL }}" \
-t git.wundenbergs.de/tim/me-fit/view:latest \
-t git.wundenbergs.de/tim/me-fit/view:$GITHUB_SHA \
view/
- run: docker push git.wundenbergs.de/tim/me-fit/view:latest
- run: docker push git.wundenbergs.de/tim/me-fit/view:$GITHUB_SHA
- run: docker rmi git.wundenbergs.de/tim/me-fit/view:latest git.wundenbergs.de/tim/me-fit/view:$GITHUB_SHA

View File

@@ -20,9 +20,15 @@
# Go workspace file # Go workspace file
go.work go.work
go.work.sum go.work.sum
*_templ.go
# env file # env file
.env .env
*.db *.db
secrets/ secrets/
node_modules/
static/css/tailwind.css
static/js/htmx.min.js
tmp/

22
Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM golang:1.23.0 AS BUILDER_GO
WORKDIR /me-fit
RUN go install github.com/a-h/templ/cmd/templ@latest
COPY . ./
RUN templ generate && go build -o /me-fit/me-fit .
FROM node:22.7.0 AS BUILDER_NODE
WORKDIR /me-fit
COPY . ./
RUN npm install && npm run build
FROM debian:12.6
WORKDIR /me-fit
RUN apt-get update && apt-get install -y ca-certificates && echo "" > .env
COPY --from=BUILDER_GO /me-fit/me-fit ./me-fit
COPY --from=BUILDER_NODE /me-fit/static ./static
COPY migrations ./migrations
EXPOSE 8080
ENTRYPOINT ["/me-fit/me-fit"]

View File

@@ -1,17 +0,0 @@
FROM golang:1.23@sha256:613a108a4a4b1dfb6923305db791a19d088f77632317cfc3446825c54fb862cd AS builder
WORKDIR /app
COPY go.mod go.sum main.go ./
COPY src src
RUN go build -o /bin/api ./main.go
FROM debian:stable-slim@sha256:382967fd7c35a0899ca3146b0b73d0791478fba2f71020c7aa8c27e3a4f26672
RUN apt-get update && apt-get install -y ca-certificates
WORKDIR /app
COPY --from=builder /bin/api ./api
COPY migrations ./migrations
EXPOSE 8080
ENTRYPOINT ["/app/api"]

View File

@@ -1,36 +0,0 @@
package middleware
import (
"api/src/utils"
"context"
"log"
"net/http"
)
type ContextKey string
const TOKEN_KEY ContextKey = "token"
func EnsureAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenStr := r.Header.Get("Authorization")
if (tokenStr == "") || (len(tokenStr) < len("Bearer ")) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
tokenStr = tokenStr[len("Bearer "):]
token, err := utils.VerifyToken(tokenStr)
if err != nil {
log.Println(err)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
var newContext = context.WithValue(r.Context(), TOKEN_KEY, token)
next.ServeHTTP(w, r.WithContext(newContext))
})
}

View File

@@ -1,36 +0,0 @@
package utils
import (
"context"
"log"
firebase "firebase.google.com/go"
"firebase.google.com/go/auth"
"google.golang.org/api/option"
)
var app *firebase.App
func VerifyToken(token string) (*auth.Token, error) {
if app == nil {
setup()
}
client, err := app.Auth(context.Background())
if err != nil {
log.Fatalf("error getting Auth client: %v\n", err)
}
return client.VerifyIDToken(context.Background(), token)
}
func setup() {
opt := option.WithCredentialsFile("./secrets/firebase.json")
firebaseApp, err := firebase.NewApp(context.Background(), nil, opt)
if err != nil {
log.Fatalf("error initializing app: %v", err)
}
app = firebaseApp
}

View File

@@ -1,10 +1,12 @@
module api module me-fit
go 1.22.5 go 1.22.5
require ( require (
firebase.google.com/go v3.13.0+incompatible firebase.google.com/go v3.13.0+incompatible
github.com/a-h/templ v0.2.747
github.com/golang-migrate/migrate/v4 v4.17.1 github.com/golang-migrate/migrate/v4 v4.17.1
github.com/joho/godotenv v1.5.1
github.com/mattn/go-sqlite3 v1.14.22 github.com/mattn/go-sqlite3 v1.14.22
github.com/prometheus/client_golang v1.20.1 github.com/prometheus/client_golang v1.20.1
google.golang.org/api v0.194.0 google.golang.org/api v0.194.0

View File

@@ -38,6 +38,8 @@ cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502Jw
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg=
github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -105,6 +107,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=

25
handler.go Normal file
View File

@@ -0,0 +1,25 @@
package main
import (
"me-fit/middleware"
"me-fit/service"
"database/sql"
"net/http"
)
func getHandler(db *sql.DB) http.Handler {
var router = http.NewServeMux()
router.HandleFunc("/", service.HandleIndexAnd404)
// Serve static files (CSS, JS and images)
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
router.HandleFunc("/app", service.App)
router.HandleFunc("POST /api/workout", service.NewWorkout(db))
router.HandleFunc("GET /api/workout", service.GetWorkouts(db))
router.HandleFunc("DELETE /api/workout", service.DeleteWorkout(db))
return middleware.Logging(middleware.EnableCors(router))
}

View File

@@ -1,14 +1,13 @@
package main package main
import ( import (
"api/src/middleware" "me-fit/utils"
"api/src/utils"
"api/src/workout"
"database/sql" "database/sql"
"log" "log"
"net/http" "net/http"
"github.com/joho/godotenv"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
) )
@@ -16,6 +15,11 @@ import (
func main() { func main() {
log.Println("Starting server...") log.Println("Starting server...")
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
db, err := sql.Open("sqlite3", "./data.db") db, err := sql.Open("sqlite3", "./data.db")
if err != nil { if err != nil {
log.Fatal("Could not open Database data.db: ", err) log.Fatal("Could not open Database data.db: ", err)
@@ -36,10 +40,9 @@ func main() {
} }
}() }()
var router = getRouter(db)
var server = http.Server{ var server = http.Server{
Addr: ":8080", Addr: ":8080",
Handler: middleware.Logging(middleware.EnableCors(middleware.EnsureAuth(router))), Handler: getHandler(db),
} }
log.Println("Starting server at", server.Addr) log.Println("Starting server at", server.Addr)
@@ -48,11 +51,3 @@ func main() {
panic(err) panic(err)
} }
} }
func getRouter(db *sql.DB) *http.ServeMux {
var router = http.NewServeMux()
router.HandleFunc("POST /workout", workout.NewWorkout(db))
router.HandleFunc("GET /workout", workout.GetWorkouts(db))
router.HandleFunc("DELETE /workout", workout.DeleteWorkout(db))
return router
}

View File

@@ -7,14 +7,14 @@ import (
) )
func EnableCors(next http.Handler) http.Handler { func EnableCors(next http.Handler) http.Handler {
var frontent_url = os.Getenv("FRONTEND_URL") var base_url = os.Getenv("BASE_URL")
if frontent_url == "" { if base_url == "" {
log.Fatal("FRONTEND_URL is not set") log.Fatal("BASE_URL is not set")
} }
log.Println("FRONTEND_URL is", frontent_url) log.Println("BASE_URL is", base_url)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", frontent_url) w.Header().Set("Access-Control-Allow-Origin", base_url)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Authorization") w.Header().Set("Access-Control-Allow-Headers", "Authorization")

1446
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "me-fit",
"version": "1.0.0",
"description": "Your (almost) independent tech stack to host on a VPC.",
"main": "index.js",
"scripts": {
"build": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --minify",
"watch": "mkdir -p static/js && cp -f node_modules/htmx.org/dist/htmx.min.js static/js/htmx.min.js && tailwindcss build -o static/css/tailwind.css --watch",
"test": ""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"htmx.org": "2.0.2",
"tailwindcss": "3.4.10",
"daisyui": "4.12.10"
}
}

20
service/static_ui.go Normal file
View File

@@ -0,0 +1,20 @@
package service
import (
"me-fit/templates"
"net/http"
"github.com/a-h/templ"
)
func HandleIndexAnd404(w http.ResponseWriter, r *http.Request) {
var comp templ.Component = nil
if r.URL.Path != "/" {
comp = templates.Layout(templates.NotFound())
w.WriteHeader(http.StatusNotFound)
} else {
comp = templates.Layout(templates.Index())
}
comp.Render(r.Context(), w)
}

View File

@@ -1,15 +1,14 @@
package workout package service
import ( import (
"api/src/middleware" "me-fit/templates"
"api/src/utils" "me-fit/utils"
"database/sql" "database/sql"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"firebase.google.com/go/auth"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
) )
@@ -24,6 +23,12 @@ var (
) )
) )
func App(w http.ResponseWriter, r *http.Request) {
comp := templates.App()
layout := templates.Layout(comp)
layout.Render(r.Context(), w)
}
func NewWorkout(db *sql.DB) http.HandlerFunc { func NewWorkout(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("new").Inc() metrics.WithLabelValues("new").Inc()
@@ -56,9 +61,10 @@ func NewWorkout(db *sql.DB) http.HandlerFunc {
return return
} }
token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) //TODO: Ensure auth
// token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
_, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", token.UID, date, typeStr, sets, reps) _, err = db.Exec("INSERT INTO workout (user_id, date, type, sets, reps) VALUES (?, ?, ?, ?, ?)", "", date, typeStr, sets, reps)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -71,8 +77,9 @@ func GetWorkouts(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("get").Inc() metrics.WithLabelValues("get").Inc()
token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
var userId = token.UID // var userId = token.UID
var userId = ""
rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", userId) rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", userId)
if err != nil { if err != nil {
@@ -112,8 +119,9 @@ func DeleteWorkout(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
metrics.WithLabelValues("delete").Inc() metrics.WithLabelValues("delete").Inc()
token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token) // token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
var userId = token.UID // var userId = token.UID
var userId = ""
rowId := r.FormValue("id") rowId := r.FormValue("id")
if rowId == "" { if rowId == "" {

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,11 +1,11 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { module.exports = {
content: ['./src/**/*.{html,js,svelte,ts}'], content: ["./templates/**/*.templ"],
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [ plugins: [
require("daisyui"), require('daisyui'),
], ],
daisyui: { daisyui: {
themes: ["retro"], themes: ["retro"],

65
templates/app.templ Normal file
View File

@@ -0,0 +1,65 @@
package templates
templ App() {
<main class="mx-2">
<form
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10"
>
<h2 class="text-4xl mb-8">Track your workout</h2>
<input
id="date"
type="date"
class="input input-bordered"
value=""
name="date"
/>
<select class="select select-bordered w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input
type="number"
class="input input-bordered"
placeholder="Sets"
name="sets"
/>
<input
type="number"
class="input input-bordered"
placeholder="Reps"
name="reps"
/>
<button class="btn btn-primary self-end">Save</button>
</form>
<!-- <div class="overflow-x-auto mx-auto max-w-screen-lg"> -->
<!-- <h2 class="text-4xl mt-14 mb-8">Workout history</h2> -->
<!-- <table class="table table-auto max-w-full"> -->
<!-- <thead> -->
<!-- <tr> -->
<!-- <th>Date</th> -->
<!-- <th>Type</th> -->
<!-- <th>Sets</th> -->
<!-- <th>Reps</th> -->
<!-- <th></th> -->
<!-- </tr> -->
<!-- </thead> -->
<!-- -->
<!-- <tbody> -->
<!-- <tr> -->
<!-- <th>{workout.date}</th> -->
<!-- <th>{workout.type}</th> -->
<!-- <th>{workout.sets}</th> -->
<!-- <th>{workout.reps}</th> -->
<!-- <th> -->
<!-- <div class="tooltip" data-tip="Delete Entry"> -->
<!-- <button on:click={() => deleteWorkout(workout.id)}> -->
<!-- <MdiDelete class="text-gray-400 text-lg"></MdiDelete> -->
<!-- </button> -->
<!-- </div> -->
<!-- </th> -->
<!-- </tr> -->
<!-- </tbody> -->
<!-- </table> -->
<!-- </div> -->
</main>
}

12
templates/header.templ Normal file
View File

@@ -0,0 +1,12 @@
package templates
templ header() {
<div class="flex justify-end items-center gap-2 py-1 px-2 md:gap-10 md:px-10 md:py-2 shadow">
<a href="/" class="flex-1 flex gap-2">
<img src="/static/favicon.svg" alt="ME-FIT logo"/>
<span>ME-FIT</span>
</a>
<a href="/signup" class="btn btn-sm">Sign Up</a>
<a href="/signin" class="btn btn-sm">Sign In</a>
</div>
}

16
templates/index.templ Normal file
View File

@@ -0,0 +1,16 @@
package templates
templ Index() {
<div class="hero bg-base-200 h-full">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect
solution for you.
</p>
<a href="/app" class="btn btn-primary">Get Started</a>
</div>
</div>
</div>
}

25
templates/layout.templ Normal file
View File

@@ -0,0 +1,25 @@
package templates
templ Layout(comp templ.Component) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>ME-FIT</title>
<link rel="icon" href="static/favicon.svg"/>
<link rel="stylesheet" href="static/css/tailwind.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="3c8efb09-44e4-4372-8a1e-c3bc675cd89a"></script>
</head>
<body>
<div class="h-screen flex flex-col">
@header()
<div class="flex-1">
if comp != nil {
@comp
}
</div>
</div>
</body>
</html>
}

11
templates/not_found.templ Normal file
View File

@@ -0,0 +1,11 @@
package templates
templ NotFound() {
<main class="flex h-full justify-center items-center ">
<div class="bg-error p-16 rounded-lg">
<h1 class="text-4xl text-error-content mb-5">Not Found</h1>
<p class="text-lg text-error-content mb-5">The page you are looking for does not exist.</p>
<a href="/" class="btn btn-lg btn-primary">Go back to home</a>
</div>
</main>
}

30
utils/auth.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
// import (
// "context"
// "log"
// )
// func VerifyToken(token string) (*auth.Token, error) {
// if app == nil {
// setup()
// }
//
// client, err := app.Auth(context.Background())
// if err != nil {
// log.Fatalf("error getting Auth client: %v\n", err)
// }
// return client.VerifyIDToken(context.Background(), token)
// }
//
// func setup() {
// opt := option.WithCredentialsFile("./secrets/firebase.json")
//
// firebaseApp, err := firebase.NewApp(context.Background(), nil, opt)
//
// if err != nil {
// log.Fatalf("error initializing app: %v", err)
// }
//
// app = firebaseApp
// }

21
view/.gitignore vendored
View File

@@ -1,21 +0,0 @@
node_modules
# Output
.output
.vercel
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,4 +0,0 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock

View File

@@ -1,8 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -1,13 +0,0 @@
FROM node:20@sha256:a4d1de4c7339eabcf78a90137dfd551b798829e3ef3e399e0036ac454afa1291 AS build
ARG PUBLIC_BASE_API_URL=
WORKDIR /app
COPY . ./
RUN npm install && npm run build
FROM nginx:1.27.1-alpine@sha256:c04c18adc2a407740a397c8407c011fc6c90026a9b65cceddef7ae5484360158
EXPOSE 80
COPY --from=build /app/build /usr/share/nginx/html

View File

@@ -1,38 +0,0 @@
# create-svelte
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.

View File

@@ -1,33 +0,0 @@
import js from '@eslint/js';
import ts from 'typescript-eslint';
import svelte from 'eslint-plugin-svelte';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
/** @type {import('eslint').Linter.FlatConfig[]} */
export default [
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
}
];

5533
view/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
{
"name": "template",
"version": "0.0.2",
"private": true,
"scripts": {
"dev": "PUBLIC_BASE_API_URL=http://localhost:8080 vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@iconify/json": "2.2.240",
"@sveltejs/adapter-static": "3.0.4",
"@sveltejs/kit": "2.5.24",
"@sveltejs/vite-plugin-svelte": "3.1.2",
"@types/eslint": "9.6.0",
"autoprefixer": "10.4.20",
"daisyui": "4.12.10",
"eslint": "9.9.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-svelte": "2.43.0",
"firebaseui": "6.1.0",
"globals": "15.9.0",
"postcss": "8.4.41",
"prettier": "3.3.3",
"prettier-plugin-svelte": "3.2.6",
"svelte": "4.2.18",
"svelte-check": "3.8.6",
"tailwindcss": "3.4.10",
"tslib": "2.6.3",
"typescript": "5.5.4",
"typescript-eslint": "8.2.0",
"unplugin-icons": "0.19.2",
"vite": "5.4.2"
},
"type": "module"
}

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

13
view/src/app.d.ts vendored
View File

@@ -1,13 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script defer src="https://umami.me-fit.eu/script.js" data-website-id="211de91f-c87a-4e6f-9bac-8214633535e3"></script>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,16 +0,0 @@
import { getAuth } from 'firebase/auth';
import { initializeApp } from 'firebase/app';
const app = initializeApp({
apiKey: 'AIzaSyCrJBW3c3Wut8DqjyVJoFAEyJ9Had__q-Q',
authDomain: 'me-fit-a9365.firebaseapp.com',
projectId: 'me-fit-a9365',
storageBucket: 'me-fit-a9365.appspot.com',
messagingSenderId: '631045688520',
appId: '1:631045688520:web:c7e0534927b52b0db629fd'
});
const auth = getAuth(app);
export { auth };

View File

@@ -1,27 +0,0 @@
import { writable } from "svelte/store";
type Type = 'success' | 'info' | 'warning' | 'error';
type Toast = {
message: string;
type: Type;
}
const toastStore = writable<Toast[]>([]);
const addToast = (message: string, type: Type) => {
const newToast = { message, type };
toastStore.update((toasts) => {
return [...toasts, newToast];
});
setTimeout(() => {
toastStore.update((toasts) => toasts.filter((t) => t !== newToast));
}, 5000);
}
export { toastStore, addToast, };
export type { Toast }

View File

@@ -1,82 +0,0 @@
<script lang="ts">
import '../app.css';
import { onDestroy, onMount } from 'svelte';
import type { Auth, User } from 'firebase/auth';
import { addToast, toastStore } from '$lib/toast';
import type { Toast } from '$lib/toast';
var auth: Auth | null = null;
var user: User | null = null;
var unsubAuth: (() => void) | null = null;
let toasts: Toast[] = [];
let unsubToast = toastStore.subscribe((value) => {
toasts = value;
});
onMount(async () => {
const { auth: _auth } = await import('../lib/firebase');
auth = _auth;
user = auth.currentUser;
unsubAuth = auth.onAuthStateChanged((newUser) => {
user = newUser;
});
});
onDestroy(() => {
unsubToast();
if (unsubAuth) {
unsubAuth();
}
});
const signOut = async () => {
try {
await (auth as Auth).signOut();
addToast('Signed out successfully', 'success');
} catch (error) {
console.error(error);
}
};
</script>
<svelte:head>
<title>ME-FIT</title>
</svelte:head>
<div class="h-screen flex flex-col">
<div class="flex justify-end items-center gap-2 py-1 px-2 md:gap-10 md:px-10 md:py-2 shadow">
<a href="/" class="flex-1 flex gap-2">
<img src="/favicon.svg" alt="ME-FIT logo" />
<span>ME-FIT</span>
</a>
{#if user}
<p class="hidden md:block">{user?.email}</p>
<button class="btn btn-sm" on:click={async () => signOut()}>Sign Out</button>
{:else}
<a href="/signup" class="btn btn-sm">Sign Up</a>
<a href="/signin" class="btn btn-sm">Sign In</a>
{/if}
</div>
<div class="flex-1">
<slot></slot>
</div>
<!-- This is needed so all class names are inside the bundle The class names are used dynamically, so -->
<!-- tailwind can't know -->
<div class="hidden">
<div class="alert-info text-info-content"></div>
<div class="alert-success text-success-content"></div>
<div class="alert-warning text-warning-content"></div>
<div class="alert-error text-error-content"></div>
</div>
<div class="toast toast-end">
{#each toasts as toast}
<div class="alert alert-{toast.type}">
<span class="text-{toast.type}-content">{toast.message}</span>
</div>
{/each}
</div>
</div>

View File

@@ -1,4 +0,0 @@
// This is needed for adapter-static
export const prerender = true;
// This is needed for nginx to work
export const trailingSlash = 'always';

View File

@@ -1,12 +0,0 @@
<div class="hero bg-base-200 h-full">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Next Level Workout Tracker</h1>
<p class="py-6">
Ever wanted to track your workouts and see your progress over time? ME-FIT is the perfect
solution for you.
</p>
<a href="/app" class="btn btn-primary">Get Started</a>
</div>
</div>
</div>

View File

@@ -1,160 +0,0 @@
<script lang="ts">
import { PUBLIC_BASE_API_URL } from '$env/static/public';
import { onMount } from 'svelte';
import type { Auth } from 'firebase/auth';
import { goto } from '$app/navigation';
import { addToast } from '$lib/toast';
import MdiDelete from '~icons/mdi/delete';
var auth: Auth | null = null;
let workouts: any[];
$: workouts = [];
async function handleSubmit(_submit: SubmitEvent) {
const form = _submit.target as HTMLFormElement;
const formData = new FormData(form);
try {
const response = await fetch(PUBLIC_BASE_API_URL + '/workout', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + (await auth?.currentUser?.getIdToken())
},
body: formData
});
if (response.ok) {
fetchWorkouts();
resetForm();
} else {
addToast('Failed to save workout: ' + (await response.text()), 'error');
}
} catch (error: any) {
addToast('Failed to save workout: ' + error.message, 'error');
}
}
function resetForm() {
const form = document.querySelector('form') as HTMLFormElement;
form.reset();
const date = new Date();
const dateInput = document.getElementById('date') as HTMLInputElement;
dateInput.value = date.toISOString().split('T')[0];
}
async function fetchWorkouts() {
try {
const response = await fetch(PUBLIC_BASE_API_URL + '/workout', {
headers: {
Authorization: 'Bearer ' + (await auth?.currentUser?.getIdToken())
},
method: 'GET'
});
if (response.ok) {
workouts = await response.json();
workouts = workouts.map((workout: any) => {
workout.date = new Date(workout.date).toLocaleDateString();
return workout;
});
} else {
addToast('Failed to fetch workouts: ' + (await response.text()), 'error');
}
} catch (error: any) {
addToast('Failed to fetch workouts: ' + error.message, 'error');
}
}
async function deleteWorkout(id: string) {
console.log('Deleting workout with id: ', id);
try {
const data = new FormData();
data.append('id', id);
const response = await fetch(PUBLIC_BASE_API_URL + '/workout', {
headers: {
Authorization: 'Bearer ' + (await auth?.currentUser?.getIdToken())
},
body: data,
method: 'DELETE'
});
if (response.ok) {
workouts = workouts.filter((workout) => workout.id !== id);
} else {
addToast('Failed to delete workout: ' + (await response.text()), 'error');
}
} catch (error: any) {
addToast('Failed to delete workout: ' + error.message, 'error');
}
}
onMount(async () => {
const authImp = import('$lib/firebase');
resetForm();
auth = (await authImp).auth;
await auth.authStateReady();
if (!auth?.currentUser) {
goto('/signin');
return;
}
await fetchWorkouts();
});
</script>
<main class="mx-2">
<form
class="max-w-xl mx-auto flex flex-col gap-4 justify-center mt-10"
on:submit|preventDefault={handleSubmit}
>
<h2 class="text-4xl mb-8">Track your workout</h2>
<input id="date" type="date" class="input input-bordered" value={new Date()} name="date" />
<select class="select select-bordered w-full" name="type">
<option>Push Ups</option>
<option>Pull Ups</option>
</select>
<input type="number" class="input input-bordered" placeholder="Sets" name="sets" />
<input type="number" class="input input-bordered" placeholder="Reps" name="reps" />
<button class="btn btn-primary self-end">Save</button>
</form>
<div class="overflow-x-auto mx-auto max-w-screen-lg">
<h2 class="text-4xl mt-14 mb-8">Workout history</h2>
<table class="table table-auto max-w-full">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sets</th>
<th>Reps</th>
<th></th>
</tr>
</thead>
<tbody>
{#each workouts as workout}
<tr>
<th>{workout.date}</th>
<th>{workout.type}</th>
<th>{workout.sets}</th>
<th>{workout.reps}</th>
<th>
<div class="tooltip" data-tip="Delete Entry">
<button on:click={() => deleteWorkout(workout.id)}>
<MdiDelete class="text-gray-400 text-lg"></MdiDelete>
</button>
</div>
</th>
</tr>
{/each}
</tbody>
</table>
</div>
</main>

View File

@@ -1,66 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { addToast } from '$lib/toast';
import { signInWithEmailAndPassword } from 'firebase/auth';
const signIn = async (event: SubmitEvent) => {
const { auth } = await import('$lib/firebase');
try {
const data = new FormData(event.target as HTMLFormElement);
await signInWithEmailAndPassword(
auth,
data.get('email') as string,
data.get('password') as string
);
goto('/');
} catch (error: any) {
const errorStr = error.code ? error.code : error.message;
addToast('Failed to sign in: ' + errorStr, 'error');
}
};
</script>
<form
class="max-w-xl px-2 mx-auto flex flex-col gap-4 h-full justify-center"
on:submit|preventDefault={signIn}
>
<h2 class="text-6xl mb-10">Sign In</h2>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
/>
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
/>
</svg>
<input type="text" class="grow" placeholder="Email" name="email" />
</label>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
/>
</svg>
<input type="password" class="grow" placeholder="Password" name="password" />
</label>
<div class="flex justify-end items-center gap-2">
<a href="/signup" class="link text-gray-500 text-sm">Don't have an account? Sign Up</a>
<button class="btn btn-primary self-end">Sign In</button>
</div>
</form>

View File

@@ -1,67 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { addToast } from '$lib/toast';
import { createUserWithEmailAndPassword } from 'firebase/auth';
const signUp = async (event: SubmitEvent) => {
const { auth } = await import('$lib/firebase');
try {
const data = new FormData(event.target as HTMLFormElement);
await createUserWithEmailAndPassword(
auth,
data.get('email') as string,
data.get('password') as string
);
addToast('Signed up successfully', 'success');
goto('/');
} catch (error: any) {
const errorStr = error.code ? error.code : error.message;
addToast('Failed to sign up: ' + errorStr, 'error');
}
};
</script>
<form
class="px-2 max-w-xl mx-auto flex flex-col gap-4 h-full justify-center"
on:submit|preventDefault={signUp}
>
<h2 class="text-6xl mb-10">Sign Up</h2>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z"
/>
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z"
/>
</svg>
<input type="text" class="grow" placeholder="Email" name="email" />
</label>
<label class="input input-bordered flex items-center gap-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="h-4 w-4 opacity-70"
>
<path
fill-rule="evenodd"
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd"
/>
</svg>
<input type="password" class="grow" placeholder="Password" name="password" />
</label>
<div class="flex justify-end items-center gap-2">
<a href="/signin" class="link text-gray-500 text-sm">Already have an account? Sign In</a>
<button class="btn btn-primary self-end">Sign Up</button>
</div>
</form>

View File

@@ -1,18 +0,0 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View File

@@ -1,19 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View File

@@ -1,10 +0,0 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import Icons from 'unplugin-icons/vite'
export default defineConfig({
plugins: [
sveltekit(),
Icons({ compiler: 'svelte' })
],
});