new file: begushiybashkir/bbvue/public/images/locations/1mayPark.webp

new file:   begushiybashkir/bbvue/public/images/locations/dinamo.jpg
	new file:   begushiybashkir/bbvue/public/images/locations/riverSide.jpeg
	modified:   begushiybashkir/bbvue/src/views/Training.vue
	modified:   serv_nginx/api_bb/internal/handlers/handler_util.go
	modified:   serv_nginx/api_bb/internal/handlers/user.go
	modified:   serv_nginx/api_bb/internal/routes/routes.go
	modified:   serv_nginx/api_bb/internal/service/auth_service.go
	modified:   serv_nginx/api_bb/internal/service/user_service.go
add photo location into trainings page
This commit is contained in:
2025-10-12 16:55:55 +05:00
parent ebe43e6617
commit 12f805f9e1
9 changed files with 124 additions and 96 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

+107 -78
View File
@@ -28,19 +28,14 @@
<div class="container"> <div class="container">
<h2 class="section-title">📅 Расписание тренировок</h2> <h2 class="section-title">📅 Расписание тренировок</h2>
<p class="section-subtitle">Регулярные занятия в лучших локациях Уфы</p> <p class="section-subtitle">Регулярные занятия в лучших локациях Уфы</p>
<div class="schedule-container"> <div class="schedule-container">
<div class="schedule-grid"> <div class="schedule-grid">
<div <div v-for="day in schedule" :key="day.name" class="schedule-day" :class="{
v-for="day in schedule" 'training-day': day.time !== '--:--',
:key="day.name" 'rest-day': day.time === '--:--',
class="schedule-day" 'today': isToday(day.name)
:class="{ }">
'training-day': day.time !== '--:--',
'rest-day': day.time === '--:--',
'today': isToday(day.name)
}"
>
<div class="day-header"> <div class="day-header">
<h4>{{ day.name }}</h4> <h4>{{ day.name }}</h4>
<span class="day-badge" v-if="isToday(day.name)">Сегодня</span> <span class="day-badge" v-if="isToday(day.name)">Сегодня</span>
@@ -55,19 +50,11 @@
📍 {{ day.location }} 📍 {{ day.location }}
</div> </div>
<div class="day-features" v-if="day.features"> <div class="day-features" v-if="day.features">
<span <span v-for="feature in day.features" :key="feature" class="feature-tag">
v-for="feature in day.features"
:key="feature"
class="feature-tag"
>
{{ feature }} {{ feature }}
</span> </span>
</div> </div>
<button <button v-if="day.time !== '--:--'" class="btn-day" @click="openTrainingModal(day)">
v-if="day.time !== '--:--'"
class="btn-day"
@click="openTrainingModal(day)"
>
🏃 Записаться 🏃 Записаться
</button> </button>
</div> </div>
@@ -150,7 +137,9 @@
<div class="locations-grid"> <div class="locations-grid">
<div class="location-card"> <div class="location-card">
<div class="location-image"> <div class="location-image">
<div class="image-placeholder">🏞</div> <div class="image-placeholder">
<img :src="getImageUrl('locations/riverSide.jpeg')" alt="Набережная Уфа Монумент">
</div>
</div> </div>
<div class="location-info"> <div class="location-info">
<h3>Набережная</h3> <h3>Набережная</h3>
@@ -165,7 +154,9 @@
</div> </div>
<div class="location-card"> <div class="location-card">
<div class="location-image"> <div class="location-image">
<div class="image-placeholder">🏟</div> <div class="image-placeholder">
<img :src="getImageUrl('locations/dinamo.jpg')" alt="Стадион Динамо">
</div>
</div> </div>
<div class="location-info"> <div class="location-info">
<h3>Стадион "Динамо"</h3> <h3>Стадион "Динамо"</h3>
@@ -178,6 +169,23 @@
<p class="location-desc">Лучшее место для работы над скоростью и техникой</p> <p class="location-desc">Лучшее место для работы над скоростью и техникой</p>
</div> </div>
</div> </div>
<div class="location-card">
<div class="location-image">
<div class="image-placeholder">
<img :src="getImageUrl('locations/1mayPark.webp')" alt="Парк Первомайский ост. ДК УМПО">
</div>
</div>
<div class="location-info">
<h3>Парк первомайский</h3>
<p class="location-address">ДК УМПО Калининский р-н</p>
<div class="location-features">
<span class="feature">📏 Круг 2.5 км</span>
<span class="feature">💡 Освещение</span>
<span class="feature">🌳 Парк</span>
</div>
<p class="location-desc">Идеальное место для групповых тренировок и спецбеговых упраждений</p>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>
@@ -240,23 +248,11 @@
<form class="signup-form" @submit.prevent="handleSignup"> <form class="signup-form" @submit.prevent="handleSignup">
<div class="form-group"> <div class="form-group">
<label for="name">Ваше имя *</label> <label for="name">Ваше имя *</label>
<input <input id="name" v-model="signupForm.name" type="text" required placeholder="Введите ваше имя">
id="name"
v-model="signupForm.name"
type="text"
required
placeholder="Введите ваше имя"
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="phone">Телефон *</label> <label for="phone">Телефон *</label>
<input <input id="phone" v-model="signupForm.phone" type="tel" required placeholder="+7 (999) 123-45-67">
id="phone"
v-model="signupForm.phone"
type="tel"
required
placeholder="+7 (999) 123-45-67"
>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="level">Уровень подготовки</label> <label for="level">Уровень подготовки</label>
@@ -290,51 +286,53 @@ export default {
level: '' level: ''
}, },
schedule: [ schedule: [
{ {
name: 'Понедельник', name: 'Понедельник',
time: '19:30', time: '19:30',
activity: 'Техника бега + ОФП', activity: 'Техника бега + ОФП',
location: 'Парк Якутова', location: 'Парк Якутова',
features: ['Коррекция техники', 'Силовые упражнения'] features: ['Коррекция техники', 'Силовые упражнения']
}, },
{ {
name: 'Вторник', name: 'Вторник',
time: '19:00', time: '19:00',
activity: 'Тренировка', activity: 'Тренировка',
location: 'Парк первомайский', location: 'Парк первомайский',
features: ['Легкий бег', 'Спецупражнения'] features: ['Легкий бег', 'Спецупражнения']
}, },
{ {
name: 'Среда', name: 'Среда',
time: '19:30', time: '19:30',
activity: 'Техника бега + СБУ', activity: 'Техника бега + СБУ',
location: 'Стадион Динамо', location: 'Стадион Динамо',
features: ['Спецупражнения', 'Работа над скоростью'] features: ['Спецупражнения', 'Работа над скоростью']
}, },
{ {
name: 'Четверг', name: 'Четверг',
time: '19:00', time: '19:00',
activity: 'Тренировка', activity: 'Тренировка',
location: 'Парк первомайский', location: 'Парк первомайский',
features: ['Самостоятельно', 'Растяжка'] features: ['Самостоятельно', 'Растяжка']
}, },
{ {
name: 'Пятница', name: 'Пятница',
time: '--:--', time: '--:--',
activity: 'Восстановление', activity: 'Восстановление',
location: 'Свободное',
features: ['Самостоятельно', 'Йога/Плавание'] features: ['Самостоятельно', 'Йога/Плавание']
}, },
{ {
name: 'Суббота', name: 'Суббота',
time: '10:00', time: '10:00',
activity: 'Длительный кросс', activity: 'Длительный кросс',
location: 'Лесопарковая зона', location: 'Лесопарковая зона',
features: ['Аэробная выносливость', '15-20 км'] features: ['Аэробная выносливость', '15-20 км']
}, },
{ {
name: 'Воскресенье', name: 'Воскресенье',
time: '--:--', time: '--:--',
activity: 'Восстановление', activity: 'Восстановление',
location: 'Свободное',
features: ['Полный отдых', 'Восстановительные процедуры'] features: ['Полный отдых', 'Восстановительные процедуры']
} }
] ]
@@ -347,6 +345,13 @@ export default {
} }
}, },
methods: { methods: {
getImageUrl(path) {
// В продакшене замените на правильный путь
const baseUrl = import.meta.env.BASE_URL
// Путь от корня public/
console.log(`${baseUrl}images/${path}`)
return `${baseUrl}images/${path}`
},
isToday(dayName) { isToday(dayName) {
return dayName === this.today return dayName === this.today
}, },
@@ -369,7 +374,7 @@ export default {
training: this.selectedTraining, training: this.selectedTraining,
user: this.signupForm user: this.signupForm
}) })
// Имитация успешной записи // Имитация успешной записи
alert(`Спасибо, ${this.signupForm.name}! Вы записаны на тренировку. Тренер свяжется с вами для подтверждения.`) alert(`Спасибо, ${this.signupForm.name}! Вы записаны на тренировку. Тренер свяжется с вами для подтверждения.`)
this.closeModal() this.closeModal()
@@ -700,6 +705,9 @@ export default {
overflow: hidden; overflow: hidden;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08); box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
transition: transform 0.3s ease; transition: transform 0.3s ease;
display: flex;
flex-direction: column;
height: 100%;
} }
.location-card:hover { .location-card:hover {
@@ -708,24 +716,42 @@ export default {
.location-image { .location-image {
height: 200px; height: 200px;
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%); overflow: hidden;
display: flex; position: relative;
align-items: center;
justify-content: center;
} }
.image-placeholder { .image-placeholder {
font-size: 4rem; width: 100%;
color: white; height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.image-placeholder img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
transition: transform 0.3s ease;
}
.location-card:hover .image-placeholder img {
transform: scale(1.05);
} }
.location-info { .location-info {
padding: 2rem; padding: 2rem;
flex: 1;
display: flex;
flex-direction: column;
} }
.location-info h3 { .location-info h3 {
color: #2e8b57; color: #2e8b57;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
font-size: 1.3rem;
} }
.location-address { .location-address {
@@ -747,11 +773,13 @@ export default {
padding: 0.3rem 0.8rem; padding: 0.3rem 0.8rem;
border-radius: 15px; border-radius: 15px;
font-size: 0.8rem; font-size: 0.8rem;
white-space: nowrap;
} }
.location-desc { .location-desc {
color: #666; color: #666;
line-height: 1.5; line-height: 1.5;
margin-top: auto;
} }
/* CTA секция */ /* CTA секция */
@@ -986,47 +1014,47 @@ export default {
.hero-title { .hero-title {
font-size: 2.2rem; font-size: 2.2rem;
} }
.hero-features { .hero-features {
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
.hero-actions { .hero-actions {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.schedule-grid { .schedule-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stats-grid { .stats-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.workouts-grid { .workouts-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.locations-grid { .locations-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.cta-features { .cta-features {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.cta-actions { .cta-actions {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.btn { .btn {
width: 100%; width: 100%;
max-width: 300px; max-width: 300px;
} }
.modal-content { .modal-content {
padding: 2rem 1.5rem; padding: 2rem 1.5rem;
} }
@@ -1036,15 +1064,15 @@ export default {
.hero-section { .hero-section {
padding: 80px 0 60px; padding: 80px 0 60px;
} }
.hero-title { .hero-title {
font-size: 1.8rem; font-size: 1.8rem;
} }
.section-title { .section-title {
font-size: 2rem; font-size: 2rem;
} }
.container { .container {
padding: 0 15px; padding: 0 15px;
} }
@@ -1056,6 +1084,7 @@ export default {
opacity: 0; opacity: 0;
transform: translateY(30px); transform: translateY(30px);
} }
to { to {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translateY(0);
@@ -2,7 +2,6 @@ package handlers
import ( import (
"api_bb/internal/models" "api_bb/internal/models"
"net/http"
) )
// Общая функция для преобразования User в UserResponse // Общая функция для преобразования User в UserResponse
@@ -21,8 +20,3 @@ func toUserResponse(user *models.User) UserResponse {
UpdatedAt: user.UpdatedAt, UpdatedAt: user.UpdatedAt,
} }
} }
// Обработчик для OPTIONS запросов
func (h *UserHandler) handleOptions(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
+4 -4
View File
@@ -19,14 +19,14 @@ import (
) )
type UserHandler struct { type UserHandler struct {
authService service.AuthService
logger logger.Interface logger logger.Interface
userService service.UserService
} }
func NewUserHandler(authService service.AuthService) *UserHandler { func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{ return &UserHandler{
authService: authService,
logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user"))), logger: logger.NewWrapper(logger.Get().With(zap.String("handler", "user"))),
userService: userService,
} }
} }
@@ -155,7 +155,7 @@ func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
} }
// Сохраняем обновленные данные // Сохраняем обновленные данные
if err := h.authService.UpdateProfile(updatedUser); err != nil { if err := h.userService.UpdateProfile(updatedUser); err != nil {
h.logger.Error("failed to update profile in service", h.logger.Error("failed to update profile in service",
zap.Uint("user_id", currentUser.ID), zap.Uint("user_id", currentUser.ID),
zap.Error(err), zap.Error(err),
+2 -1
View File
@@ -32,11 +32,12 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
// Initialize services with logger // Initialize services with logger
jwtService := service.NewJWTService(config.JWTSecret) jwtService := service.NewJWTService(config.JWTSecret)
authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер authService := service.NewAuthService(userRepo, jwtService, baseLogger) // Передаем логгер
userService := service.NewUserService(userRepo, jwtService, baseLogger)
// Initialize handlers // Initialize handlers
healthHandler := handlers.NewHealthHandler() healthHandler := handlers.NewHealthHandler()
authHandler := handlers.NewAuthHandler(authService, jwtService) authHandler := handlers.NewAuthHandler(authService, jwtService)
userHandler := handlers.NewUserHandler(authService) userHandler := handlers.NewUserHandler(&userService)
// Health routes // Health routes
r.Mount("/api", healthHandler.Routes()) r.Mount("/api", healthHandler.Routes())
@@ -15,8 +15,6 @@ import (
type AuthService interface { type AuthService interface {
Register(user *models.User) error Register(user *models.User) error
Login(email, password string) (*models.User, string, error) Login(email, password string) (*models.User, string, error)
GetUserProfile(userID uint) (*models.User, error)
UpdateProfile(user *models.User) error
} }
type authService struct { type authService struct {
@@ -21,11 +21,17 @@ type userService struct {
logger logger.Interface logger logger.Interface
} }
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) AuthService {
// Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "auth"))
return &authService{ // UpdateProfile implements UserService.
func (s userService) UpdateProfile(user *models.User) error {
panic("unimplemented")
}
func NewUserService(userRepo repository.UserRepository, jwtService JWTService, log logger.Interface) userService {
// Создаем логгер с контекстом для сервиса
serviceLogger := log.With(zap.String("service", "user"))
return userService{
userRepo: userRepo, userRepo: userRepo,
jwtService: jwtService, jwtService: jwtService,
logger: serviceLogger, logger: serviceLogger,
@@ -64,7 +70,7 @@ func (s *authService) UpdateProfile(user *models.User) error {
return s.userRepo.UpdateExcludeEmail(updateData) return s.userRepo.UpdateExcludeEmail(updateData)
} }
func (s *authService) GetUserProfile(userID uint) (*models.User, error) { func (s *userService) GetUserProfile(userID uint) (*models.User, error) {
s.logger.Debug("Getting user profile", s.logger.Debug("Getting user profile",
zap.Uint("user_id", userID), zap.Uint("user_id", userID),
) )