modified: README.md
modified: serv_nginx/api_bb/go.mod modified: serv_nginx/api_bb/go.sum new file: serv_nginx/api_bb/internal/models/achievement.go new file: serv_nginx/api_bb/internal/models/common.go new file: serv_nginx/api_bb/internal/models/event.go new file: serv_nginx/api_bb/internal/models/gallery.go modified: serv_nginx/api_bb/internal/models/news.go new file: serv_nginx/api_bb/internal/models/personal_best.go new file: serv_nginx/api_bb/internal/models/training_plan.go modified: serv_nginx/api_bb/internal/models/user.go new file: serv_nginx/api_bb/internal/models/user_stats.go modified: serv_nginx/api_bb/internal/models/workout.go modified: serv_nginx/bbvue/src/components/NavigationMenu.vue new file: serv_nginx/bbvue/src/components/writeLogo.vue add satructs for begushiybashkir.ru site
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
## for yalarba.ru && begushiybashkir.ru
|
## for yalarba.ru && begushiybashkir.ru
|
||||||
|
|
||||||
### tasks: profile page need full upgreade
|
### tasks: profile page need full upgreade, main sender (почтовая рассылка)
|
||||||
### BackEnd REST API on Golang 1.25.1
|
### BackEnd REST API on Golang 1.25.1
|
||||||
### FrontEnd vue3.js
|
### FrontEnd vue3.js
|
||||||
### Zagir Загир тренер
|
### Zagir Загир тренер
|
||||||
|
|||||||
@@ -12,13 +12,17 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
|
gorm.io/driver/mysql v1.5.6 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -33,4 +37,5 @@ require (
|
|||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/text v0.30.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
|
gorm.io/datatypes v1.2.7
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -15,8 +17,13 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
|
|||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
|
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||||
@@ -58,7 +65,12 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||||
|
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||||
|
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||||
|
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// models/achievement.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AchievementType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AchievementTypeDistance AchievementType = "distance"
|
||||||
|
AchievementTypeSpeed AchievementType = "speed"
|
||||||
|
AchievementTypeConsistency AchievementType = "consistency"
|
||||||
|
AchievementTypeEvent AchievementType = "event"
|
||||||
|
AchievementTypeSpecial AchievementType = "special"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Achievement struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
Type AchievementType `json:"type" gorm:"type:varchar(20);not null"`
|
||||||
|
Title string `json:"title" gorm:"size:255;not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
Result string `json:"result" gorm:"size:100"` // Достигнутый результат
|
||||||
|
Distance string `json:"distance" gorm:"size:50"` // Дистанция достижения
|
||||||
|
Date time.Time `json:"date" gorm:"not null"`
|
||||||
|
Verified bool `json:"verified" gorm:"default:false"`
|
||||||
|
BadgeImage string `json:"badge_image" gorm:"size:500"` // Изображение бейджа
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook
|
||||||
|
func (a *Achievement) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if a.CreatedAt.IsZero() {
|
||||||
|
a.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if a.UpdatedAt.IsZero() {
|
||||||
|
a.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (a *Achievement) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
a.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания достижения
|
||||||
|
type AchievementCreateRequest struct {
|
||||||
|
Type AchievementType `json:"type" validate:"required,oneof=distance speed consistency event special"`
|
||||||
|
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
Result string `json:"result" validate:"max=100"`
|
||||||
|
Distance string `json:"distance" validate:"max=50"`
|
||||||
|
Date time.Time `json:"date" validate:"required"`
|
||||||
|
BadgeImage string `json:"badge_image" validate:"max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для ответа с достижениями пользователя
|
||||||
|
type UserAchievementsResponse struct {
|
||||||
|
TotalAchievements int `json:"total_achievements"`
|
||||||
|
Completed int `json:"completed"`
|
||||||
|
ProgressPercent float64 `json:"progress_percent"`
|
||||||
|
Achievements []Achievement `json:"achievements"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// models/common.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Общая структура для информации об авторе
|
||||||
|
type AuthorInfo struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
Avatar string `json:"avatar,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"` // Добавляем email
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для пагинации
|
||||||
|
type PaginationRequest struct {
|
||||||
|
Page int `form:"page" validate:"min=1" default:"1"`
|
||||||
|
PerPage int `form:"per_page" validate:"min=1,max=100" default:"10"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationResponse struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для фильтров
|
||||||
|
type DateRangeFilter struct {
|
||||||
|
StartDate *time.Time `form:"start_date"`
|
||||||
|
EndDate *time.Time `form:"end_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkoutFilter struct {
|
||||||
|
DateRangeFilter
|
||||||
|
Type string `form:"type"`
|
||||||
|
UserID uint `form:"user_id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
// models/event.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EventType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EventTypeRace EventType = "race"
|
||||||
|
EventTypeTraining EventType = "training"
|
||||||
|
EventTypeSocial EventType = "social"
|
||||||
|
EventTypeWorkshop EventType = "workshop"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Title string `json:"title" gorm:"size:255;not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text;not null"`
|
||||||
|
Date time.Time `json:"date" gorm:"not null;index"`
|
||||||
|
Location string `json:"location" gorm:"size:255;not null"`
|
||||||
|
Type EventType `json:"type" gorm:"type:varchar(20);not null"`
|
||||||
|
Distance string `json:"distance" gorm:"size:50"` // Дистанция забега
|
||||||
|
ParticipantsCount int `json:"participants_count" gorm:"default:0"` // Количество участников
|
||||||
|
MaxParticipants int `json:"max_participants" gorm:"default:0"` // Максимальное количество участников
|
||||||
|
RegistrationOpen bool `json:"registration_open" gorm:"default:true"` // Открыта ли регистрация
|
||||||
|
Image string `json:"image" gorm:"size:500"` // Изображение события
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
Registrations []EventRegistration `json:"registrations,omitempty" gorm:"foreignKey:EventID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventRegistration struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
EventID uint `json:"event_id" gorm:"not null;index"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
Status string `json:"status" gorm:"size:20;default:pending"` // pending, confirmed, cancelled, completed
|
||||||
|
ResultTime *string `json:"result_time" gorm:"size:20"` // Результат забега
|
||||||
|
BibNumber *string `json:"bib_number" gorm:"size:10"` // Стартовый номер
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
Event Event `json:"event,omitempty" gorm:"foreignKey:EventID"`
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hooks
|
||||||
|
func (e *Event) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if e.CreatedAt.IsZero() {
|
||||||
|
e.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if e.UpdatedAt.IsZero() {
|
||||||
|
e.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *EventRegistration) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if er.CreatedAt.IsZero() {
|
||||||
|
er.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if er.UpdatedAt.IsZero() {
|
||||||
|
er.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hooks
|
||||||
|
func (e *Event) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
e.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (er *EventRegistration) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
er.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания события
|
||||||
|
type EventCreateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||||
|
Description string `json:"description" validate:"required,min=10"`
|
||||||
|
Date time.Time `json:"date" validate:"required"`
|
||||||
|
Location string `json:"location" validate:"required,max=255"`
|
||||||
|
Type EventType `json:"type" validate:"required,oneof=race training social workshop"`
|
||||||
|
Distance string `json:"distance" validate:"max=50"`
|
||||||
|
MaxParticipants int `json:"max_participants" validate:"min=0"`
|
||||||
|
RegistrationOpen bool `json:"registration_open"`
|
||||||
|
Image string `json:"image" validate:"max=500"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для регистрации на событие
|
||||||
|
type EventRegistrationRequest struct {
|
||||||
|
EventID uint `json:"event_id" validate:"required"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// models/gallery.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GalleryCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
GalleryCategoryTraining GalleryCategory = "training"
|
||||||
|
GalleryCategoryEvents GalleryCategory = "events"
|
||||||
|
GalleryCategoryCommunity GalleryCategory = "community"
|
||||||
|
GalleryCategoryAchievements GalleryCategory = "achievements"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Gallery struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
Title string `json:"title" gorm:"size:255;not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
ImagePath string `json:"image_path" gorm:"size:500;not null"` // Путь к изображению
|
||||||
|
Category GalleryCategory `json:"category" gorm:"type:varchar(20);not null"`
|
||||||
|
AuthorID uint `json:"author_id" gorm:"not null;index"`
|
||||||
|
EventDate *time.Time `json:"event_date"` // Дата события на фото
|
||||||
|
Views int `json:"views" gorm:"default:0"`
|
||||||
|
Likes int `json:"likes" gorm:"default:0"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
Author User `json:"author,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook
|
||||||
|
func (g *Gallery) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if g.CreatedAt.IsZero() {
|
||||||
|
g.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if g.UpdatedAt.IsZero() {
|
||||||
|
g.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (g *Gallery) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
g.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания записи в галерее
|
||||||
|
type GalleryCreateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
ImagePath string `json:"image_path" validate:"required,max=500"`
|
||||||
|
Category GalleryCategory `json:"category" validate:"required,oneof=training events community achievements"`
|
||||||
|
EventDate *time.Time `json:"event_date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для ответа с галереей
|
||||||
|
type GalleryResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
ImagePath string `json:"image_path"`
|
||||||
|
Category GalleryCategory `json:"category"`
|
||||||
|
EventDate *time.Time `json:"event_date"`
|
||||||
|
Views int `json:"views"`
|
||||||
|
Likes int `json:"likes"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Author AuthorInfo `json:"author"`
|
||||||
|
}
|
||||||
@@ -80,13 +80,6 @@ type NewsResponse struct {
|
|||||||
Comments int `json:"comments_count"`
|
Comments int `json:"comments_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthorInfo struct {
|
|
||||||
ID uint `json:"id"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DTO для комментария
|
// DTO для комментария
|
||||||
type CreateCommentRequest struct {
|
type CreateCommentRequest struct {
|
||||||
Content string `json:"content" validate:"required,min=1,max=1000"`
|
Content string `json:"content" validate:"required,min=1,max=1000"`
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// models/personal_best.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DistanceType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Distance5K DistanceType = "5k"
|
||||||
|
Distance10K DistanceType = "10k"
|
||||||
|
DistanceHalf DistanceType = "half_marathon"
|
||||||
|
DistanceFull DistanceType = "marathon"
|
||||||
|
DistanceOther DistanceType = "other"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PersonalBest struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
DistanceType DistanceType `json:"distance_type" gorm:"type:varchar(20);not null"`
|
||||||
|
Time string `json:"time" gorm:"size:20;not null"` // Время в формате "HH:MM:SS"
|
||||||
|
Pace string `json:"pace" gorm:"size:20"` // Темп
|
||||||
|
Date time.Time `json:"date" gorm:"not null"`
|
||||||
|
Verified bool `json:"verified" gorm:"default:false"` // Подтвержден ли результат
|
||||||
|
EventName string `json:"event_name" gorm:"size:255"` // Название забега
|
||||||
|
Location string `json:"location" gorm:"size:255"` // Место проведения
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook
|
||||||
|
func (pb *PersonalBest) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if pb.CreatedAt.IsZero() {
|
||||||
|
pb.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if pb.UpdatedAt.IsZero() {
|
||||||
|
pb.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (pb *PersonalBest) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
pb.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания личного рекорда
|
||||||
|
type PersonalBestCreateRequest struct {
|
||||||
|
DistanceType DistanceType `json:"distance_type" validate:"required,oneof=5k 10k half_marathon marathon other"`
|
||||||
|
Time string `json:"time" validate:"required,max=20"`
|
||||||
|
Pace string `json:"pace" validate:"max=20"`
|
||||||
|
Date time.Time `json:"date" validate:"required"`
|
||||||
|
EventName string `json:"event_name" validate:"max=255"`
|
||||||
|
Location string `json:"location" validate:"max=255"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для обновления личного рекорда
|
||||||
|
type PersonalBestUpdateRequest struct {
|
||||||
|
DistanceType DistanceType `json:"distance_type" validate:"omitempty,oneof=5k 10k half_marathon marathon other"`
|
||||||
|
Time string `json:"time" validate:"omitempty,max=20"`
|
||||||
|
Pace string `json:"pace" validate:"omitempty,max=20"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
EventName string `json:"event_name" validate:"omitempty,max=255"`
|
||||||
|
Location string `json:"location" validate:"omitempty,max=255"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// models/training_plan.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrainingPlan struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
Title string `json:"title" gorm:"size:255;not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
Weeks int `json:"weeks" gorm:"not null;default:12"` // Длительность плана в неделях
|
||||||
|
WorkoutsPerWeek int `json:"workouts_per_week" gorm:"not null;default:3"` // Тренировок в неделю
|
||||||
|
TargetDistance string `json:"target_distance" gorm:"size:50"` // Целевая дистанция
|
||||||
|
TargetDate time.Time `json:"target_date"` // Дата цели
|
||||||
|
CurrentWeek int `json:"current_week" gorm:"default:1"` // Текущая неделя
|
||||||
|
Completed bool `json:"completed" gorm:"default:false"` // Завершен ли план
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
Workouts []TrainingWorkout `json:"workouts,omitempty" gorm:"foreignKey:PlanID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrainingWorkout struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
PlanID uint `json:"plan_id" gorm:"not null;index"`
|
||||||
|
Week int `json:"week" gorm:"not null"` // Неделя плана
|
||||||
|
Day int `json:"day" gorm:"not null"` // День недели (1-7)
|
||||||
|
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
|
||||||
|
Description string `json:"description" gorm:"type:text"`
|
||||||
|
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2)"`
|
||||||
|
Duration int `json:"duration_min"`
|
||||||
|
Completed bool `json:"completed" gorm:"default:false"`
|
||||||
|
CompletedAt *time.Time `json:"completed_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hooks
|
||||||
|
func (tp *TrainingPlan) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if tp.CreatedAt.IsZero() {
|
||||||
|
tp.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if tp.UpdatedAt.IsZero() {
|
||||||
|
tp.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tw *TrainingWorkout) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if tw.CreatedAt.IsZero() {
|
||||||
|
tw.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (tp *TrainingPlan) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
tp.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания плана тренировок
|
||||||
|
type TrainingPlanCreateRequest struct {
|
||||||
|
Title string `json:"title" validate:"required,min=5,max=255"`
|
||||||
|
Description string `json:"description" validate:"max=1000"`
|
||||||
|
Weeks int `json:"weeks" validate:"required,min=1,max=52"`
|
||||||
|
WorkoutsPerWeek int `json:"workouts_per_week" validate:"required,min=1,max=7"`
|
||||||
|
TargetDistance string `json:"target_distance" validate:"max=50"`
|
||||||
|
TargetDate time.Time `json:"target_date"`
|
||||||
|
}
|
||||||
@@ -10,32 +10,43 @@ import (
|
|||||||
|
|
||||||
// models/user.go - добавить поле Avatar
|
// models/user.go - добавить поле Avatar
|
||||||
type User struct {
|
type User struct {
|
||||||
ID uint `json:"id" gorm:"primaryKey"`
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||||
Password string `json:"-" gorm:"not null"`
|
Password string `json:"-" gorm:"not null"`
|
||||||
FirstName string `json:"first_name" gorm:"not null"`
|
FirstName string `json:"first_name" gorm:"not null"`
|
||||||
LastName string `json:"last_name" gorm:"not null"`
|
LastName string `json:"last_name" gorm:"not null"`
|
||||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Experience string `json:"experience"`
|
Experience string `json:"experience"`
|
||||||
Goals string `json:"goals"`
|
Goals string `json:"goals"`
|
||||||
Newsletter bool `json:"newsletter"`
|
Newsletter bool `json:"newsletter"`
|
||||||
Role string `json:"role" gorm:"default:user"`
|
Role string `json:"role" gorm:"default:user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
Workouts []Workout `json:"workouts,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
PersonalBests []PersonalBest `json:"personal_bests,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
Achievements []Achievement `json:"achievements,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
TrainingPlans []TrainingPlan `json:"training_plans,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
News []News `json:"news,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
Comments []Comment `json:"comments,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
Reviews []Review `json:"reviews,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
Gallery []Gallery `json:"gallery,omitempty" gorm:"foreignKey:AuthorID"`
|
||||||
|
EventRegistrations []EventRegistration `json:"event_registrations,omitempty" gorm:"foreignKey:UserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserUpdate struct {
|
type UserUpdate struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
FirstName string `json:"first_name"`
|
FirstName string `json:"first_name"`
|
||||||
LastName string `json:"last_name"`
|
LastName string `json:"last_name"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
Experience string `json:"experience"`
|
Experience string `json:"experience"`
|
||||||
Goals string `json:"goals"`
|
Goals string `json:"goals"`
|
||||||
Newsletter bool `json:"newsletter"`
|
Newsletter bool `json:"newsletter"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HashPassword хеширует пароль перед сохранением
|
// HashPassword хеширует пароль перед сохранением
|
||||||
@@ -70,3 +81,34 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
|||||||
u.UpdatedAt = time.Now()
|
u.UpdatedAt = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DTO для обновления профиля
|
||||||
|
type UserUpdateRequest struct {
|
||||||
|
FirstName string `json:"first_name" validate:"required,min=2,max=100"`
|
||||||
|
LastName string `json:"last_name" validate:"required,min=2,max=100"`
|
||||||
|
Phone string `json:"phone" validate:"max=20"`
|
||||||
|
Experience string `json:"experience" validate:"max=50"`
|
||||||
|
Goals string `json:"goals" validate:"max=100"`
|
||||||
|
Newsletter bool `json:"newsletter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для ответа с пользователем (без sensitive данных)
|
||||||
|
type UserResponse struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
Experience string `json:"experience"`
|
||||||
|
Goals string `json:"goals"`
|
||||||
|
Newsletter bool `json:"newsletter"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для ответа с пользователем и статистикой
|
||||||
|
type UserWithStatsResponse struct {
|
||||||
|
UserResponse
|
||||||
|
Stats *UserStatsResponse `json:"stats,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// models/user_stats.go
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserStats struct {
|
||||||
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"uniqueIndex;not null"`
|
||||||
|
TotalDistance float64 `json:"total_distance" gorm:"type:decimal(10,2);default:0"` // Общий пробег в км
|
||||||
|
TotalTime int `json:"total_time" gorm:"default:0"` // Общее время в минутах
|
||||||
|
AvgPace string `json:"avg_pace" gorm:"size:20"` // Средний темп
|
||||||
|
WorkoutsCount int `json:"workouts_count" gorm:"default:0"` // Количество тренировок
|
||||||
|
CurrentStreak int `json:"current_streak" gorm:"default:0"` // Текущая серия дней подряд
|
||||||
|
LongestStreak int `json:"longest_streak" gorm:"default:0"` // Самая длинная серия
|
||||||
|
WeeklyDistance float64 `json:"weekly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за неделю
|
||||||
|
MonthlyDistance float64 `json:"monthly_distance" gorm:"type:decimal(8,2);default:0"` // Пробег за месяц
|
||||||
|
Best5K string `json:"best_5k" gorm:"size:20"` // Лучший результат на 5к
|
||||||
|
Best10K string `json:"best_10k" gorm:"size:20"` // Лучший результат на 10к
|
||||||
|
BestHalf string `json:"best_half" gorm:"size:20"` // Лучший результат на полумарафон
|
||||||
|
BestMarathon string `json:"best_marathon" gorm:"size:20"` // Лучший результат на марафон
|
||||||
|
LastWorkout time.Time `json:"last_workout"` // Последняя тренировка
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook
|
||||||
|
func (us *UserStats) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if us.CreatedAt.IsZero() {
|
||||||
|
us.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if us.UpdatedAt.IsZero() {
|
||||||
|
us.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (us *UserStats) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
us.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для статистики пользователя
|
||||||
|
type UserStatsResponse struct {
|
||||||
|
TotalDistance float64 `json:"total_distance"`
|
||||||
|
TotalTime int `json:"total_time"`
|
||||||
|
AvgPace string `json:"avg_pace"`
|
||||||
|
WorkoutsCount int `json:"workouts_count"`
|
||||||
|
CurrentStreak int `json:"current_streak"`
|
||||||
|
LongestStreak int `json:"longest_streak"`
|
||||||
|
WeeklyDistance float64 `json:"weekly_distance"`
|
||||||
|
MonthlyDistance float64 `json:"monthly_distance"`
|
||||||
|
PersonalBests PersonalBestsSummary `json:"personal_bests"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonalBestsSummary struct {
|
||||||
|
Best5K string `json:"best_5k"`
|
||||||
|
Best10K string `json:"best_10k"`
|
||||||
|
BestHalf string `json:"best_half"`
|
||||||
|
BestMarathon string `json:"best_marathon"`
|
||||||
|
}
|
||||||
@@ -1,5 +1,89 @@
|
|||||||
|
// models/workout.go
|
||||||
package models
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WorkoutType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkoutTypeEasy WorkoutType = "easy"
|
||||||
|
WorkoutTypeTempo WorkoutType = "tempo"
|
||||||
|
WorkoutTypeInterval WorkoutType = "interval"
|
||||||
|
WorkoutTypeLong WorkoutType = "long"
|
||||||
|
WorkoutTypeRecovery WorkoutType = "recovery"
|
||||||
|
)
|
||||||
|
|
||||||
type Workout struct {
|
type Workout struct {
|
||||||
ID uint
|
ID uint `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"not null;index"`
|
||||||
|
Type WorkoutType `json:"type" gorm:"type:varchar(20);not null"`
|
||||||
|
Distance float64 `json:"distance_km" gorm:"type:decimal(5,2);not null"` // Дистанция в км
|
||||||
|
Duration int `json:"duration_min" gorm:"not null"` // Продолжительность в минутах
|
||||||
|
Pace string `json:"pace" gorm:"size:20"` // Темп (например, "5:30")
|
||||||
|
Calories int `json:"calories" gorm:"default:0"` // Сожженные калории
|
||||||
|
Notes string `json:"notes" gorm:"type:text"` // Заметки к тренировке
|
||||||
|
Date time.Time `json:"date" gorm:"not null;index"` // Дата тренировки
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
// Связи
|
||||||
|
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeCreate hook
|
||||||
|
func (w *Workout) BeforeCreate(tx *gorm.DB) error {
|
||||||
|
if w.CreatedAt.IsZero() {
|
||||||
|
w.CreatedAt = time.Now()
|
||||||
|
}
|
||||||
|
if w.UpdatedAt.IsZero() {
|
||||||
|
w.UpdatedAt = time.Now()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BeforeUpdate hook
|
||||||
|
func (w *Workout) BeforeUpdate(tx *gorm.DB) error {
|
||||||
|
w.UpdatedAt = time.Now()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для создания тренировки
|
||||||
|
type WorkoutCreateRequest struct {
|
||||||
|
Type WorkoutType `json:"type" validate:"required,oneof=easy tempo interval long recovery"`
|
||||||
|
Distance float64 `json:"distance_km" validate:"required,min=0.1,max=1000"`
|
||||||
|
Duration int `json:"duration_min" validate:"required,min=1,max=1440"`
|
||||||
|
Pace string `json:"pace" validate:"max=20"`
|
||||||
|
Calories int `json:"calories" validate:"min=0,max=5000"`
|
||||||
|
Notes string `json:"notes" validate:"max=1000"`
|
||||||
|
Date time.Time `json:"date" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для обновления тренировки
|
||||||
|
type WorkoutUpdateRequest struct {
|
||||||
|
Type WorkoutType `json:"type" validate:"omitempty,oneof=easy tempo interval long recovery"`
|
||||||
|
Distance float64 `json:"distance_km" validate:"omitempty,min=0.1,max=1000"`
|
||||||
|
Duration int `json:"duration_min" validate:"omitempty,min=1,max=1440"`
|
||||||
|
Pace string `json:"pace" validate:"omitempty,max=20"`
|
||||||
|
Calories int `json:"calories" validate:"omitempty,min=0,max=5000"`
|
||||||
|
Notes string `json:"notes" validate:"omitempty,max=1000"`
|
||||||
|
Date time.Time `json:"date"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DTO для статистики тренировок
|
||||||
|
type WorkoutStatsResponse struct {
|
||||||
|
TotalWorkouts int `json:"total_workouts"`
|
||||||
|
TotalDistance float64 `json:"total_distance_km"`
|
||||||
|
TotalTime int `json:"total_time_min"`
|
||||||
|
AveragePace string `json:"average_pace"`
|
||||||
|
MonthlyStats []MonthlyStat `json:"monthly_stats"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonthlyStat struct {
|
||||||
|
Month string `json:"month"`
|
||||||
|
Distance float64 `json:"distance_km"`
|
||||||
|
Workouts int `json:"workouts"`
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,8 @@
|
|||||||
<div>
|
<div>
|
||||||
<!-- Бургер-меню для всех устройств -->
|
<!-- Бургер-меню для всех устройств -->
|
||||||
<div class="burger-menu-container">
|
<div class="burger-menu-container">
|
||||||
<button
|
<button class="burger-menu" :class="{ 'active': isMobileMenuOpen }" @click="toggleMobileMenu"
|
||||||
class="burger-menu"
|
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'" :aria-expanded="isMobileMenuOpen">
|
||||||
:class="{ 'active': isMobileMenuOpen }"
|
|
||||||
@click="toggleMobileMenu"
|
|
||||||
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'"
|
|
||||||
:aria-expanded="isMobileMenuOpen"
|
|
||||||
>
|
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
<span></span>
|
<span></span>
|
||||||
@@ -18,10 +13,8 @@
|
|||||||
<div class="dropdown-menu" :class="{ 'active': isMobileMenuOpen }" ref="dropdownMenu">
|
<div class="dropdown-menu" :class="{ 'active': isMobileMenuOpen }" ref="dropdownMenu">
|
||||||
<nav class="dropdown-nav">
|
<nav class="dropdown-nav">
|
||||||
<div class="mobile-menu-header">
|
<div class="mobile-menu-header">
|
||||||
<div class="mobile-logo">
|
<!-- Добавьте мобильный логотип здесь -->
|
||||||
<img src="../assets/logo/Logo.png" alt="Little logo begushiy bashkir" class="little-logo">
|
<WriteLogo />
|
||||||
<span>Бегущий Башкир</span>
|
|
||||||
</div>
|
|
||||||
<button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню">
|
<button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -73,8 +66,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import writeLogo from './writeLogo.vue';
|
||||||
export default {
|
export default {
|
||||||
name: 'NavigationMenu',
|
name: 'NavigationMenu',
|
||||||
|
components: {
|
||||||
|
writeLogo
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isMobileMenuOpen: false,
|
isMobileMenuOpen: false,
|
||||||
@@ -95,8 +92,8 @@ export default {
|
|||||||
},
|
},
|
||||||
handleClickOutside(event) {
|
handleClickOutside(event) {
|
||||||
if (!event.target.closest('.burger-menu-container') &&
|
if (!event.target.closest('.burger-menu-container') &&
|
||||||
!event.target.closest('.dropdown-menu') &&
|
!event.target.closest('.dropdown-menu') &&
|
||||||
this.isMobileMenuOpen) {
|
this.isMobileMenuOpen) {
|
||||||
this.closeMobileMenu()
|
this.closeMobileMenu()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -241,7 +238,7 @@ export default {
|
|||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
background: #2e8b57;
|
background: #2e8b57;
|
||||||
color: white;
|
color: white;
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-logo {
|
.mobile-logo {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mobile-logo">
|
||||||
|
<img :src="logoUrl" alt="Little logo begushiy bashkir" class="little-logo">
|
||||||
|
<span>Бегущий Башкир</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'WriteLogo',
|
||||||
|
computed: {
|
||||||
|
logoUrl() {
|
||||||
|
// Для правильного пути к изображению в разных окружениях
|
||||||
|
return new URL('../assets/logo/Logo.png', import.meta.url).href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mobile-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: 50px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.little-logo {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo span {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2e8b57;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скрыть на десктопе по умолчанию */
|
||||||
|
@media (min-width: 769px) {
|
||||||
|
.mobile-logo {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Показать на мобильных устройствах */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-logo {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user