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:
@@ -12,13 +12,17 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-playground/locales v0.14.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/stretchr/testify v1.11.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -33,4 +37,5 @@ require (
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/sync v0.17.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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/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-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/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/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/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/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"`
|
||||
}
|
||||
|
||||
type AuthorInfo struct {
|
||||
ID uint `json:"id"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Email string `json:"email,omitempty"`
|
||||
}
|
||||
|
||||
// DTO для комментария
|
||||
type CreateCommentRequest struct {
|
||||
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
|
||||
type User struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
Email string `json:"email" gorm:"uniqueIndex;not null"`
|
||||
Password string `json:"-" gorm:"not null"`
|
||||
FirstName string `json:"first_name" gorm:"not null"`
|
||||
LastName string `json:"last_name" gorm:"not null"`
|
||||
Avatar string `json:"avatar"` // Путь к файлу аватара
|
||||
Phone string `json:"phone"`
|
||||
Experience string `json:"experience"`
|
||||
Goals string `json:"goals"`
|
||||
Newsletter bool `json:"newsletter"`
|
||||
Role string `json:"role" gorm:"default:user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
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 {
|
||||
ID uint `json:"id"`
|
||||
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"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `json:"id"`
|
||||
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"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// HashPassword хеширует пароль перед сохранением
|
||||
@@ -70,3 +81,34 @@ func (u *User) BeforeUpdate(tx *gorm.DB) error {
|
||||
u.UpdatedAt = time.Now()
|
||||
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
|
||||
|
||||
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 {
|
||||
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 class="burger-menu-container">
|
||||
<button
|
||||
class="burger-menu"
|
||||
:class="{ 'active': isMobileMenuOpen }"
|
||||
@click="toggleMobileMenu"
|
||||
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'"
|
||||
:aria-expanded="isMobileMenuOpen"
|
||||
>
|
||||
<button class="burger-menu" :class="{ 'active': isMobileMenuOpen }" @click="toggleMobileMenu"
|
||||
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'" :aria-expanded="isMobileMenuOpen">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
@@ -18,15 +13,13 @@
|
||||
<div class="dropdown-menu" :class="{ 'active': isMobileMenuOpen }" ref="dropdownMenu">
|
||||
<nav class="dropdown-nav">
|
||||
<div class="mobile-menu-header">
|
||||
<div class="mobile-logo">
|
||||
<img src="../assets/logo/Logo.png" alt="Little logo begushiy bashkir" class="little-logo">
|
||||
<span>Бегущий Башкир</span>
|
||||
</div>
|
||||
<!-- Добавьте мобильный логотип здесь -->
|
||||
<WriteLogo />
|
||||
<button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="dropdown-nav-content">
|
||||
<router-link to="/" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||
🏠 Главная
|
||||
@@ -73,8 +66,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import writeLogo from './writeLogo.vue';
|
||||
export default {
|
||||
name: 'NavigationMenu',
|
||||
components: {
|
||||
writeLogo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isMobileMenuOpen: false,
|
||||
@@ -94,9 +91,9 @@ export default {
|
||||
document.body.style.overflow = ''
|
||||
},
|
||||
handleClickOutside(event) {
|
||||
if (!event.target.closest('.burger-menu-container') &&
|
||||
!event.target.closest('.dropdown-menu') &&
|
||||
this.isMobileMenuOpen) {
|
||||
if (!event.target.closest('.burger-menu-container') &&
|
||||
!event.target.closest('.dropdown-menu') &&
|
||||
this.isMobileMenuOpen) {
|
||||
this.closeMobileMenu()
|
||||
}
|
||||
},
|
||||
@@ -121,7 +118,7 @@ export default {
|
||||
const touch = event.touches[0]
|
||||
const startX = touch.clientX
|
||||
const menu = this.$refs.dropdownMenu
|
||||
|
||||
|
||||
// Более точная проверка для свайпа
|
||||
if (menu && startX < 50) { // Свайп от левого края
|
||||
event.preventDefault()
|
||||
@@ -135,7 +132,7 @@ export default {
|
||||
document.addEventListener('keydown', this.handleEscapeKey)
|
||||
document.addEventListener('touchmove', this.handleTouchMove, { passive: false })
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
|
||||
|
||||
// Закрываем меню при навигации
|
||||
this.$router.afterEach(() => {
|
||||
this.closeMobileMenu()
|
||||
@@ -241,7 +238,7 @@ export default {
|
||||
padding: 1rem 1.5rem;
|
||||
background: #2e8b57;
|
||||
color: white;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.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