Compare commits
36 Commits
cfdb5b4d50
...
rowing_fro
| Author | SHA1 | Date | |
|---|---|---|---|
| e25fc5f1d1 | |||
| 5bcc65668e | |||
| 2c1ecce99a | |||
| 095cd72946 | |||
| 1d4beca336 | |||
| f2ba3494b1 | |||
| d56bd5783d | |||
| f60636942f | |||
| b087172bb1 | |||
| 0c93c6bc27 | |||
| 48ae2f59ea | |||
| c9faa90abd | |||
| ef78974744 | |||
| 49499052b0 | |||
| dbb4914745 | |||
| 34fa96ddab | |||
| 8a9f3c373d | |||
| dc05ade798 | |||
| 1e47919a40 | |||
| 8e9734fca7 | |||
| da9a083f2d | |||
| 3c40eb9f08 | |||
| e016e3af46 | |||
| 0c91f512b4 | |||
| f63b61431b | |||
| f3ea83c477 | |||
| 4b5ed4787a | |||
| 747a403bcb | |||
| fe16ccab97 | |||
| 7bcb485fc6 | |||
| a3d73b12f4 | |||
| 47a8e6c35e | |||
| f885ff9175 | |||
| d574fa7692 | |||
| ac171f7846 | |||
| b5b86a2a37 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,6 +6,9 @@ backend/token/
|
||||
gitea/data/*
|
||||
gitea-runner/data/*
|
||||
|
||||
# Will add in future (webpack)
|
||||
nginx/vue/crates/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
@@ -7,12 +7,13 @@ 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.0.0-20210810183815-faf39c7919d5
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
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
|
||||
@@ -23,7 +24,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.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // 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
|
||||
@@ -40,6 +41,11 @@ 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,6 +33,8 @@ 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=
|
||||
@@ -102,6 +104,8 @@ 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=
|
||||
@@ -172,6 +176,8 @@ 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=
|
||||
@@ -183,6 +189,16 @@ 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=
|
||||
@@ -287,6 +303,8 @@ 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=
|
||||
|
||||
154
backend/handlers/handle_rowing.go
Normal file
154
backend/handlers/handle_rowing.go
Normal file
@@ -0,0 +1,154 @@
|
||||
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,6 +4,7 @@ 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"
|
||||
@@ -13,6 +14,7 @@ type Store struct {
|
||||
DB *gorm.DB
|
||||
SpotifyAuth *spotifyauth.Authenticator
|
||||
SpotifyClient *spotify.Client
|
||||
ClaudeClient *anthropic.Client
|
||||
Auth *services.Auth
|
||||
Notes *services.Notes
|
||||
|
||||
|
||||
@@ -39,12 +39,18 @@ 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, client := services.InitSpotifyAuth(&spotifyConfig)
|
||||
spotifyAuth, spotifyClient := services.InitSpotifyAuth(&spotifyConfig)
|
||||
|
||||
// CLAUDE
|
||||
claudeAPIKey := os.Getenv("CLAUDE_API_KEY")
|
||||
claudeConfig := services.ClaudeConfig{APIKey: claudeAPIKey}
|
||||
claudeClient := services.InitClaude(&claudeConfig)
|
||||
|
||||
authSecret := os.Getenv("BACKEND_SECRET")
|
||||
domainName := os.Getenv("DOMAIN")
|
||||
@@ -58,7 +64,7 @@ func main() {
|
||||
notesConfig := services.NotesConfig{Dir: notesDir}
|
||||
notes := services.InitNotes(¬esConfig)
|
||||
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: client, Auth: auth, Notes: notes}
|
||||
store := handlers.Store{DB: db, SpotifyAuth: spotifyAuth, SpotifyClient: spotifyClient, ClaudeClient: claudeClient, Auth: auth, Notes: notes}
|
||||
|
||||
protected := r.Group("/", store.AuthMiddlewear)
|
||||
|
||||
@@ -66,6 +72,10 @@ 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,3 +55,14 @@ 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"`
|
||||
}
|
||||
|
||||
15
backend/services/claude.go
Normal file
15
backend/services/claude.go
Normal file
@@ -0,0 +1,15 @@
|
||||
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,6 +36,7 @@ func migrateDatabase(db *gorm.DB) error {
|
||||
&models.Post{},
|
||||
&models.Activity{},
|
||||
&models.Favorite{},
|
||||
&models.Rowing{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
BIN
nginx/vue/public/img/screenshot.png
Normal file
BIN
nginx/vue/public/img/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 650 KiB |
@@ -9,9 +9,10 @@ import { useTemplateRef, onMounted, onBeforeUnmount } from "vue";
|
||||
|
||||
const container = useTemplateRef("container");
|
||||
|
||||
const SPEED = 1; // px per frame
|
||||
const SPEED = 0.0005; // % per frame
|
||||
const PAUSE = 2000; // ms at top/bottom
|
||||
|
||||
let pos = 0;
|
||||
let direction = 1; // 1 = down, -1 = up
|
||||
let timeoutId;
|
||||
let timeoutId2;
|
||||
@@ -27,17 +28,26 @@ function handleHover() {
|
||||
|
||||
function tick() {
|
||||
const el = container.value;
|
||||
el.scrollTop += SPEED * direction;
|
||||
|
||||
const reachedBottom = el.scrollTop + el.clientHeight >= el.scrollHeight;
|
||||
const reachedTop = el.scrollTop <= 0;
|
||||
const reachedBottom = pos <= 0;
|
||||
const reachedTop = pos >= 1;
|
||||
|
||||
if (reachedBottom || reachedTop) {
|
||||
direction *= -1;
|
||||
if (reachedBottom) {
|
||||
pos = 0.001;
|
||||
direction = 1;
|
||||
handleHover();
|
||||
return;
|
||||
} else if (reachedTop) {
|
||||
pos = 0.999;
|
||||
direction = -1;
|
||||
handleHover();
|
||||
return;
|
||||
}
|
||||
|
||||
pos += direction * SPEED;
|
||||
|
||||
el.scrollTop = pos * el.scrollHeight;
|
||||
|
||||
timeoutId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
|
||||
58
nginx/vue/src/components/util/CommitHistory.vue
Normal file
58
nginx/vue/src/components/util/CommitHistory.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<script setup>
|
||||
import axios from "axios";
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const url =
|
||||
"https://www.adam-french.co.uk/gitea/api/v1/users/adamf/activities/feeds?limit=1";
|
||||
|
||||
const feed = ref(null);
|
||||
const isLoading = ref(true);
|
||||
const hasError = ref(false);
|
||||
|
||||
async function checkFeed() {
|
||||
try {
|
||||
const res = await axios.get(url);
|
||||
feed.value = res.data[0] || null;
|
||||
hasError.value = false;
|
||||
} catch (err) {
|
||||
hasError.value = true;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkFeed();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="justify-center text-center">
|
||||
<div v-if="isLoading">
|
||||
<p>Loading latest activity...</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasError">
|
||||
<p>Could not fetch feed. Please try again later.</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="feed" class="flex-col justify-center flex">
|
||||
<h3>Last git activity</h3>
|
||||
<img
|
||||
:src="feed.act_user.avatar_url"
|
||||
alt="User avatar"
|
||||
class="avatar"
|
||||
/>
|
||||
<a :href="feed.repo.html_url">
|
||||
<h3>repo: {{ feed.repo.full_name }}</h3>
|
||||
</a>
|
||||
<p>Action: {{ feed.op_type }}</p>
|
||||
<p>Message: {{ JSON.parse(feed.content).Commits[0].Message }}</p>
|
||||
<small> {{ new Date(feed.created).toLocaleString() }}</small>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p>No activity found.</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -13,7 +13,7 @@ const keys = ["name", "link"];
|
||||
|
||||
<template>
|
||||
<a v-for="(row, rowIndex) in linkArr" :key="rowIndex" :href="row.link">
|
||||
<p class="bdr-2 bg-bg_primary">
|
||||
<p class="bdr-2 bg-bg_tertiary">
|
||||
{{ row.name }}
|
||||
</p>
|
||||
</a>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<div v-else>
|
||||
<img src="/img/tmpen31z3pe.PNG" />
|
||||
<div class="m-1">
|
||||
<p>Stream is offline. Tune in Fridays @ 6:00pm, Monday @ 8:00am</p>
|
||||
<Button @click="checkStream()">Check Stream</Button>
|
||||
<p>Radio is offline. Message for info!</p>
|
||||
<Button class="w-full" @click="checkStream()">Check Stream</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,64 +2,64 @@ import { createRouter, createWebHistory } from "vue-router";
|
||||
import Home from "@/views/home/Home.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: "/cv",
|
||||
name: "cv",
|
||||
component: () => import("../views/CV.vue"),
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: () => import("../views/Admin.vue"),
|
||||
},
|
||||
{
|
||||
path: "/bookmarks",
|
||||
name: "bookmarks",
|
||||
component: () => import("../views/Bookmarks.vue"),
|
||||
},
|
||||
{
|
||||
path: "/notes/:path(.*)*",
|
||||
name: "notes",
|
||||
component: () => import("../views/Notes.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines",
|
||||
name: "shrine links",
|
||||
component: () => import("../views/Shrines.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/gto",
|
||||
name: "gto shrine",
|
||||
component: () => import("../views/shrines/GTO.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/skipskipbenben",
|
||||
name: "skipskipbenben shrine",
|
||||
component: () => import("../views/shrines/Skipskipbenben.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/evangelion",
|
||||
name: "evangelion shrine",
|
||||
component: () => import("../views/shrines/Evangelion.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/demoman",
|
||||
name: "demoman shrine",
|
||||
component: () => import("../views/shrines/Demoman.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "404",
|
||||
component: () => import("../views/404.vue"),
|
||||
},
|
||||
],
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: "/cv",
|
||||
name: "cv",
|
||||
component: () => import("../views/CV/CV.vue"),
|
||||
},
|
||||
{
|
||||
path: "/admin",
|
||||
name: "admin",
|
||||
component: () => import("../views/admin/Admin.vue"),
|
||||
},
|
||||
{
|
||||
path: "/bookmarks",
|
||||
name: "bookmarks",
|
||||
component: () => import("../views/Bookmarks.vue"),
|
||||
},
|
||||
{
|
||||
path: "/notes/:path(.*)*",
|
||||
name: "notes",
|
||||
component: () => import("../views/Notes.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines",
|
||||
name: "shrine links",
|
||||
component: () => import("../views/Shrines.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/gto",
|
||||
name: "gto shrine",
|
||||
component: () => import("../views/shrines/GTO.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/skipskipbenben",
|
||||
name: "skipskipbenben shrine",
|
||||
component: () => import("../views/shrines/Skipskipbenben.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/evangelion",
|
||||
name: "evangelion shrine",
|
||||
component: () => import("../views/shrines/Evangelion.vue"),
|
||||
},
|
||||
{
|
||||
path: "/shrines/demoman",
|
||||
name: "demoman shrine",
|
||||
component: () => import("../views/shrines/Demoman.vue"),
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
name: "404",
|
||||
component: () => import("../views/404.vue"),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,603 +0,0 @@
|
||||
<template>
|
||||
<main>
|
||||
<div class="a4page">
|
||||
<div class="contact">
|
||||
<h1>Adam French</h1>
|
||||
<!-- <a href="covers.html"><img width=25 height=50 src="img/rune.png"></a> -->
|
||||
<div class="contact-details">
|
||||
<p>+447563266931</p>
|
||||
<p>adam.a.french@outlook.com</p>
|
||||
<p>www.adam-french.co.uk</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Profile</h2>
|
||||
<p>
|
||||
Recently graduated from the University of Leeds with a BSc
|
||||
Computer Science with Mathematics (International) degree.
|
||||
Currently self-studying and building projects aligned with the
|
||||
type of roles I am seeking. I have a strong background across a
|
||||
variety of programming languages and will be able to quickly get
|
||||
on board with any codebase.
|
||||
</p>
|
||||
<p>
|
||||
I am most keen to work for a company with altruistic values and
|
||||
a focus on durable solutions. Looking forward to learning from
|
||||
experts and collaborating with motivated individuals.
|
||||
</p>
|
||||
|
||||
<h2>Personal Projects</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Project</th>
|
||||
<th>Skills</th>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Personal Websites</td>
|
||||
<td>Nginx, Vue, Postgres, Docker, Go, Python</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
My personal site, Currently
|
||||
<b>self hosted</b>
|
||||
using <b>listed skills</b>. In the past, I have used
|
||||
Svelte, React/Redux, SQLite, Rust and Deno.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Computer Graphics</td>
|
||||
<td>Rust, Linear Algebra, Multithreading</td>
|
||||
<td>2023</td>
|
||||
<td class="row-leftalign">
|
||||
A multithreaded, recursive ray tracer implemented in
|
||||
Rust.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mobile Automata</td>
|
||||
<td>Mathematica, JS</td>
|
||||
<td>2024</td>
|
||||
<td class="row-leftalign">
|
||||
Investigated properties of cellular automata by
|
||||
observing emergent behaviors through custom
|
||||
simulations.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Arduino Programming & Circuits</td>
|
||||
<td>C++, Soldering, Embedded Systems</td>
|
||||
<td>2022 - 2025</td>
|
||||
<td class="row-leftalign">
|
||||
Created decorations using salvaged components from
|
||||
discarded electronics.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Memory Palace Website</td>
|
||||
<td>TS, Rust, React, Redux, SQLite</td>
|
||||
<td>2025</td>
|
||||
<td class="row-leftalign">
|
||||
Full-stack web application aiming to make the
|
||||
“memory palace” memorization technique easy.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3D Printing</td>
|
||||
<td>FreeCAD</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Designing quality of life objects using FreeCAD and
|
||||
printing with a BambuLab A1.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Education</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>The University of Leeds</td>
|
||||
<td>
|
||||
<!-- <div style="display: flex; flex-direction: column; align-items: center;"> -->
|
||||
<!-- <span>2021</span> -->
|
||||
<!-- <span>to</span> -->
|
||||
<!-- <span>2025</span> -->
|
||||
<!-- </div> -->
|
||||
2021-2025
|
||||
</td>
|
||||
<td class="row-leftalign">
|
||||
<strong
|
||||
>BSc Computer Science with Mathematics
|
||||
(International)</strong
|
||||
><br />
|
||||
<strong
|
||||
>Average:
|
||||
81.1%           (First
|
||||
Class Honours) </strong
|
||||
><br />
|
||||
<strong>Relevant Courses: </strong>
|
||||
Procedural Programming, Object Oriented Programming,
|
||||
Algorithms and Data Structures I & II, Databases,
|
||||
Computer Processors, Compiler Design and
|
||||
Construction, Formal Languages and Finite Automata,
|
||||
Probability and Statistics I, Machine Learning,
|
||||
Graph Algorithms & Complexity Theory
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>The University of Waterloo</td>
|
||||
<td>
|
||||
<!-- <div style="display: flex; flex-direction: column; align-items: center;"> -->
|
||||
<!-- <span>2023</span> -->
|
||||
<!-- <span>to</span> -->
|
||||
<!-- <span>2024</span> -->
|
||||
<!-- </div> -->
|
||||
2023-2024
|
||||
</td>
|
||||
<td class="row-leftalign">
|
||||
<strong>Average: 74.5%</strong>
|
||||
<br />
|
||||
<strong>Relevant Courses:</strong>
|
||||
Applied Cryptography, Introduction to Computer
|
||||
Graphics, Introduction to Rings and Fields with
|
||||
Applications<br /><br />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="a4page">
|
||||
<h2>Experience</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Role</th>
|
||||
<th>Location</th>
|
||||
<th>Date</th>
|
||||
<th>Duties</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Student</td>
|
||||
<td>Wolfram Summer School</td>
|
||||
<td>2024</td>
|
||||
<td class="row-leftalign">
|
||||
Designed and completed a time-constrained research
|
||||
project exploring Mobile Automata and conditions for
|
||||
computational reversibility. Communicated findings
|
||||
through visualizations and presentations.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bartender, Waiter, Cashier</td>
|
||||
<td>Hospitality Venues</td>
|
||||
<td>2018-2023</td>
|
||||
<td class="row-leftalign">
|
||||
Delivered heartfelt customer service in various
|
||||
fast-paced, high-pressure hospitality environments.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Commitments</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Activity</th>
|
||||
<th>Date</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Learning Mandarin</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Aiming to complete HSK 3 proficiency exam by
|
||||
December 2026
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr> -->
|
||||
<!-- <td>Cybersecurity Training</td> -->
|
||||
<!-- <td>Ongoing</td> -->
|
||||
<!-- <td class="row-leftalign"> -->
|
||||
<!-- Using <em>pwn.college, tryhackme.com</em> to learn pentesting techniques.</td> -->
|
||||
<!-- </tr> -->
|
||||
<tr>
|
||||
<td>Sports Activities</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Run weekly, active gym attendee, regularly go
|
||||
hiking.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>meetup.com</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Attending various tech meetups and social events.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Boardgames</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Meet up regularly to play the game
|
||||
<i>Root</i>.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Leetcode</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Do the leetcode daily challenge and hone in on
|
||||
different programming languages.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Construction and Landscaping</td>
|
||||
<td>Ongoing</td>
|
||||
<td class="row-leftalign">
|
||||
Involved in building a house in Bulgaria.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>University of Waterloo Film Club</td>
|
||||
<td>2023-2024</td>
|
||||
<td class="row-leftalign">
|
||||
Worked on student films <em>“Moon King”</em> and
|
||||
<em>“HAM”</em>, available online.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Socratica</td>
|
||||
<td>2023-2024</td>
|
||||
<td class="row-leftalign">
|
||||
Worked with individuals exploring innovative tech.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>University of Leeds Hockey Club</td>
|
||||
<td>2022-2023</td>
|
||||
<td class="row-leftalign">
|
||||
Played for the University of Leeds Hockey Club.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Royal Air Force Air Cadets</td>
|
||||
<td>2017-2020</td>
|
||||
<td class="row-leftalign">
|
||||
Achieved the role of Sergeant and “Best Cadet"
|
||||
award.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- <div class="interests"> -->
|
||||
<!-- <table> -->
|
||||
<!-- <tr><th>Personal qualities</th></tr> -->
|
||||
<!-- <tr><td>Intuitive</td></tr> -->
|
||||
<!-- <tr><td>Communicative</td></tr> -->
|
||||
<!-- <tr><td>Adaptable</td></tr> -->
|
||||
<!-- <tr><td>Versatile</td></tr> -->
|
||||
<!-- <tr><td>Diligent</td></tr> -->
|
||||
<!-- </table> -->
|
||||
<!-- <table> -->
|
||||
<!-- <tr><th>Interests</th></tr> -->
|
||||
<!-- <tr><td>Neuroscience</td></tr> -->
|
||||
<!-- <tr><td>Bouldering</td></tr> -->
|
||||
<!-- <tr><td>Science Fiction</td></tr> -->
|
||||
<!-- <tr><td>Mathematics</td></tr> -->
|
||||
<!-- <tr><td>Hiking</td></tr> -->
|
||||
<!-- </table> -->
|
||||
<!-- <table> -->
|
||||
<!-- <tr><th>Languages</th></tr> -->
|
||||
<!-- <tr><td>Rust</td></tr> -->
|
||||
<!-- <tr><td>HTML/JS</td></tr> -->
|
||||
<!-- <tr><td>C/C++</td></tr> -->
|
||||
<!-- <tr><td>React/Vue</td></tr> -->
|
||||
<!-- <tr><td>Python</td></tr> -->
|
||||
<!-- </table> -->
|
||||
<!-- </div> -->
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Fonts */
|
||||
/*@font-face {
|
||||
font-family: "AldoTheApache";
|
||||
src: url("/fonts/AldotheApache.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "RobotFont";
|
||||
src: url("/fonts/Robot_Font.otf") format("opentype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "m12";
|
||||
src: url("/fonts/m12.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}*/
|
||||
|
||||
@font-face {
|
||||
font-family: "big_noodle_titling";
|
||||
src: url("/fonts/big_noodle_titling.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "CreatoDisplay";
|
||||
src: url("/fonts/CreatoDisplay-Bold.otf") format("opentype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
* {
|
||||
/* Blue - Beige */
|
||||
/* --primary: #153448;
|
||||
--secondary: #3C5B6F;
|
||||
--tertiary: #948979;
|
||||
--quaternary: #f5bb78;
|
||||
--background: #DFD0B8; */
|
||||
|
||||
/* Blue - Turqouise */
|
||||
/* --primary: #161D6F;
|
||||
--secondary: #0B2F9F;
|
||||
--tertiary: #98DED9;
|
||||
--quaternary: #C7FFD8;
|
||||
--background: #C2EFD1; */
|
||||
|
||||
/* Red - Blue */
|
||||
/* --primary: #ff204e; */
|
||||
/* --secondary: #a0153e; */
|
||||
/* --tertiary: #5d0341; */
|
||||
/* --quaternary: #3a0e41; */
|
||||
/* --background: #00224d; */
|
||||
|
||||
/* Blue - Brown */
|
||||
/* --primary: #35374B; */
|
||||
/* --secondary: #344955; */
|
||||
/* --tertiary: #50727b; */
|
||||
/* --quaternary: #78a083; */
|
||||
/* --background: #c7b077; */
|
||||
|
||||
/* Black - White */
|
||||
--primary: black;
|
||||
--secondary: black;
|
||||
--tertiary: black;
|
||||
--quaternary: #cccccc;
|
||||
--background: white;
|
||||
|
||||
/* Blue - White */
|
||||
/* --primary: #201e43; */
|
||||
/* --secondary: #134b70; */
|
||||
/* --tertiary: #508c9b; */
|
||||
/* --quaternary: #cceeee; */
|
||||
/* --background: #eeeeee; */
|
||||
|
||||
--font-heading: big_noodle_titling;
|
||||
--font-text: CreatoDisplay;
|
||||
--font-size-text: 90%;
|
||||
--font-size-heading: 2.5em;
|
||||
--font-size-subheading: 1.5em;
|
||||
--font-size-tableheading: 1.2em;
|
||||
}
|
||||
|
||||
/* A5 Page */
|
||||
.a5page {
|
||||
/* overflow: scroll; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: var(--font-text);
|
||||
height: 148mm;
|
||||
width: 210mm;
|
||||
padding: 4mm;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--background);
|
||||
box-shadow: 0 20px 20px rgba(0, 0, 0, 0.2);
|
||||
border: solid 2px var(--primary);
|
||||
}
|
||||
|
||||
/* A4 Page */
|
||||
.a4page {
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-text);
|
||||
width: 210mm;
|
||||
/* Standard A4 width */
|
||||
height: 297mm;
|
||||
/* Standard A4 height */
|
||||
padding: 8mm;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--background);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--primary);
|
||||
overflow: auto;
|
||||
/* Enables scrolling when content exceeds height */
|
||||
margin: auto auto;
|
||||
/* Centers the page horizontally */
|
||||
}
|
||||
|
||||
/* Component Styling */
|
||||
main {
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-family: var(--font-heading);
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-heading);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0px;
|
||||
margin-bottom: 3px;
|
||||
border-bottom: 1px solid var(--primary);
|
||||
font-size: var(--font-size-subheading);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--secondary);
|
||||
font-size: var(--font-size-text);
|
||||
margin-top: 0.3em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
table {
|
||||
color: var(--secondary);
|
||||
border-collapse: collapse;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td {
|
||||
/* border: 2px solid var(--tertiary); */
|
||||
color: var(--secondary);
|
||||
border-top: 1px solid var(--tertiary);
|
||||
padding: 1px 10px 1px 10px;
|
||||
font-size: var(--font-size-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--secondary);
|
||||
border: 2px solid var(--tertiary);
|
||||
padding: 1px 0px 1px 7px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-size-tableheading);
|
||||
background-color: var(--quaternary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a:hover,
|
||||
a:visited {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Classes */
|
||||
/* Cover Navigation (for ease of use) */
|
||||
.cover-nav {
|
||||
position: fixed;
|
||||
top: 0.5vh;
|
||||
/* Position the element at the top of the screen */
|
||||
left: 80vw;
|
||||
/* Position the element at the left of the screen */
|
||||
border: 2px solid var(--tertiary);
|
||||
width: 19.5vw;
|
||||
/* Make the element span the width of the screen (optional) */
|
||||
background-color: var(--background);
|
||||
/* Set a background color to avoid overlap issues */
|
||||
z-index: 1000;
|
||||
/* Ensures the element is above other content */
|
||||
}
|
||||
|
||||
.cover-nav td,
|
||||
tr {
|
||||
font-family: var(--font-text);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.cover-nav th {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid var(--tertiary);
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cover letter styling */
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 0px solid var(--primary);
|
||||
resize: none;
|
||||
font-family: var(--font-text);
|
||||
}
|
||||
|
||||
/* Contact At Top of Page */
|
||||
.contact {
|
||||
all: unset;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--primary);
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
all: unset;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contact-details p {
|
||||
margin: 1px 0;
|
||||
}
|
||||
|
||||
/* Interests and Skills at bottom of page */
|
||||
.interests {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-top: solid 2px var(--primary);
|
||||
}
|
||||
|
||||
.interests td,
|
||||
tr,
|
||||
th {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.row-leftalign {
|
||||
/* background-image: url("https://www.fridakahlo.org/assets/img/paintings/without-hope.jpg"); */
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
336
nginx/vue/src/views/CV/CV.vue
Normal file
336
nginx/vue/src/views/CV/CV.vue
Normal file
@@ -0,0 +1,336 @@
|
||||
<script setup>
|
||||
import Project from "./Project.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="a4page">
|
||||
<div class="flex flex-row justify-between">
|
||||
<h1 class="name">Adam French</h1>
|
||||
<!-- <a href="covers.html"><img width=25 height=50 src="img/rune.png"></a> -->
|
||||
<div class="contact-details text-right">
|
||||
<p>+447563266931</p>
|
||||
<p>adam.a.french@outlook.com</p>
|
||||
<a href="https://www.adam-french.co.uk">
|
||||
www.adam-french.co.uk
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Profile</h2>
|
||||
<p>
|
||||
Recent Computer Science with Mathematics (International)
|
||||
graduate from the University of Leeds, awarded First Class
|
||||
Honours (81.1%). Strong foundation in full-stack software
|
||||
development, CI/CD workflows, and modern programming languages.
|
||||
Experienced in creating scalable, maintainable systems and
|
||||
motivated by solving complex technical problems. Enthusiastic
|
||||
about working within organisations that promote innovation,
|
||||
collaboration, and positive social impact.
|
||||
</p>
|
||||
|
||||
<h2>Projects</h2>
|
||||
|
||||
<Project class="border-b border-dotted">
|
||||
<template v-slot:left>
|
||||
<a
|
||||
href="https://www.adam-french.co.uk/gitea/adamf/web_server.git"
|
||||
>
|
||||
web_server.git
|
||||
</a>
|
||||
</template>
|
||||
<template v-slot:top>
|
||||
<small>
|
||||
Nginx, Vue, Postgres, Docker, Go, Python, Rust -> Wasm,
|
||||
Git Actions, JWT Auth
|
||||
</small>
|
||||
<small>2025</small>
|
||||
</template>
|
||||
<p>
|
||||
Developed and self-hosted a personal website with a fully
|
||||
automated maintenance CI/CD pipeline. Experimented with
|
||||
diverse tech stacks including Svelte, React/Redux, SQLite,
|
||||
Rust Actix, and Deno.
|
||||
</p>
|
||||
</Project>
|
||||
<Project class="border-b border-dotted">
|
||||
<template v-slot:left>
|
||||
<a
|
||||
href="https://www.adam-french.co.uk/gitea/adamf/tour.git"
|
||||
>
|
||||
tour.git
|
||||
</a>
|
||||
</template>
|
||||
<template v-slot:top>
|
||||
<small>Rust</small>
|
||||
<small>2026</small>
|
||||
</template>
|
||||
<p>
|
||||
Created a command-line tool for building and viewing
|
||||
interactive code tutorials. Designed functionality analogous
|
||||
to Git for intuitive version traversal and educational use.
|
||||
</p>
|
||||
</Project>
|
||||
<Project class="border-b border-dotted">
|
||||
<template v-slot:left>
|
||||
<a
|
||||
href="https://www.adam-french.co.uk/gitea/adamf/rust-raytracer.git"
|
||||
>
|
||||
rust-raytracer.git
|
||||
</a>
|
||||
</template>
|
||||
<template v-slot:top>
|
||||
<small>Rust, Linear Algebra, Multithreading</small>
|
||||
<small>2023</small>
|
||||
</template>
|
||||
<p>
|
||||
Built a parallelised, recursive ray tracer for realistic 3D
|
||||
rendering as part of a university module. Focused on
|
||||
algorithmic efficiency and low-level memory management in
|
||||
Rust.
|
||||
</p>
|
||||
</Project>
|
||||
<Project>
|
||||
<template #left>
|
||||
<p>
|
||||
<a
|
||||
class="text-center w-full"
|
||||
href="https://community.wolfram.com/groups/-/m/t/3210947"
|
||||
>
|
||||
Wolfram Summer School
|
||||
</a>
|
||||
</p>
|
||||
</template>
|
||||
<template #top>
|
||||
<small>Wolfram Mathematica</small>
|
||||
<small>2024</small>
|
||||
</template>
|
||||
<p>
|
||||
Designed and implemented a research project on Mobile
|
||||
Automata, including data visualisation and presentation of
|
||||
findings. Completed the project within a short deadline and
|
||||
collaborated with academic mentors to refine outcomes.
|
||||
</p>
|
||||
</Project>
|
||||
<h2>University & Modules</h2>
|
||||
<div class="w-full h-fit flex-row flex gap-5">
|
||||
<div class="flex-1 border-r border-dotted pr-3">
|
||||
<h3>University of Leeds</h3>
|
||||
<div
|
||||
class="flex-row flex place-content-between m-auto place-items-center"
|
||||
>
|
||||
<small> 81.1% (First Class Honours)</small>
|
||||
<small> 2021-2025 </small>
|
||||
</div>
|
||||
<small>BSc Computer Science with Mathematics </small>
|
||||
<ul>
|
||||
<li>Procedural & Object Oriented Programming,</li>
|
||||
<li></li>
|
||||
<li>Algorithms and Data Structures I & II</li>
|
||||
|
||||
<li>Databases</li>
|
||||
<li>Computer Processors</li>
|
||||
<li>Compiler Design and Construction</li>
|
||||
|
||||
<li>Formal Languages and Finite Automata</li>
|
||||
<li>Probability and Statistics I</li>
|
||||
<li>Machine Learning</li>
|
||||
<li>Graph Algorithms & Complexity Theory</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex-1 pl-3">
|
||||
<h3>The University of Waterloo</h3>
|
||||
<div
|
||||
class="flex-row flex place-content-between m-auto place-items-center"
|
||||
>
|
||||
<small>---</small>
|
||||
<small> 2023-2024 </small>
|
||||
</div>
|
||||
<div class="flex-row flex place-content-between"></div>
|
||||
<ul>
|
||||
<li>Applied Cryptography</li>
|
||||
<li>Introduction to Computer Graphics</li>
|
||||
<li>
|
||||
Introduction to Rings and Fields with Applications
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="a4page"> -->
|
||||
<!-- <h2>Experience</h2> -->
|
||||
<!-- <Project> -->
|
||||
<!-- <template #left> -->
|
||||
<!-- <p>Hospitality</p> -->
|
||||
<!-- </template> -->
|
||||
<!-- <template #top> -->
|
||||
<!-- <small>Cashier, Bartender, Waiter</small> -->
|
||||
<!-- <small>2018-2023</small> -->
|
||||
<!-- </template> -->
|
||||
<!-- <p> -->
|
||||
<!-- Worked at venues including: -->
|
||||
<!-- <em>Belgrave Music Hall</em>, -->
|
||||
<!-- <em>The Crown and Anchor Eastbourne</em>, -->
|
||||
<!-- <em>To The Rise Bakery</em>, -->
|
||||
<!-- <em>BFI Riverfront Kitchen</em>. -->
|
||||
<!-- </p> -->
|
||||
<!-- </Project> -->
|
||||
<!-- <h2>Commitments</h2> -->
|
||||
<!-- <ul> -->
|
||||
<!-- <li>Gym</li> -->
|
||||
<!-- <li>Climbing</li> -->
|
||||
<!-- <li>Meetup.com</li> -->
|
||||
<!-- <li>Boardgames</li> -->
|
||||
<!-- <li>Leetcode</li> -->
|
||||
<!-- <li>Learning Mandarin</li> -->
|
||||
<!-- </ul> -->
|
||||
<!-- </div> -->
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Fonts */
|
||||
@font-face {
|
||||
font-family: "big_noodle_titling";
|
||||
src: url("/fonts/big_noodle_titling.ttf") format("truetype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "CreatoDisplay";
|
||||
src: url("/fonts/CreatoDisplay-Bold.otf") format("opentype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
* {
|
||||
/* Black - White */
|
||||
--primary: black;
|
||||
--secondary: #0000ff;
|
||||
--tertiary: #ff0000;
|
||||
--quaternary: #cccccc;
|
||||
--background: white;
|
||||
|
||||
--font-heading: big_noodle_titling;
|
||||
--font-text: CreatoDisplay;
|
||||
--font-size-name: 2.5em;
|
||||
--font-size-text: 100%;
|
||||
--font-size-small: 0.9em;
|
||||
--font-size-heading: 2.1em;
|
||||
--font-size-subheading: 1.7em;
|
||||
--font-size-subsubheading: 1.4em;
|
||||
}
|
||||
|
||||
/* A4 Page */
|
||||
.a4page {
|
||||
line-height: 1.6;
|
||||
font-family: var(--font-text);
|
||||
width: 210mm;
|
||||
/* Standard A4 width */
|
||||
height: 297mm;
|
||||
/* Standard A4 height */
|
||||
padding: 5mm;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--background);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--primary);
|
||||
overflow: hidden;
|
||||
/* Enables scrolling when content exceeds height */
|
||||
margin: auto auto;
|
||||
/* Centers the page horizontally */
|
||||
}
|
||||
|
||||
/* Component Styling */
|
||||
main {
|
||||
padding: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
span {
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-family: var(--font-heading);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-heading);
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-bottom: 1px solid var(--primary);
|
||||
font-size: var(--font-size-subheading);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-subsubheading);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--tertiary);
|
||||
}
|
||||
a {
|
||||
background-color: transparent;
|
||||
color: var(--secondary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0.2em;
|
||||
color: var(--primary);
|
||||
font-size: var(--font-size-text);
|
||||
}
|
||||
|
||||
table {
|
||||
color: var(--secondary);
|
||||
border-collapse: collapse;
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
td {
|
||||
/* border: 2px solid var(--tertiary); */
|
||||
color: var(--secondary);
|
||||
border-top: 1px solid var(--tertiary);
|
||||
padding: 1px 10px 1px 10px;
|
||||
font-size: var(--font-size-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
color: var(--secondary);
|
||||
border: 2px solid var(--tertiary);
|
||||
padding: 1px 0px 1px 7px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-size-subsubheading);
|
||||
background-color: var(--quaternary);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
small {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--primary);
|
||||
}
|
||||
ul {
|
||||
font-size: var(--font-size-small);
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
17
nginx/vue/src/views/CV/Project.vue
Normal file
17
nginx/vue/src/views/CV/Project.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<div class="flex-row flex">
|
||||
<div class="w-2/7 p-5 m-auto">
|
||||
<slot name="left" />
|
||||
</div>
|
||||
<div class="w-full p-2">
|
||||
<div
|
||||
class="flex-row flex place-content-between m-auto place-items-center"
|
||||
>
|
||||
<slot name="top" />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -2,11 +2,12 @@
|
||||
import { ref } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
import Login from "@/components/admin/Login.vue";
|
||||
import CreateUser from "@/components/admin/CreateUser.vue";
|
||||
import CreatePost from "@/components/admin/CreatePost.vue";
|
||||
import CreateFavorite from "@/components/admin/CreateFavorite.vue";
|
||||
import CreateActivity from "@/components/admin/CreateActivity.vue";
|
||||
import Login from "./Login.vue";
|
||||
import CreateUser from "./CreateUser.vue";
|
||||
import CreatePost from "./CreatePost.vue";
|
||||
import CreateFavorite from "./CreateFavorite.vue";
|
||||
import CreateActivity from "./CreateActivity.vue";
|
||||
import CreateRowing from "./CreateRowing.vue";
|
||||
|
||||
const auth = useAuthStore();
|
||||
</script>
|
||||
@@ -21,6 +22,7 @@ const auth = useAuthStore();
|
||||
<CreatePost class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<CreateFavorite class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<CreateActivity class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
<CreateRowing class="bdr-2 bg-bg_primary" v-if="auth.loggedIn" />
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
46
nginx/vue/src/views/admin/CreateRowing.vue
Normal file
46
nginx/vue/src/views/admin/CreateRowing.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup>
|
||||
import Button from "@/components/input/Button.vue";
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
|
||||
const image = ref(null);
|
||||
const status = ref("");
|
||||
const error = ref("");
|
||||
|
||||
function onFileChange(e) {
|
||||
image.value = e.target.files[0] || null;
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!image.value) {
|
||||
error.value = "Please select an image";
|
||||
return;
|
||||
}
|
||||
error.value = "";
|
||||
status.value = "Uploading...";
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", image.value);
|
||||
|
||||
try {
|
||||
const res = await axios.post("/api/rowing", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
status.value = `Saved: ${res.data.Distance}m in ${Math.floor(res.data.Time / 1e9 / 60)}:${String(Math.floor((res.data.Time / 1e9) % 60)).padStart(2, "0")}`;
|
||||
image.value = null;
|
||||
} catch (err) {
|
||||
status.value = "";
|
||||
error.value = err.response?.data?.error || "Upload failed";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1>Create Rowing</h1>
|
||||
<input type="file" accept="image/jpeg,image/png,image/gif,image/webp" @change="onFileChange" />
|
||||
<Button @click="submit">Upload</Button>
|
||||
<p v-if="status">{{ status }}</p>
|
||||
<p v-if="error" class="text-red-500">{{ error }}</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -5,10 +5,10 @@ import Header from "@/components/text/Header.vue";
|
||||
|
||||
const images = [
|
||||
{ url: "/img/memes/pidgeon.gif", comment: "鸟" },
|
||||
//{ url: "/img/memes/no_slip.png" },
|
||||
// { url: "/img/memes/no_slip.png" },
|
||||
//{ url: "/img/memes/epic.jpeg" },
|
||||
{ url: "/img/bedroom/img2.png", comment: "办公桌" },
|
||||
{ url: "/img/bedroom/img1.png", comment: "床" },
|
||||
// { url: "/img/bedroom/img2.png", comment: "办公桌" },
|
||||
// { url: "/img/bedroom/img1.png", comment: "床" },
|
||||
];
|
||||
|
||||
const currentIndex = ref(0);
|
||||
@@ -18,6 +18,12 @@ const currentUrl = computed(() => images[currentIndex.value].url);
|
||||
let nextId;
|
||||
|
||||
function nextImage() {
|
||||
clearTimeout(nextId);
|
||||
currentIndex.value = (currentIndex.value + 1) % images.length;
|
||||
nextId = setTimeout(nextImage, 10000);
|
||||
}
|
||||
|
||||
function nextRandomImage() {
|
||||
clearTimeout(nextId);
|
||||
let newIndex;
|
||||
do {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import Timer from "@/components/util/Timer.vue";
|
||||
import Time from "@/components/util/Time.vue";
|
||||
import Radio from "@/components/util/Radio.vue";
|
||||
import Elle from "@/components/elle/Elle.vue";
|
||||
import Chat from "@/components/util/Chat.vue";
|
||||
import MusicPlayer from "@/components/util/MusicPlayer.vue";
|
||||
import CommitHistory from "@/components/util/CommitHistory.vue";
|
||||
|
||||
import Intro from "./Intro.vue";
|
||||
import Intro2 from "./Intro2.vue";
|
||||
@@ -23,8 +25,8 @@ import Consumption from "./Consumption.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" />
|
||||
<Intro2 class="intro" />
|
||||
<!-- <BadApple class="intro" /> -->
|
||||
<Listening class="listening" />
|
||||
<Stamps class="stamps" />
|
||||
<Feed class="feed" />
|
||||
@@ -34,16 +36,30 @@ import Consumption from "./Consumption.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 ">
|
||||
<div class="flex flex-col flex-1">
|
||||
<Time class="bg-bg_primary border-primary border" />
|
||||
<div
|
||||
class="sidebar border-quaternary place-content-between flex-1 flex flex-col m-10 w-60"
|
||||
>
|
||||
<div class="flex flex-col flex-1 gap-2">
|
||||
<Time
|
||||
class="bg-bg_primary border-primary border text-center"
|
||||
/>
|
||||
<Timer class="border-primary border bg-bg_primary" />
|
||||
<Radio
|
||||
class="border-primary border bg-bg_primary text-center"
|
||||
/>
|
||||
<CommitHistory
|
||||
class="border-primary border bg-bg_primary text-center"
|
||||
/>
|
||||
|
||||
<!-- <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>
|
||||
@@ -51,7 +67,7 @@ import Consumption from "./Consumption.vue";
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.homeGrid>* {
|
||||
.homeGrid > * {
|
||||
border: 2px solid var(--quaternary);
|
||||
border-color: var(--quaternary);
|
||||
background-color: var(--bg_primary);
|
||||
@@ -73,7 +89,6 @@ import Consumption from "./Consumption.vue";
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
|
||||
.tr,
|
||||
.br,
|
||||
.sidebar {
|
||||
|
||||
@@ -1,79 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { rand } from '@vueuse/core'
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { rand } from "@vueuse/core";
|
||||
import { ref, onMounted, onUnmounted, nextTick } from "vue";
|
||||
|
||||
interface Item {
|
||||
x: number
|
||||
y: number
|
||||
dx: number
|
||||
dy: number
|
||||
content: string
|
||||
x: number;
|
||||
y: number;
|
||||
dx: number;
|
||||
dy: number;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null)
|
||||
const itemEls = ref<HTMLDivElement[]>([])
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
const itemEls = ref<HTMLDivElement[]>([]);
|
||||
|
||||
const phrases = [
|
||||
'Welcome to my website',
|
||||
'Thank you for visiting',
|
||||
"Welcome to my website",
|
||||
"I'm looking to do a big revamp",
|
||||
"A4 sheets of paper are what I'm used to",
|
||||
"I'd love to know your recommendations",
|
||||
"Message me on discord or steam",
|
||||
"for very short books",
|
||||
"Message me by any means necessary",
|
||||
"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,
|
||||
dx: rand(0, 30) / 100,
|
||||
dy: 0.5,
|
||||
content: text,
|
||||
}))
|
||||
)
|
||||
})),
|
||||
);
|
||||
|
||||
let rafId = 0
|
||||
let rafId = 0;
|
||||
|
||||
function animate() {
|
||||
const c = container.value
|
||||
if (!c) return
|
||||
const c = container.value;
|
||||
if (!c) return;
|
||||
|
||||
const cw = c.clientWidth
|
||||
const ch = c.clientHeight
|
||||
const cw = c.clientWidth;
|
||||
const ch = c.clientHeight;
|
||||
|
||||
items.value.forEach((item, i) => {
|
||||
const el = itemEls.value[i]
|
||||
if (!el) return
|
||||
const el = itemEls.value[i];
|
||||
if (!el) return;
|
||||
|
||||
const ew = el.offsetWidth
|
||||
const eh = el.offsetHeight
|
||||
const ew = el.offsetWidth;
|
||||
const eh = el.offsetHeight;
|
||||
|
||||
item.x += item.dx
|
||||
item.y += item.dy
|
||||
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
|
||||
})
|
||||
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)
|
||||
rafId = requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
rafId = requestAnimationFrame(animate)
|
||||
})
|
||||
await nextTick();
|
||||
rafId = requestAnimationFrame(animate);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelAnimationFrame(rafId)
|
||||
})
|
||||
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)`
|
||||
}">
|
||||
<div
|
||||
ref="container"
|
||||
class="w-full h-full min-h-125 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>
|
||||
|
||||
@@ -11,6 +11,7 @@ const site_links = [
|
||||
];
|
||||
|
||||
const social_links = [
|
||||
{ name: "Gitea", link: "/gitea/explore/repos" },
|
||||
{ name: "Steam", link: "https://steamcommunity.com/id/SteveThePug" },
|
||||
{ name: "Github", link: "https://github.com/SteveThePug" },
|
||||
{ name: "Spotify", link: "https://open.spotify.com/user/stevethepug" },
|
||||
|
||||
@@ -42,7 +42,8 @@ img {
|
||||
width: 89px;
|
||||
height: 59px;
|
||||
}
|
||||
|
||||
.tst {
|
||||
width: calc(89px * 4);
|
||||
min-width: calc(89px * 4);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,10 +7,10 @@ import vueDevTools from "vite-plugin-vue-devtools";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
974
package-lock.json
generated
974
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^14.2.0"
|
||||
"@vueuse/core": "^14.2.0",
|
||||
"vite": "^7.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
43
readme.md
43
readme.md
@@ -1,22 +1,49 @@
|
||||
# TODO
|
||||
- ML for 4chan
|
||||
# Introduction
|
||||
|
||||

|
||||
|
||||
Welcome to the source code for my website! Please contact me if you would like to collaborate and thank you for visiting.
|
||||
|
||||
This website is currently self hosted on my Rasberry PI. Any interference and the killswitch will activate and stop the UK national grid power system so please don't tamper with my domain :).
|
||||
|
||||
# Future ideas
|
||||
|
||||
- Rust to wasm
|
||||
- ML for chatboards
|
||||
- Cache requests
|
||||
- Login to add / remove posts (auth)
|
||||
- Design webpage
|
||||
- Design more webpages
|
||||
- Calendar to show radio times
|
||||
- Nice smooth function background
|
||||
- Shrines
|
||||
- Redis (not really)
|
||||
- Nice smooth function background and transitions
|
||||
- Design shrines
|
||||
- Redis (not really but practical experience)
|
||||
|
||||
# .env
|
||||
|
||||
These environment variables are found in the `.env` file. The use of environment variables can be found by reading the code so the security of the variable names are not significant.
|
||||
|
||||
```
|
||||
|
||||
POSTGRES_USER=
|
||||
POSTGRES_PASSWORD=
|
||||
POSTGRES_DB=
|
||||
POSTGRES_PORT=
|
||||
POSTGRES_HOST=
|
||||
|
||||
GITEA_HOST=
|
||||
GITEA_PORT=
|
||||
POSTGRES_GITEA_DB=
|
||||
|
||||
GITEA_RUNNER_HOST=
|
||||
GITEA_RUNNER_NAME=
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN=
|
||||
|
||||
BACKEND_PORT=
|
||||
BACKEND_HOST=
|
||||
BACKEND_SECRET=
|
||||
BACKEND_ENDPOINT=
|
||||
|
||||
OBSIDIAN_DIR=
|
||||
|
||||
SPOTIFY_CLIENT_ID=
|
||||
SPOTIFY_CLIENT_SECRET=
|
||||
SPOTIFY_REDIRECT_URI=
|
||||
@@ -32,3 +59,5 @@ ICECAST_MOUNT=
|
||||
|
||||
DOMAIN=
|
||||
EMAIL=
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user