Compare commits
27 Commits
rowing
...
cfdb5b4d50
| Author | SHA1 | Date | |
|---|---|---|---|
| cfdb5b4d50 | |||
| 37580cdc42 | |||
| 711236b776 | |||
| 75454c2ed8 | |||
| 78c824c4c8 | |||
| ba3b933068 | |||
| 14c430bbad | |||
| 26c7422e34 | |||
| 470b1c79d8 | |||
| d849b606ec | |||
| 46a9da4c90 | |||
| 398a610cb2 | |||
| b506bae515 | |||
| 11ad0b5a83 | |||
| d7393e1419 | |||
| 0d32333c0c | |||
| 050a38a76f | |||
| bc43e9ed02 | |||
| 75b8b02825 | |||
| 5c69a1d0a7 | |||
| aa915e1071 | |||
| 7dc3f49273 | |||
| 21d3997a16 | |||
| c56ba217dd | |||
| 91804f1fe7 | |||
| 7e74ce5a2a | |||
| e92ac49140 |
19
.gitea/workflows/deploy.yaml
Normal file
19
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Deploy with Docker Compose
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Pull changes
|
||||
working-directory: /home/adamf/deploy/web_server
|
||||
run: git pull gitea main
|
||||
|
||||
- name: Run docker compose up
|
||||
working-directory: /home/adamf/deploy/web_server
|
||||
env:
|
||||
DOCKER_API_VERSION: "1.41"
|
||||
run: docker compose up -d --build --remove-orphans
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,9 @@ certbot/www
|
||||
backend/token/
|
||||
.env
|
||||
|
||||
gitea/data/*
|
||||
gitea-runner/data/*
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -7,13 +7,12 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/zmb3/spotify/v2 v2.4.3
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
@@ -24,7 +23,7 @@ require (
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
@@ -41,11 +40,6 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
|
||||
@@ -33,8 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
@@ -104,8 +102,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
@@ -176,8 +172,6 @@ github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -189,16 +183,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
@@ -303,8 +287,6 @@ golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4Iltr
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
|
||||
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
|
||||
"adam-french.co.uk/backend/models"
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ExtractedRowingData struct {
|
||||
TimeMinutes float64 `json:"timeMinutes"`
|
||||
TimeSeconds float64 `json:"timeSeconds"`
|
||||
Distance float64 `json:"distance"`
|
||||
}
|
||||
|
||||
func (store *Store) GetRowing(ctx *gin.Context) {
|
||||
var rowing []models.Rowing
|
||||
if err := store.DB.Order("Created_At DESC").Find(&rowing).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, rowing)
|
||||
}
|
||||
|
||||
func (store *Store) CreateRowing(ctx *gin.Context) {
|
||||
file, err := ctx.FormFile("image")
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "image is required"})
|
||||
return
|
||||
}
|
||||
|
||||
f, err := file.Open()
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open image"})
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Get the date taken from the EXIF data
|
||||
x, err := exif.Decode(f)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "no EXIF data found"})
|
||||
return
|
||||
}
|
||||
|
||||
dateTaken, err := x.DateTime()
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "no date found in EXIF data"})
|
||||
return
|
||||
}
|
||||
|
||||
// Seek back to start since exif.Decode advanced the cursor
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to seek image"})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to read image"})
|
||||
return
|
||||
}
|
||||
|
||||
allowedMediaTypes := map[string]bool{
|
||||
"image/jpeg": true,
|
||||
"image/png": true,
|
||||
"image/gif": true,
|
||||
"image/webp": true,
|
||||
}
|
||||
mediaType := file.Header.Get("Content-Type")
|
||||
if !allowedMediaTypes[mediaType] {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "unsupported image type"})
|
||||
return
|
||||
}
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
|
||||
// Build the message with an image + text prompt
|
||||
message, err := store.ClaudeClient.Messages.New(context.Background(), anthropic.MessageNewParams{
|
||||
Model: anthropic.ModelClaudeHaiku4_5,
|
||||
MaxTokens: 256,
|
||||
Messages: []anthropic.MessageParam{
|
||||
{
|
||||
Role: "user",
|
||||
Content: []anthropic.ContentBlockParamUnion{
|
||||
// Image block
|
||||
anthropic.NewImageBlock(anthropic.Base64ImageSourceParam{
|
||||
Type: "base64",
|
||||
MediaType: anthropic.Base64ImageSourceMediaType(mediaType),
|
||||
Data: encoded,
|
||||
}),
|
||||
// Text prompt requesting exactly 2 variables
|
||||
anthropic.NewTextBlock(
|
||||
`Look at this rowing machine display. Extract the total elapsed time and total distance.
|
||||
|
||||
Return ONLY a JSON object with these exact keys and numeric values:
|
||||
- "timeMinutes": total minutes (e.g. 2:30 = 2)
|
||||
- "timeSeconds": total seconds (e.g. 2:30 = 30)
|
||||
- "distance": distance in meters as a number (e.g. 5000)
|
||||
|
||||
No text, no markdown, no explanation. Just the JSON object.`),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to analyze image"})
|
||||
return
|
||||
}
|
||||
|
||||
if len(message.Content) == 0 {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "empty response from Claude"})
|
||||
return
|
||||
}
|
||||
|
||||
extractedData := ExtractedRowingData{}
|
||||
err = json.Unmarshal([]byte(message.Content[0].Text), &extractedData)
|
||||
if err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to parse JSON response"})
|
||||
return
|
||||
}
|
||||
|
||||
if extractedData.Distance == 0 {
|
||||
ctx.JSON(http.StatusBadRequest, gin.H{"error": "invalid distance in image"})
|
||||
return
|
||||
}
|
||||
|
||||
totalSeconds := extractedData.TimeMinutes*60 + extractedData.TimeSeconds
|
||||
totalDuration := time.Duration(totalSeconds * float64(time.Second))
|
||||
per500m := time.Duration(totalSeconds / extractedData.Distance * 500 * float64(time.Second))
|
||||
|
||||
rowing := models.Rowing{
|
||||
Date: dateTaken,
|
||||
Time: totalDuration,
|
||||
TimePer500m: per500m,
|
||||
Distance: extractedData.Distance,
|
||||
Calories: extractedData.Distance / 7500.0 * 500.0,
|
||||
}
|
||||
|
||||
if err := store.DB.Create(&rowing).Error; err != nil {
|
||||
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "failed to save rowing"})
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, rowing)
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"time"
|
||||
|
||||
"adam-french.co.uk/backend/services"
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
"github.com/zmb3/spotify/v2"
|
||||
spotifyauth "github.com/zmb3/spotify/v2/auth"
|
||||
"gorm.io/gorm"
|
||||
@@ -14,7 +13,6 @@ type Store struct {
|
||||
DB *gorm.DB
|
||||
SpotifyAuth *spotifyauth.Authenticator
|
||||
SpotifyClient *spotify.Client
|
||||
ClaudeClient *anthropic.Client
|
||||
Auth *services.Auth
|
||||
Notes *services.Notes
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
logsDir := "/backend/logs"
|
||||
logFile, err := os.OpenFile(logsDir+"/go.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
@@ -40,18 +39,12 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// SPOTIFY
|
||||
spotifyAuthState := os.Getenv("SPOTIFY_AUTH_STATE")
|
||||
spotifyRedirectURL := os.Getenv("SPOTIFY_REDIRECT_URI")
|
||||
spotifyClientID := os.Getenv("SPOTIFY_CLIENT_ID")
|
||||
spotifyClientSecret := os.Getenv("SPOTIFY_CLIENT_SECRET")
|
||||
spotifyConfig := services.SpotifyConfig{AuthState: spotifyAuthState, RedirectURL: spotifyRedirectURL, ClientID: spotifyClientID, ClientSecret: spotifyClientSecret}
|
||||
spotifyAuth, spotifyClient := services.InitSpotifyAuth(&spotifyConfig)
|
||||
|
||||
// CLAUDE
|
||||
claudeAPIKey := os.Getenv("CLAUDE_API_KEY")
|
||||
claudeConfig := services.ClaudeConfig{APIKey: claudeAPIKey}
|
||||
claudeClient := services.InitClaude(&claudeConfig)
|
||||
spotifyAuth, client := services.InitSpotifyAuth(&spotifyConfig)
|
||||
|
||||
authSecret := os.Getenv("BACKEND_SECRET")
|
||||
domainName := os.Getenv("DOMAIN")
|
||||
@@ -65,7 +58,7 @@ func main() {
|
||||
notesConfig := services.NotesConfig{Dir: notesDir}
|
||||
notes := services.InitNotes(¬esConfig)
|
||||
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes}
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: client, Auth: auth, Notes: notes}
|
||||
|
||||
protected := r.Group("/", store.AuthMiddlewear)
|
||||
|
||||
@@ -73,10 +66,6 @@ func main() {
|
||||
r.GET("/favorites", store.GetFavorites)
|
||||
protected.POST("/favorites", store.CreateFavorite)
|
||||
|
||||
// ROWING
|
||||
r.GET("/rowing", store.GetRowing)
|
||||
protected.POST("/rowing", store.CreateRowing)
|
||||
|
||||
// ACTIVITIES
|
||||
r.GET("/activity", store.GetActivity)
|
||||
protected.POST("/activity", store.CreateActivity)
|
||||
|
||||
@@ -55,14 +55,3 @@ type Favorite struct {
|
||||
Name string `json:"name"`
|
||||
Link *string `json:"link"`
|
||||
}
|
||||
|
||||
type Rowing struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"`
|
||||
Date time.Time `json:"date"`
|
||||
Time time.Duration `json:"time"`
|
||||
TimePer500m time.Duration `json:"timePer500m"`
|
||||
Distance float64 `json:"distance"`
|
||||
Calories float64 `json:"calories"`
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/anthropics/anthropic-sdk-go"
|
||||
"github.com/anthropics/anthropic-sdk-go/option"
|
||||
)
|
||||
|
||||
type ClaudeConfig struct {
|
||||
APIKey string
|
||||
}
|
||||
|
||||
func InitClaude(config *ClaudeConfig) *anthropic.Client {
|
||||
client := anthropic.NewClient(option.WithAPIKey(config.APIKey))
|
||||
return &client
|
||||
}
|
||||
@@ -36,7 +36,6 @@ func migrateDatabase(db *gorm.DB) error {
|
||||
&models.Post{},
|
||||
&models.Activity{},
|
||||
&models.Favorite{},
|
||||
&models.Rowing{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -12,10 +12,11 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
container_name: nginx
|
||||
env_file: ./.env
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
- icecast2
|
||||
- gitea
|
||||
networks:
|
||||
- app-network
|
||||
ports:
|
||||
@@ -33,6 +34,8 @@ services:
|
||||
- ./certbot/conf:/etc/letsencrypt
|
||||
- ./certbot/www:/var/www/certbot
|
||||
entrypoint: ["/entrypoint.sh"]
|
||||
env_file:
|
||||
- .env
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
@@ -41,7 +44,7 @@ services:
|
||||
context: ./backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: "${BACKEND_HOST}"
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
networks:
|
||||
@@ -56,13 +59,15 @@ services:
|
||||
db:
|
||||
image: postgres:16
|
||||
container_name: "${POSTGRES_HOST}"
|
||||
restart: unless-stopped
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
icecast2:
|
||||
build:
|
||||
@@ -76,3 +81,43 @@ services:
|
||||
- ./.env
|
||||
ports:
|
||||
- "${ICECAST_PORT}:${ICECAST_PORT}"
|
||||
|
||||
gitea-runner:
|
||||
image: gitea/act_runner:latest
|
||||
container_name: "${GITEA_RUNNER_HOST}"
|
||||
environment:
|
||||
GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME}
|
||||
CONFIG_FILE: /config.yaml
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN}
|
||||
GITEA_INSTANCE_URL: "http://${GITEA_HOST}:3000"
|
||||
GITEA_RUNNER_LABELS: "self-hosted:host"
|
||||
volumes:
|
||||
- ./gitea-runner/config.yaml:/config.yaml
|
||||
- ./gitea-runner/data:/data
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
gitea:
|
||||
image: docker.gitea.com/gitea:1.25.4-rootless
|
||||
container_name: "${GITEA_HOST}"
|
||||
networks:
|
||||
- app-network
|
||||
environment:
|
||||
- GITEA__database__DB_TYPE=postgres
|
||||
- GITEA__database__HOST=${POSTGRES_HOST}
|
||||
- GITEA__database__NAME=${POSTGRES_GITEA_DB}
|
||||
- GITEA__database__USER=${POSTGRES_USER}
|
||||
- GITEA__database__PASSWD=${POSTGRES_PASSWORD}
|
||||
restart: always
|
||||
volumes:
|
||||
- ./gitea/data:/var/lib/gitea
|
||||
- ./gitea/config:/etc/gitea
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "2222:2222"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
110
gitea-runner/config.yaml
Normal file
110
gitea-runner/config.yaml
Normal file
@@ -0,0 +1,110 @@
|
||||
# Example configuration file, it's safe to copy this as the default config file without any modification.
|
||||
|
||||
# You don't have to copy this file to your instance,
|
||||
# just run `./act_runner generate-config > config.yaml` to generate a config file.
|
||||
|
||||
log:
|
||||
# The level of logging, can be trace, debug, info, warn, error, fatal
|
||||
level: info
|
||||
|
||||
runner:
|
||||
# Where to store the registration result.
|
||||
file: .runner
|
||||
# Execute how many tasks concurrently at the same time.
|
||||
capacity: 1
|
||||
# Extra environment variables to run jobs.
|
||||
envs:
|
||||
A_TEST_ENV_NAME_1: a_test_env_value_1
|
||||
A_TEST_ENV_NAME_2: a_test_env_value_2
|
||||
# Extra environment variables to run jobs from a file.
|
||||
# It will be ignored if it's empty or the file doesn't exist.
|
||||
env_file: .env
|
||||
# The timeout for a job to be finished.
|
||||
# Please note that the Gitea instance also has a timeout (3h by default) for the job.
|
||||
# So the job could be stopped by the Gitea instance if it's timeout is shorter than this.
|
||||
timeout: 3h
|
||||
# The timeout for the runner to wait for running jobs to finish when shutting down.
|
||||
# Any running jobs that haven't finished after this timeout will be cancelled.
|
||||
shutdown_timeout: 0s
|
||||
# Whether skip verifying the TLS certificate of the Gitea instance.
|
||||
insecure: false
|
||||
# The timeout for fetching the job from the Gitea instance.
|
||||
fetch_timeout: 5s
|
||||
# The interval for fetching the job from the Gitea instance.
|
||||
fetch_interval: 2s
|
||||
# The github_mirror of a runner is used to specify the mirror address of the github that pulls the action repository.
|
||||
# It works when something like `uses: actions/checkout@v4` is used and DEFAULT_ACTIONS_URL is set to github,
|
||||
# and github_mirror is not empty. In this case,
|
||||
# it replaces https://github.com with the value here, which is useful for some special network environments.
|
||||
github_mirror: ''
|
||||
# The labels of a runner are used to determine which jobs the runner can run, and how to run them.
|
||||
# Like: "macos-arm64:host" or "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
# Find more images provided by Gitea at https://gitea.com/docker.gitea.com/runner-images .
|
||||
# If it's empty when registering, it will ask for inputting labels.
|
||||
# If it's empty when execute `daemon`, will use labels in `.runner` file.
|
||||
labels:
|
||||
- "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest"
|
||||
- "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04"
|
||||
- "ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04"
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
enabled: true
|
||||
# The directory to store the cache data.
|
||||
# If it's empty, the cache data will be stored in $HOME/.cache/actcache.
|
||||
dir: ""
|
||||
# The host of the cache server.
|
||||
# It's not for the address to listen, but the address to connect from job containers.
|
||||
# So 0.0.0.0 is a bad choice, leave it empty to detect automatically.
|
||||
host: ""
|
||||
# The port of the cache server.
|
||||
# 0 means to use a random available port.
|
||||
port: 0
|
||||
# The external cache server URL. Valid only when enable is true.
|
||||
# If it's specified, act_runner will use this URL as the ACTIONS_CACHE_URL rather than start a server by itself.
|
||||
# The URL should generally end with "/".
|
||||
external_server: ""
|
||||
|
||||
container:
|
||||
# Specifies the network to which the container will connect.
|
||||
# Could be host, bridge or the name of a custom network.
|
||||
# If it's empty, act_runner will create a network automatically.
|
||||
network: ""
|
||||
# Whether to use privileged mode or not when launching task containers (privileged mode is required for Docker-in-Docker).
|
||||
privileged: false
|
||||
# And other options to be used when the container is started (eg, --add-host=my.gitea.url:host-gateway).
|
||||
options:
|
||||
# The parent directory of a job's working directory.
|
||||
# NOTE: There is no need to add the first '/' of the path as act_runner will add it automatically.
|
||||
# If the path starts with '/', the '/' will be trimmed.
|
||||
# For example, if the parent directory is /path/to/my/dir, workdir_parent should be path/to/my/dir
|
||||
# If it's empty, /workspace will be used.
|
||||
workdir_parent:
|
||||
# Volumes (including bind mounts) can be mounted to containers. Glob syntax is supported, see https://github.com/gobwas/glob
|
||||
# You can specify multiple volumes. If the sequence is empty, no volumes can be mounted.
|
||||
# For example, if you only allow containers to mount the `data` volume and all the json files in `/src`, you should change the config to:
|
||||
# valid_volumes:
|
||||
# - data
|
||||
# - /src/*.json
|
||||
# If you want to allow any volume, please use the following configuration:
|
||||
# valid_volumes:
|
||||
# - '**'
|
||||
valid_volumes: []
|
||||
# overrides the docker client host with the specified one.
|
||||
# If it's empty, act_runner will find an available docker host automatically.
|
||||
# If it's "-", act_runner will find an available docker host automatically, but the docker host won't be mounted to the job containers and service containers.
|
||||
# If it's not empty or "-", the specified docker host will be used. An error will be returned if it doesn't work.
|
||||
docker_host: ""
|
||||
# Pull docker image(s) even if already present
|
||||
force_pull: true
|
||||
# Rebuild docker image(s) even if already present
|
||||
force_rebuild: false
|
||||
# Always require a reachable docker daemon, even if not required by act_runner
|
||||
require_docker: false
|
||||
# Timeout to wait for the docker daemon to be reachable, if docker is required by require_docker or act_runner
|
||||
docker_timeout: 0s
|
||||
|
||||
host:
|
||||
# The parent directory of a job's working directory.
|
||||
# If it's empty, $HOME/.cache/act/ will be used.
|
||||
workdir_parent:
|
||||
98
gitea/config/app.ini
Normal file
98
gitea/config/app.ini
Normal file
@@ -0,0 +1,98 @@
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
RUN_USER = git
|
||||
RUN_MODE = prod
|
||||
WORK_PATH = /var/lib/gitea
|
||||
|
||||
[repository]
|
||||
ROOT = /var/lib/gitea/git/repositories
|
||||
|
||||
[repository.local]
|
||||
LOCAL_COPY_PATH = /tmp/gitea/local-repo
|
||||
|
||||
[repository.upload]
|
||||
TEMP_PATH = /tmp/gitea/uploads
|
||||
|
||||
[server]
|
||||
APP_DATA_PATH = /var/lib/gitea
|
||||
SSH_DOMAIN = adam-french.co.uk
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = https://adam-french.co.uk/gitea/
|
||||
DISABLE_SSH = false
|
||||
; In rootless gitea container only internal ssh server is supported
|
||||
START_SSH_SERVER = true
|
||||
SSH_PORT = 2222
|
||||
SSH_LISTEN_PORT = 2222
|
||||
BUILTIN_SSH_SERVER_USER = git
|
||||
LFS_START_SERVER = true
|
||||
DOMAIN = stppi.local
|
||||
LFS_JWT_SECRET = XHIJprS_aMv0tizioZpUD38GGqTtNMFXMz1R6LuPvjU
|
||||
OFFLINE_MODE = true
|
||||
|
||||
[database]
|
||||
PATH = /var/lib/gitea/data/gitea.db
|
||||
DB_TYPE = postgres
|
||||
HOST = db
|
||||
NAME = gitea
|
||||
USER = postgres
|
||||
PASSWD = password
|
||||
SCHEMA =
|
||||
SSL_MODE = disable
|
||||
LOG_SQL = false
|
||||
|
||||
[session]
|
||||
PROVIDER_CONFIG = /var/lib/gitea/data/sessions
|
||||
PROVIDER = file
|
||||
|
||||
[picture]
|
||||
AVATAR_UPLOAD_PATH = /var/lib/gitea/data/avatars
|
||||
REPOSITORY_AVATAR_UPLOAD_PATH = /var/lib/gitea/data/repo-avatars
|
||||
|
||||
[attachment]
|
||||
PATH = /var/lib/gitea/data/attachments
|
||||
|
||||
[log]
|
||||
ROOT_PATH = /var/lib/gitea/data/log
|
||||
MODE = console
|
||||
LEVEL = info
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
SECRET_KEY =
|
||||
REVERSE_PROXY_LIMIT = 1
|
||||
REVERSE_PROXY_TRUSTED_PROXIES = *
|
||||
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3NzEyNDMyMTd9.yHsgFcEwDNWmZebftpe8tpWRFa5aR5tkpQuVYybeVaY
|
||||
PASSWORD_HASH_ALGO = pbkdf2
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = true
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
REGISTER_EMAIL_CONFIRM = false
|
||||
ENABLE_NOTIFY_MAIL = false
|
||||
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
|
||||
ENABLE_CAPTCHA = false
|
||||
DEFAULT_KEEP_EMAIL_PRIVATE = false
|
||||
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
|
||||
DEFAULT_ENABLE_TIMETRACKING = true
|
||||
NO_REPLY_ADDRESS = noreply.localhost
|
||||
|
||||
[lfs]
|
||||
PATH = /var/lib/gitea/git/lfs
|
||||
|
||||
[mailer]
|
||||
ENABLED = false
|
||||
|
||||
[openid]
|
||||
ENABLE_OPENID_SIGNIN = true
|
||||
ENABLE_OPENID_SIGNUP = true
|
||||
|
||||
[cron.update_checker]
|
||||
ENABLED = false
|
||||
|
||||
[repository.pull-request]
|
||||
DEFAULT_MERGE_STYLE = merge
|
||||
|
||||
[repository.signing]
|
||||
DEFAULT_TRUST_MODEL = committer
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = pYiwW8xxGi23gysl2pa-02Cf567Z5ERvR6DDFGIn2iQ
|
||||
@@ -4,7 +4,7 @@ set -e
|
||||
# Check if certificate exists
|
||||
if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then
|
||||
echo "Certificates found. Using production nginx config."
|
||||
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT}' \
|
||||
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \
|
||||
< /etc/nginx/nginx.conf.template \
|
||||
> /etc/nginx/nginx.conf
|
||||
else
|
||||
|
||||
@@ -98,6 +98,18 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /gitea {
|
||||
return 301 /gitea/;
|
||||
}
|
||||
|
||||
location /gitea/ {
|
||||
proxy_pass http://$GITEA_HOST:$GITEA_PORT/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
69
nginx/vue/package-lock.json
generated
69
nginx/vue/package-lock.json
generated
@@ -10,12 +10,14 @@
|
||||
"dependencies": {
|
||||
"@mdit/plugin-katex": "^0.24.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.2",
|
||||
"katex": "^0.16.27",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-wikilinks": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
@@ -1631,6 +1633,12 @@
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web-bluetooth": {
|
||||
"version": "0.0.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz",
|
||||
@@ -1916,6 +1924,44 @@
|
||||
"integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
|
||||
"integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/web-bluetooth": "^0.0.21",
|
||||
"@vueuse/metadata": "14.2.1",
|
||||
"@vueuse/shared": "14.2.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/metadata": {
|
||||
"version": "14.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
|
||||
"integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/shared": {
|
||||
"version": "14.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
|
||||
"integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansis": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz",
|
||||
@@ -1939,13 +1985,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
|
||||
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
|
||||
"version": "1.13.5",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
|
||||
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
@@ -3382,6 +3428,19 @@
|
||||
"utf8-byte-length": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
"dependencies": {
|
||||
"@mdit/plugin-katex": "^0.24.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"axios": "^1.13.2",
|
||||
"katex": "^0.16.27",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-wikilinks": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
|
||||
@@ -279,8 +279,8 @@ td {
|
||||
background-position: 0 0;
|
||||
|
||||
mask-image: linear-gradient(
|
||||
30deg,
|
||||
-180deg,
|
||||
rgba(1, 1, 1, 1) 0%,
|
||||
rgba(1, 1, 1, 0.9) 100%
|
||||
rgba(1, 1, 1, 0.92) 100%
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,14 +49,10 @@ const faces_string = faces.join(" ");
|
||||
<RouterLink class="bdr-2 bg-bg_primary" to="/" v-if="!inHome">
|
||||
<a>HOME</a>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
class="bdr-2 bg-bg_primary"
|
||||
v-if="parentPath"
|
||||
:to="parentPath"
|
||||
>
|
||||
<RouterLink class="bdr-2 bg-bg_primary" v-if="parentPath" :to="parentPath">
|
||||
<a>UP</a>
|
||||
</RouterLink>
|
||||
<Headline class="border flex-1">
|
||||
<Headline class="border flex-1 max-w-full">
|
||||
<code class="whitespace-pre">{{ faces_string }}</code>
|
||||
</Headline>
|
||||
</nav>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, useTemplateRef, onUnmounted } from "vue";
|
||||
import { onMounted, useTemplateRef, onUnmounted } from "vue";
|
||||
|
||||
const container = useTemplateRef("container");
|
||||
const item1 = useTemplateRef("item1");
|
||||
const item2 = useTemplateRef("item2");
|
||||
|
||||
let offset = 0;
|
||||
|
||||
@@ -14,7 +13,6 @@ const speed = 0.5; // pixels per frame
|
||||
function animate() {
|
||||
const ctnr = container.value;
|
||||
const it1 = item1.value;
|
||||
const it2 = item2.value;
|
||||
|
||||
const width = Math.max(ctnr.offsetWidth, it1.scrollWidth);
|
||||
|
||||
@@ -24,8 +22,7 @@ function animate() {
|
||||
offset += width;
|
||||
}
|
||||
|
||||
it1.style.transform = `translateX(${offset}px)`;
|
||||
it2.style.transform = `translateX(${width + offset}px)`;
|
||||
ctnr.style.transform = `translateX(${offset}px)`;
|
||||
|
||||
rafId = requestAnimationFrame(animate);
|
||||
}
|
||||
@@ -40,40 +37,32 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="marquee">
|
||||
<div class="root">
|
||||
<div class="container" ref="container">
|
||||
<div class="item" ref="item1"><slot /></div>
|
||||
<div class="item item2" ref="item2"><slot /></div>
|
||||
<div ref="item1">
|
||||
<slot />
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.marquee {
|
||||
.root {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
position: relative;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.item {
|
||||
height: fit-content;
|
||||
top: 0px;
|
||||
padding-right: 3em;
|
||||
width: fit-content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item1 {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.item2 {
|
||||
position: absolute;
|
||||
height: fit-content;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: max-content;
|
||||
/* Each column fits its content */
|
||||
overflow-x: visible;
|
||||
will-change: transform;
|
||||
gap: 10em;
|
||||
}
|
||||
</style>
|
||||
|
||||
42
nginx/vue/src/views/home/BadApple.vue
Normal file
42
nginx/vue/src/views/home/BadApple.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import { useTemplateRef, ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const display = useTemplateRef('display')
|
||||
const displayText = ref("");
|
||||
|
||||
const charHeight: number = 14;
|
||||
const charWidth: number = charHeight * 0.6;
|
||||
let n: number;
|
||||
let m: number;
|
||||
|
||||
function setup() {
|
||||
display.value.style.fontSize = `${charHeight}px`;
|
||||
display.value.style.lineHeight = `${charHeight}px`;
|
||||
fillDisplay()
|
||||
}
|
||||
|
||||
function fillDisplay() {
|
||||
// M rows N columns
|
||||
m = Math.floor(display.value.offsetHeight / charHeight);
|
||||
n = Math.floor(display.value.offsetWidth / charWidth);
|
||||
const row = ' '.repeat(n);
|
||||
displayText.value = (row + '\n').repeat(m - 1) + row
|
||||
}
|
||||
|
||||
function close() {
|
||||
displayText.value = ""
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setup()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
close()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<pre class="overflow-scroll w-full h-full bg-black text-white m-0 p-0" id="container" ref="display">{{ displayText
|
||||
}}</pre>
|
||||
</template>
|
||||
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import Timer from "@/components/util/Timer.vue";
|
||||
import Elle from "@/components/elle/Elle.vue";
|
||||
import Time from "@/components/util/Time.vue";
|
||||
import Elle from "@/components/elle/Elle.vue";
|
||||
import Chat from "@/components/util/Chat.vue";
|
||||
import MusicPlayer from "@/components/util/MusicPlayer.vue";
|
||||
|
||||
import Intro from "./Intro.vue";
|
||||
import Intro2 from "./Intro2.vue";
|
||||
import BadApple from "./BadApple.vue";
|
||||
import Stamps from "./Stamps.vue";
|
||||
import Listening from "./Listening.vue";
|
||||
import Links from "./Links.vue";
|
||||
@@ -14,15 +16,15 @@ import Collage from "./Collage.vue";
|
||||
import Favorites from "./Favorites.vue";
|
||||
import Gym from "./Gym.vue";
|
||||
import Consumption from "./Consumption.vue";
|
||||
|
||||
import UtenaFrame from "@/components/borders/UtenaFrame.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="halftone justify-center flex flex-row w-full h-full">
|
||||
<div class="h-fit flex flex-row">
|
||||
<div class="a4page-portrait homeGrid relative bdr-1">
|
||||
<Intro class="intro" />
|
||||
<!-- <Intro class="intro" /> -->
|
||||
<!-- <Intro2 class="intro" /> -->
|
||||
<BadApple class="intro" />
|
||||
<Listening class="listening" />
|
||||
<Stamps class="stamps" />
|
||||
<Feed class="feed" />
|
||||
@@ -32,21 +34,16 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
|
||||
<Favorites class="favorites" />
|
||||
<Gym class="gym" />
|
||||
</div>
|
||||
<div
|
||||
class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60 border-2"
|
||||
>
|
||||
<div class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60 ">
|
||||
<div class="flex flex-col flex-1">
|
||||
<Time class="bg-bg_primary border-primary border-b" />
|
||||
<Timer class="border-primary border-b bg-bg_primary" />
|
||||
<Time class="bg-bg_primary border-primary border" />
|
||||
<Timer class="border-primary border bg-bg_primary" />
|
||||
<!-- <Elle class="flex-1" /> -->
|
||||
<!-- <Chat class="bdr-2 bg-bg_primary" /> -->
|
||||
<!-- <MusicPlayer /> -->
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="/img/memes/fire-woman.gif"
|
||||
class="border-tertiary border"
|
||||
/>
|
||||
<img src="/img/memes/fire-woman.gif" class="border-tertiary border" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,6 +73,7 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
.tr,
|
||||
.br,
|
||||
.sidebar {
|
||||
@@ -117,6 +115,7 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
|
||||
grid-column: span 4;
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.gym {
|
||||
grid-column: span 3;
|
||||
grid-row: span 2;
|
||||
|
||||
@@ -4,34 +4,27 @@ import Paragraph from "@/components/text/Paragraph.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start"
|
||||
>
|
||||
<Header>Intro</Header>
|
||||
<Paragraph>
|
||||
Hi, I'm Adam, thank you for visiting my website. I'm currently a 20
|
||||
something graduate looking for work. I like to game, listen to lots
|
||||
of music and occasionally watch anime.
|
||||
</Paragraph>
|
||||
<Header>Getting around</Header>
|
||||
<Paragraph>
|
||||
Pages available can be traversed through links below. I am hoping to
|
||||
add some shrines, code-walkthoughs, live chat and page transitions
|
||||
at a later date.
|
||||
</Paragraph>
|
||||
<Header>Contact</Header>
|
||||
<Paragraph>
|
||||
Please email me <a href="mailto:adam.a.french@outlook.com">here</a>,
|
||||
or contact me though any of the social medias linked.
|
||||
</Paragraph>
|
||||
<Header>A Quote</Header>
|
||||
<!-- <p>
|
||||
What makes me a good demoman? If I were a bad demoman, I wouldn't be
|
||||
sittin' here discussin' it with you, now would I?!
|
||||
</p> -->
|
||||
<Paragraph>
|
||||
One crossed wire, one wayward pinch of potassium chlorate, one
|
||||
errant twitch, and KA-BLOOIE!
|
||||
</Paragraph>
|
||||
<div class="flex-1 border-box flex flex-col p-1 text-left items-start justify-start">
|
||||
<Header>Yo</Header>
|
||||
<!-- <Header>Intro</Header> -->
|
||||
<!-- <Paragraph> -->
|
||||
<!-- Hi, I'm Adam, thank you for visiting my website. -->
|
||||
<!-- </Paragraph> -->
|
||||
<!-- <Header>Getting around</Header> -->
|
||||
<!-- <Paragraph> -->
|
||||
<!-- Pages available can be traversed through links below. I am hoping to -->
|
||||
<!-- add some shrines, code-walkthoughs, live chat and page transitions -->
|
||||
<!-- at a later date. -->
|
||||
<!-- </Paragraph> -->
|
||||
<!-- <Header>Contact</Header> -->
|
||||
<!-- <Paragraph> -->
|
||||
<!-- Please email me <a href="mailto:adam.a.french@outlook.com">here</a>, -->
|
||||
<!-- or contact me though any of the social medias linked. -->
|
||||
<!-- </Paragraph> -->
|
||||
<!-- <Header>A Quote</Header> -->
|
||||
<!-- <Paragraph> -->
|
||||
<!-- One crossed wire, one wayward pinch of potassium chlorate, one -->
|
||||
<!-- errant twitch, and KA-BLOOIE! -->
|
||||
<!-- </Paragraph> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
82
nginx/vue/src/views/home/Intro2.vue
Normal file
82
nginx/vue/src/views/home/Intro2.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { rand } from '@vueuse/core'
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
|
||||
interface Item {
|
||||
x: number
|
||||
y: number
|
||||
dx: number
|
||||
dy: number
|
||||
content: string
|
||||
}
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null)
|
||||
const itemEls = ref<HTMLDivElement[]>([])
|
||||
|
||||
const phrases = [
|
||||
'Welcome to my website',
|
||||
'Thank you for visiting',
|
||||
"I'd love to know your recommendations",
|
||||
"Message me on discord or steam",
|
||||
"I like anime, all kinds of music and sci fic",
|
||||
"Try to stay away from instagram",
|
||||
"Always watching too much youtube",
|
||||
]
|
||||
|
||||
const items = ref<Item[]>(
|
||||
phrases.map((text, i) => ({
|
||||
x: i * 20,
|
||||
y: i * 20,
|
||||
dx: rand(0, 100) / 100,
|
||||
dy: 0.5,
|
||||
content: text,
|
||||
}))
|
||||
)
|
||||
|
||||
let rafId = 0
|
||||
|
||||
function animate() {
|
||||
const c = container.value
|
||||
if (!c) return
|
||||
|
||||
const cw = c.clientWidth
|
||||
const ch = c.clientHeight
|
||||
|
||||
items.value.forEach((item, i) => {
|
||||
const el = itemEls.value[i]
|
||||
if (!el) return
|
||||
|
||||
const ew = el.offsetWidth
|
||||
const eh = el.offsetHeight
|
||||
|
||||
item.x += item.dx
|
||||
item.y += item.dy
|
||||
|
||||
if (item.x < 0 || item.x > cw - ew) item.dx *= -1
|
||||
if (item.y < 0 || item.y > ch - eh) item.dy *= -1
|
||||
})
|
||||
|
||||
rafId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
rafId = requestAnimationFrame(animate)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="container" class="w-full h-full relative overflow-hidden">
|
||||
<div v-for="(item, i) in items" :key="i" ref="itemEls" class=" absolute w-fit h-fit" :style="{
|
||||
transform: `translate(${item.x}px, ${item.y}px)`
|
||||
}">
|
||||
<h1>
|
||||
{{ item.content }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup>
|
||||
import RouterTable from "@/components/util/RouterTable.vue";
|
||||
import LinkTable from "@/components/util/LinkTable.vue";
|
||||
import Markdown from "@/components/util/Markdown.vue";
|
||||
|
||||
const site_links = [
|
||||
{ name: "CV", link: "/cv" },
|
||||
|
||||
Reference in New Issue
Block a user