Merge pull request '109-htmx' (#117) from 109-htmx into master
Reviewed-on: tim/me-fit#117
This commit is contained in:
49
.air.toml
Normal file
49
.air.toml
Normal 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
|
||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
6
api/.gitignore → .gitignore
vendored
6
api/.gitignore → .gitignore
vendored
@@ -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
22
Dockerfile
Normal 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"]
|
||||||
|
|
||||||
@@ -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"]
|
|
||||||
|
|
||||||
@@ -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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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
25
handler.go
Normal 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))
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
1446
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal 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
20
service/static_ui.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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 == "" {
|
||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -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
65
templates/app.templ
Normal 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
12
templates/header.templ
Normal 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
16
templates/index.templ
Normal 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
25
templates/layout.templ
Normal 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
11
templates/not_found.templ
Normal 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
30
utils/auth.go
Normal 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
21
view/.gitignore
vendored
@@ -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-*
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
engine-strict=true
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Package Managers
|
|
||||||
package-lock.json
|
|
||||||
pnpm-lock.yaml
|
|
||||||
yarn.lock
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"useTabs": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 100,
|
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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.
|
|
||||||
@@ -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
5533
view/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
13
view/src/app.d.ts
vendored
13
view/src/app.d.ts
vendored
@@ -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 {};
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 };
|
|
||||||
@@ -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 }
|
|
||||||
@@ -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>
|
|
||||||
@@ -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';
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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' })
|
|
||||||
],
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user