#109 move last url to new layout
This commit is contained in:
@@ -3,7 +3,7 @@ tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "/bin/bash -c 'FRONTEND_URL=xxx ./tmp/main'"
|
||||
bin = "./tmp/main"
|
||||
cmd = "templ generate && go build -o ./tmp/main ."
|
||||
delay = 1000
|
||||
exclude_dir = ["static", "migrations", "node_modules", "tmp"]
|
||||
|
||||
3
go.mod
3
go.mod
@@ -4,7 +4,9 @@ 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
|
||||
@@ -19,7 +21,6 @@ require (
|
||||
cloud.google.com/go/iam v1.1.12 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.11 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
github.com/a-h/templ v0.2.747 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -107,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=
|
||||
|
||||
@@ -11,11 +11,12 @@ import (
|
||||
func getHandler(db *sql.DB) http.Handler {
|
||||
var router = http.NewServeMux()
|
||||
|
||||
router.HandleFunc("/", service.HandleStaticUi)
|
||||
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))
|
||||
|
||||
6
main.go
6
main.go
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -14,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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/a-h/templ"
|
||||
)
|
||||
|
||||
func HandleStaticUi(w http.ResponseWriter, r *http.Request) {
|
||||
func HandleIndexAnd404(w http.ResponseWriter, r *http.Request) {
|
||||
var comp templ.Component = nil
|
||||
if r.URL.Path != "/" {
|
||||
comp = templates.Layout(templates.NotFound())
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"me-fit/templates"
|
||||
"me-fit/utils"
|
||||
|
||||
"database/sql"
|
||||
@@ -22,6 +23,13 @@ var (
|
||||
)
|
||||
)
|
||||
|
||||
func App(w http.ResponseWriter, r *http.Request) {
|
||||
comp := templates.App()
|
||||
layout := templates.Layout(comp)
|
||||
layout.Render(r.Context(), w)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func NewWorkout(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
metrics.WithLabelValues("new").Inc()
|
||||
|
||||
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>
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package templates
|
||||
|
||||
templ header() {
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user