Compare commits

29 Commits

Author SHA1 Message Date
ac171f7846 yes
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m24s
Merge branch 'main' of ssh://adam-french.co.uk:2222/adamf/web_server
2026-02-18 21:09:20 +00:00
b5b86a2a37 revert to old website intro 2026-02-18 21:08:33 +00:00
cfdb5b4d50 Update .gitea/workflows/deploy.yaml
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-18 16:38:06 +00:00
37580cdc42 Stop da haxxers
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3s
2026-02-18 16:26:52 +00:00
711236b776 fix actions
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m35s
2026-02-18 16:21:52 +00:00
75454c2ed8 fix actions
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 5s
2026-02-18 16:19:58 +00:00
78c824c4c8 remove mountpoint
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2m56s
2026-02-17 18:37:46 +00:00
ba3b933068 remove mountpoint
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 5s
2026-02-17 18:36:23 +00:00
14c430bbad adding gitea actions
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s
2026-02-17 18:34:55 +00:00
26c7422e34 adding gitea-runner service 2026-02-17 18:10:19 +00:00
470b1c79d8 fix ssh domain for gitea 2026-02-16 16:57:20 +00:00
d849b606ec fix certbot env vars 2026-02-16 15:07:44 +00:00
46a9da4c90 gitea runner 2026-02-16 14:52:33 +00:00
398a610cb2 fix root of gitea 2026-02-16 13:58:09 +00:00
b506bae515 fix gitea 2026-02-16 13:57:28 +00:00
11ad0b5a83 reverse proxy to gitea 2026-02-16 13:21:11 +00:00
d7393e1419 Revert "Revert "idk what I changed""
This reverts commit 0d32333c0c.
2026-02-16 13:03:13 +00:00
0d32333c0c Revert "idk what I changed"
This reverts commit 7dc3f49273.
2026-02-16 13:02:31 +00:00
050a38a76f Merge branch 'main' of /home/adamf/repos/web_server 2026-02-16 12:56:23 +00:00
bc43e9ed02 add gitea 2026-02-16 12:52:08 +00:00
75b8b02825 ignore gitea volume 2026-02-16 12:51:48 +00:00
5c69a1d0a7 adding gitea 2026-02-16 11:46:49 +00:00
aa915e1071 Merge branch 'main' of 192.168.178.24:~/repos/web_server
ye
2026-02-14 22:03:47 +00:00
7dc3f49273 idk what I changed 2026-02-14 22:03:32 +00:00
21d3997a16 remove ts 2026-02-10 17:04:52 +00:00
c56ba217dd remove tsconfig 2026-02-10 17:01:26 +00:00
91804f1fe7 adding vueuse 2026-02-10 17:00:59 +00:00
7e74ce5a2a adding typescript 2026-02-10 16:57:00 +00:00
e92ac49140 new components 2026-02-10 16:46:49 +00:00
25 changed files with 604 additions and 370 deletions

View 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
View File

@@ -3,6 +3,9 @@ certbot/www
backend/token/
.env
gitea/data/*
gitea-runner/data/*
# Logs
logs
*.log

View File

@@ -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

View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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(&notesConfig)
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)

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -36,7 +36,6 @@ func migrateDatabase(db *gorm.DB) error {
&models.Post{},
&models.Activity{},
&models.Favorite{},
&models.Rowing{},
)
if err != nil {
return err

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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;
}
}
}

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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%
);
}

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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,8 +16,6 @@ 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>
@@ -23,6 +23,8 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
<div class="h-fit flex flex-row">
<div class="a4page-portrait homeGrid relative bdr-1">
<Intro class="intro" />
<!-- <Intro2 class="intro" /> -->
<!-- <BadApple class="intro" /> -->
<Listening class="listening" />
<Stamps class="stamps" />
<Feed class="feed" />
@@ -33,11 +35,11 @@ import UtenaFrame from "@/components/borders/UtenaFrame.vue";
<Gym class="gym" />
</div>
<div
class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60 border-2"
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 /> -->
@@ -117,6 +119,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;

View File

@@ -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>

View 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>

View File

@@ -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" },