#109 switch svelte to SSR with go
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 46s
All checks were successful
Build Docker Image / Explore-Gitea-Actions (push) Successful in 46s
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:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- run: docker build api/ -t api-test
|
||||
- run: docker rmi api-test
|
||||
- run: |
|
||||
docker build \
|
||||
--build-arg="PUBLIC_BASE_API_URL=${{ vars.PUBLIC_BASE_API_URL }}" \
|
||||
-t view-test \
|
||||
view/
|
||||
- run: docker rmi view-test
|
||||
- run: docker build . -t me-fit-test
|
||||
- run: docker rmi me-fit-test
|
||||
|
||||
@@ -11,20 +11,10 @@ jobs:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
|
||||
- run: docker login git.wundenbergs.de -u tim -p ${{ secrets.DOCKER_GITEA_TOKEN }}
|
||||
- run: |
|
||||
docker build \
|
||||
-t git.wundenbergs.de/tim/me-fit/api:latest \
|
||||
-t git.wundenbergs.de/tim/me-fit/api:$GITHUB_SHA \
|
||||
api/
|
||||
- run: docker push git.wundenbergs.de/tim/me-fit/api:latest
|
||||
- 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
|
||||
- run: docker build . \
|
||||
-t git.wundenbergs.de/tim/me-fit:latest \
|
||||
-t git.wundenbergs.de/tim/me-fit:$GITHUB_SHA \
|
||||
- run: docker push git.wundenbergs.de/tim/me-fit:latest
|
||||
- run: docker push git.wundenbergs.de/tim/me-fit:$GITHUB_SHA
|
||||
- run: docker rmi git.wundenbergs.de/tim/me-fit:latest git.wundenbergs.de/tim/me-fit:$GITHUB_SHA
|
||||
|
||||
|
||||
6
api/.gitignore → .gitignore
vendored
6
api/.gitignore → .gitignore
vendored
@@ -20,9 +20,15 @@
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
*_templ.go
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
*.db
|
||||
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
|
||||
|
||||
require (
|
||||
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/joho/godotenv v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/prometheus/client_golang v1.20.1
|
||||
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/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
|
||||
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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
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/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/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/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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
|
||||
|
||||
import (
|
||||
"api/src/middleware"
|
||||
"api/src/utils"
|
||||
"api/src/workout"
|
||||
"me-fit/utils"
|
||||
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -16,6 +15,11 @@ import (
|
||||
func main() {
|
||||
log.Println("Starting server...")
|
||||
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
log.Fatal("Error loading .env file")
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite3", "./data.db")
|
||||
if err != nil {
|
||||
log.Fatal("Could not open Database data.db: ", err)
|
||||
@@ -36,10 +40,9 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
var router = getRouter(db)
|
||||
var server = http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: middleware.Logging(middleware.EnableCors(middleware.EnsureAuth(router))),
|
||||
Handler: getHandler(db),
|
||||
}
|
||||
log.Println("Starting server at", server.Addr)
|
||||
|
||||
@@ -48,11 +51,3 @@ func main() {
|
||||
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 {
|
||||
var frontent_url = os.Getenv("FRONTEND_URL")
|
||||
if frontent_url == "" {
|
||||
log.Fatal("FRONTEND_URL is not set")
|
||||
var base_url = os.Getenv("BASE_URL")
|
||||
if base_url == "" {
|
||||
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) {
|
||||
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-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 (
|
||||
"api/src/middleware"
|
||||
"api/src/utils"
|
||||
"me-fit/templates"
|
||||
"me-fit/utils"
|
||||
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"firebase.google.com/go/auth"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.WithLabelValues("new").Inc()
|
||||
@@ -56,9 +61,10 @@ func NewWorkout(db *sql.DB) http.HandlerFunc {
|
||||
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 {
|
||||
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) {
|
||||
metrics.WithLabelValues("get").Inc()
|
||||
|
||||
token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
|
||||
var userId = token.UID
|
||||
// token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
|
||||
// var userId = token.UID
|
||||
var userId = ""
|
||||
|
||||
rows, err := db.Query("SELECT rowid, date, type, sets, reps FROM workout WHERE user_id = ?", userId)
|
||||
if err != nil {
|
||||
@@ -112,8 +119,9 @@ func DeleteWorkout(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.WithLabelValues("delete").Inc()
|
||||
|
||||
token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
|
||||
var userId = token.UID
|
||||
// token := r.Context().Value(middleware.TOKEN_KEY).(*auth.Token)
|
||||
// var userId = token.UID
|
||||
var userId = ""
|
||||
|
||||
rowId := r.FormValue("id")
|
||||
if rowId == "" {
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -1,11 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
module.exports = {
|
||||
content: ["./templates/**/*.templ"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui"),
|
||||
require('daisyui'),
|
||||
],
|
||||
daisyui: {
|
||||
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