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