moove bbvue
@@ -0,0 +1,8 @@
|
|||||||
|
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||||
|
charset = utf-8
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
|
max_line_length = 100
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_APP_DEBUG=true
|
||||||
|
VITE_API_BASE_URL=https://begushiybashkir.ru/api/v1
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"EditorConfig.EditorConfig",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"explorer.fileNesting.enabled": true,
|
||||||
|
"explorer.fileNesting.patterns": {
|
||||||
|
"tsconfig.json": "tsconfig.*.json, env.d.ts",
|
||||||
|
"vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
|
||||||
|
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig"
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# bbvue
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Recommended Browser Setup
|
||||||
|
|
||||||
|
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||||
|
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||||
|
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||||
|
- Firefox:
|
||||||
|
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||||
|
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
import globals from 'globals'
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
||||||
|
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
skipFormatting,
|
||||||
|
])
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="src/assets/logo/Logo.png" />
|
||||||
|
<title>Бегущий Башкир | Беговой клуб в Уфе</title>
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Беговой клуб 'Бегущий Башкир' в Уфе: тренировки на свежем воздухе, профессиональный тренер, участие в марафонах. Запишитесь на занятия!"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="keywords"
|
||||||
|
content="беговой клуб,
|
||||||
|
беговая школа Уфы,
|
||||||
|
беговая академия Башкортостана,
|
||||||
|
тренировки на свежем воздухе,
|
||||||
|
марафон Уфа,
|
||||||
|
полумарафон,
|
||||||
|
трейловый бег,
|
||||||
|
Аминев Загир,
|
||||||
|
Мастер спорта по полиатлону,
|
||||||
|
КМС по скайраннингу,
|
||||||
|
беговые достижения,
|
||||||
|
беговая команда,
|
||||||
|
спорт в Уфе,
|
||||||
|
здоровый образ жизни,
|
||||||
|
беговые тренировки,
|
||||||
|
беговые клубы Башкортостана,
|
||||||
|
бег в Уфе,
|
||||||
|
беговой клуб Уфа,
|
||||||
|
ультрамарафон"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "bbvue",
|
||||||
|
"version": "0.0.13",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.33.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"eslint": "^9.33.0",
|
||||||
|
"eslint-plugin-vue": "~10.4.0",
|
||||||
|
"globals": "^16.3.0",
|
||||||
|
"prettier": "3.6.2",
|
||||||
|
"vite": "^7.1.7",
|
||||||
|
"vite-plugin-vue-devtools": "^8.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.2 MiB |
|
After Width: | Height: | Size: 6.1 MiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 382 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 321 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 292 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 4.5 MiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<header class="app-header">
|
||||||
|
<div class="container header-container">
|
||||||
|
<!-- Логотип -->
|
||||||
|
<router-link to="/" class="logo-link">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-box">
|
||||||
|
<img src="./assets/logo/Logo.png" alt="Little logo begushiy bashkir" class="little-logo">
|
||||||
|
</div>
|
||||||
|
<div class="logo-box">
|
||||||
|
<span><i>Бегущий Башкир</i></span>
|
||||||
|
</div>
|
||||||
|
<div class="logo-box team">
|
||||||
|
<span>team</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- Используем компонент меню -->
|
||||||
|
<NavigationMenu />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="app-footer">
|
||||||
|
<div class="container">
|
||||||
|
<p>© 2025 Беговой клуб "Бегущий Башкир". Все права защищены.</p>
|
||||||
|
<p>Уфа, Республика Башкортостан</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NavigationMenu from './components/NavigationMenu.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
NavigationMenu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Остальные стили остаются без изменений */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.app-header {
|
||||||
|
background-color: #2e8b56;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem 0;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo Styles */
|
||||||
|
.logo-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 1002;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.little-logo {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
min-height: calc(100vh - 140px);
|
||||||
|
padding: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.app-footer {
|
||||||
|
background-color: #1a3e23;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1.5rem 0;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1023px) and (min-width: 768px) {
|
||||||
|
.team {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.logo {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.little-logo {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 360px) {
|
||||||
|
.logo-box:nth-child(2) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Styles */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #e6c200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page Styles */
|
||||||
|
.page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h1 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'LogoFont';
|
||||||
|
src: url('./fonts/Lobster-Regular.ttf');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
@import './base.css';
|
||||||
|
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<!-- AvatarUpload.vue -->
|
||||||
|
<template>
|
||||||
|
<div class="avatar-upload">
|
||||||
|
<div class="avatar-preview">
|
||||||
|
<img v-if="previewUrl" :src="previewUrl" alt="Аватар" class="avatar-image" @error="handleImageError" />
|
||||||
|
<div v-else class="avatar-placeholder">
|
||||||
|
👤
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showActions" class="avatar-actions">
|
||||||
|
<label class="btn btn-small" :class="{ 'btn-disabled': uploading }">
|
||||||
|
{{ uploading ? 'Загрузка...' : '📷 Загрузить' }}
|
||||||
|
<input type="file" accept="image/*" @change="handleFileSelect" :disabled="uploading"
|
||||||
|
style="display: none;">
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button v-if="previewUrl" class="btn btn-small btn-danger" @click="deleteAvatar" :disabled="uploading">
|
||||||
|
🗑️ Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AvatarUpload',
|
||||||
|
props: {
|
||||||
|
user: Object,
|
||||||
|
showActions: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return { authStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
previewUrl: null,
|
||||||
|
uploading: false,
|
||||||
|
error: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
user: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newUser) {
|
||||||
|
console.log('User data in AvatarUpload:', newUser)
|
||||||
|
if (newUser?.avatar) {
|
||||||
|
console.log('Avatar path:', newUser.avatar)
|
||||||
|
const fullUrl = this.getFullAvatarUrl(newUser.avatar)
|
||||||
|
console.log('Full avatar URL:', fullUrl)
|
||||||
|
this.previewUrl = fullUrl
|
||||||
|
} else {
|
||||||
|
console.log('No avatar found')
|
||||||
|
this.previewUrl = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFullAvatarUrl(avatarPath) {
|
||||||
|
if (!avatarPath) return null;
|
||||||
|
|
||||||
|
console.log('Building URL for avatar path:', avatarPath);
|
||||||
|
|
||||||
|
if (avatarPath.startsWith('http')) {
|
||||||
|
return avatarPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Извлекаем имя файла из пути
|
||||||
|
const filename = avatarPath.trim('/').split('/').pop();
|
||||||
|
|
||||||
|
// Используем API эндпоинт вместо прямого доступа
|
||||||
|
const fullUrl = `https://begushiybashkir.ru/api/v1/user/avatars/${filename}`;
|
||||||
|
console.log('Built URL with API endpoint:', fullUrl);
|
||||||
|
return fullUrl;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleImageError(event) {
|
||||||
|
console.error('Error loading avatar image:', event)
|
||||||
|
this.previewUrl = null
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFileSelect(event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (file) {
|
||||||
|
// Валидация файла
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
this.error = 'Пожалуйста, выберите файл изображения'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) { // 5MB
|
||||||
|
this.error = 'Размер файла не должен превышать 5MB'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем preview
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.previewUrl = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
|
||||||
|
// Загружаем на сервер
|
||||||
|
this.uploadAvatar(file)
|
||||||
|
|
||||||
|
// Сбрасываем input
|
||||||
|
event.target.value = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async uploadAvatar(file) {
|
||||||
|
this.uploading = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.authStore.updateAvatar(file)
|
||||||
|
console.log('Upload result:', result)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.$emit('avatar-updated', result.avatar)
|
||||||
|
} else {
|
||||||
|
this.error = result.error || 'Ошибка загрузки'
|
||||||
|
// Восстанавливаем старый preview
|
||||||
|
this.previewUrl = this.getFullAvatarUrl(this.user?.avatar)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Upload error:', err)
|
||||||
|
this.error = 'Ошибка загрузки: ' + (err.message || 'Неизвестная ошибка')
|
||||||
|
this.previewUrl = this.getFullAvatarUrl(this.user?.avatar)
|
||||||
|
} finally {
|
||||||
|
this.uploading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAvatar() {
|
||||||
|
this.uploading = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.authStore.deleteAvatar()
|
||||||
|
console.log('Delete result:', result)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.previewUrl = null
|
||||||
|
this.$emit('avatar-updated', null)
|
||||||
|
} else {
|
||||||
|
this.error = result.error || 'Ошибка удаления'
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Delete error:', err)
|
||||||
|
this.error = 'Ошибка удаления: ' + (err.message || 'Неизвестная ошибка')
|
||||||
|
} finally {
|
||||||
|
this.uploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.avatar-upload {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid #e0e0e0;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 3rem;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-small {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover:not(:disabled) {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Бургер-меню для всех устройств -->
|
||||||
|
<div class="burger-menu-container">
|
||||||
|
<button
|
||||||
|
class="burger-menu"
|
||||||
|
:class="{ 'active': isMobileMenuOpen }"
|
||||||
|
@click="toggleMobileMenu"
|
||||||
|
:aria-label="isMobileMenuOpen ? 'Закрыть меню' : 'Открыть меню'"
|
||||||
|
:aria-expanded="isMobileMenuOpen"
|
||||||
|
>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Выпадающее меню -->
|
||||||
|
<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>
|
||||||
|
<button class="close-menu" @click="closeMobileMenu" aria-label="Закрыть меню">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown-nav-content">
|
||||||
|
<router-link to="/" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
🏠 Главная
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/profile" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
👤 Профиль
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/about" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
👥 О нас
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/achievements" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
🏆 Достижения
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/gallery" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
📸 Галерея
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/training" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
📅 Тренировки
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/news" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
📰 Новости
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/members" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
👥 Участники
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/reviews" class="dropdown-nav-link" @click="closeMobileMenu">
|
||||||
|
⭐ Отзывы
|
||||||
|
</router-link>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<router-link to="/register" class="dropdown-nav-link accent" @click="closeMobileMenu">
|
||||||
|
📝 Регистрация
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/login" class="dropdown-nav-link accent" @click="closeMobileMenu">
|
||||||
|
🔐 Войти
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Оверлей для мобильных -->
|
||||||
|
<div class="mobile-overlay" :class="{ 'active': isMobileMenuOpen }" @click="closeMobileMenu"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'NavigationMenu',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isMobileMenuOpen: false,
|
||||||
|
resizeTimeout: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleMobileMenu() {
|
||||||
|
this.isMobileMenuOpen = !this.isMobileMenuOpen
|
||||||
|
// Блокируем скролл body когда меню открыто на мобильных
|
||||||
|
if (window.innerWidth <= 767) {
|
||||||
|
document.body.style.overflow = this.isMobileMenuOpen ? 'hidden' : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
closeMobileMenu() {
|
||||||
|
this.isMobileMenuOpen = false
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
},
|
||||||
|
handleClickOutside(event) {
|
||||||
|
if (!event.target.closest('.burger-menu-container') &&
|
||||||
|
!event.target.closest('.dropdown-menu') &&
|
||||||
|
this.isMobileMenuOpen) {
|
||||||
|
this.closeMobileMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleEscapeKey(event) {
|
||||||
|
if (event.key === 'Escape' && this.isMobileMenuOpen) {
|
||||||
|
this.closeMobileMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
// Дебаунс для оптимизации
|
||||||
|
clearTimeout(this.resizeTimeout)
|
||||||
|
this.resizeTimeout = setTimeout(() => {
|
||||||
|
// Закрываем мобильное меню при переходе на десктоп
|
||||||
|
if (window.innerWidth > 767 && this.isMobileMenuOpen) {
|
||||||
|
this.closeMobileMenu()
|
||||||
|
}
|
||||||
|
}, 250)
|
||||||
|
},
|
||||||
|
handleTouchMove(event) {
|
||||||
|
// Обработка свайпа для закрытия меню на мобильных
|
||||||
|
if (this.isMobileMenuOpen && window.innerWidth <= 767) {
|
||||||
|
const touch = event.touches[0]
|
||||||
|
const startX = touch.clientX
|
||||||
|
const menu = this.$refs.dropdownMenu
|
||||||
|
|
||||||
|
// Более точная проверка для свайпа
|
||||||
|
if (menu && startX < 50) { // Свайп от левого края
|
||||||
|
event.preventDefault()
|
||||||
|
this.closeMobileMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
document.addEventListener('click', this.handleClickOutside)
|
||||||
|
document.addEventListener('keydown', this.handleEscapeKey)
|
||||||
|
document.addEventListener('touchmove', this.handleTouchMove, { passive: false })
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
|
||||||
|
// Закрываем меню при навигации
|
||||||
|
this.$router.afterEach(() => {
|
||||||
|
this.closeMobileMenu()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
document.removeEventListener('click', this.handleClickOutside)
|
||||||
|
document.removeEventListener('keydown', this.handleEscapeKey)
|
||||||
|
document.removeEventListener('touchmove', this.handleTouchMove)
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
document.body.style.overflow = '' // Восстанавливаем скролл при размонтировании
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Стили остаются такими же как в App.vue */
|
||||||
|
.burger-menu-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 30px;
|
||||||
|
height: 21px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu span {
|
||||||
|
display: block;
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu.active span:nth-child(1) {
|
||||||
|
transform: rotate(45deg) translate(6px, 6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu.active span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger-menu.active span:nth-child(3) {
|
||||||
|
transform: rotate(-45deg) translate(6px, -6px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
min-width: 280px;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
margin-top: 10px;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-header {
|
||||||
|
display: none;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-logo .little-logo {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-menu {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-menu:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link:hover {
|
||||||
|
background-color: #f8fff8;
|
||||||
|
color: #2e8b57;
|
||||||
|
border-left-color: #2e8b57;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link:active {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link.router-link-active {
|
||||||
|
background-color: #f0f8f0;
|
||||||
|
color: #2e8b57;
|
||||||
|
border-left-color: #2e8b57;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link.accent {
|
||||||
|
background-color: #f8fff8;
|
||||||
|
color: #2e8b57;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link.accent:hover {
|
||||||
|
background-color: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e9ecef;
|
||||||
|
margin: 0.5rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 998;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.dropdown-menu {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: -100%;
|
||||||
|
width: 85%;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100vh;
|
||||||
|
border-radius: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
transition: right 0.3s ease;
|
||||||
|
box-shadow: -5px 0 25px rgba(0, 0, 0, 0.3);
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu.active {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-header {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-menu {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-content {
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link {
|
||||||
|
padding: 1.4rem 1.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-link:hover,
|
||||||
|
.dropdown-nav-link.router-link-active {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 1px solid #2e8b57;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для скроллбара */
|
||||||
|
.dropdown-nav-content::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-content::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #2e8b57;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-nav-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #26734a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import pinia from './stores'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// Инициализация auth store после создания app
|
||||||
|
import { useAuthStore } from './stores/auth'
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
// Инициализируем авторизацию
|
||||||
|
authStore.initializeAuth().then(() => {
|
||||||
|
console.log('Auth initialization completed')
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Auth initialization failed:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
name: 'About',
|
||||||
|
component: () => import('../views/About.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/achievements',
|
||||||
|
name: 'Achievements',
|
||||||
|
component: () => import('../views/Achievements.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/gallery',
|
||||||
|
name: 'Gallery',
|
||||||
|
component: () => import('../views/Gallery.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/training',
|
||||||
|
name: 'Training',
|
||||||
|
component: () => import('../views/Training.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/news',
|
||||||
|
name: 'News',
|
||||||
|
component: () => import('../views/News.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/members',
|
||||||
|
name: 'Members',
|
||||||
|
component: () => import('../views/Members.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/reviews',
|
||||||
|
name: 'Reviews',
|
||||||
|
component: () => import('../views/Reviews.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('../views/Login.vue'),
|
||||||
|
meta: { guestOnly: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
name: 'Profile',
|
||||||
|
component: () => import('../views/Profile.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'Register',
|
||||||
|
component: () => import('../views/Register.vue'),
|
||||||
|
meta: { guestOnly: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/profile/edit',
|
||||||
|
name: 'ProfileEdit',
|
||||||
|
component: () => import('../views/ProfileEdit.vue'),
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/terms',
|
||||||
|
name: 'TermsOfService',
|
||||||
|
component: () => import('../views/TermsOfService.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/privacy',
|
||||||
|
name: 'PrivacyPolicy',
|
||||||
|
component: () => import('../views/PrivacyPolicy.vue')
|
||||||
|
},
|
||||||
|
// Добавляем маршрут для выхода
|
||||||
|
{
|
||||||
|
path: '/logout',
|
||||||
|
name: 'Logout',
|
||||||
|
component: () => import('../views/Logout.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Функция для показа уведомлений
|
||||||
|
function showNotification(message) {
|
||||||
|
// Создаем элемент уведомления
|
||||||
|
const notification = document.createElement('div')
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
`
|
||||||
|
notification.textContent = message
|
||||||
|
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
// Автоматически удаляем через 3 секунды
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.beforeEach(async (to, from, next) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// Проверяем, требует ли маршрут аутентификации
|
||||||
|
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
|
||||||
|
// Если есть токен, пробуем загрузить профиль
|
||||||
|
if (authStore.token) {
|
||||||
|
try {
|
||||||
|
await authStore.fetchProfile()
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Token validation failed:', error)
|
||||||
|
// Если токен невалидный, очищаем его и редиректим на логин
|
||||||
|
authStore.clearAuth()
|
||||||
|
next('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Если нет токена, редиректим на логин
|
||||||
|
next('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, предназначен ли маршрут только для гостей
|
||||||
|
if (to.meta.guestOnly && authStore.isAuthenticated) {
|
||||||
|
showNotification("Вы уже авторизованы. Перенаправляем в профиль...")
|
||||||
|
|
||||||
|
// Ждем немного чтобы пользователь увидел уведомление, затем редиректим
|
||||||
|
setTimeout(() => {
|
||||||
|
next('/profile')
|
||||||
|
}, 2000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если все проверки пройдены, разрешаем навигацию
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
// stores/auth.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { apiClient, withLoading } from './helpers/api'
|
||||||
|
import { handleApiError } from './helpers/api';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
|
// State
|
||||||
|
const user = ref(null)
|
||||||
|
const token = ref(localStorage.getItem('auth_token') || '')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const initialized = ref(false)
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const isAuthenticated = computed(() => !!token.value && !!user.value)
|
||||||
|
const userFullName = computed(() =>
|
||||||
|
user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
|
||||||
|
)
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const setToken = (newToken) => {
|
||||||
|
token.value = newToken
|
||||||
|
localStorage.setItem('auth_token', newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearAuth = () => {
|
||||||
|
token.value = ''
|
||||||
|
user.value = null
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUser = (userData) => {
|
||||||
|
user.value = userData
|
||||||
|
}
|
||||||
|
|
||||||
|
const register = async (userData) => {
|
||||||
|
// Передаем store объект с loading и error
|
||||||
|
return withLoading({ loading, error }, async () => {
|
||||||
|
await apiClient.post('/auth/register', userData)
|
||||||
|
|
||||||
|
// Auto-login after registration
|
||||||
|
const loginResponse = await apiClient.post('/auth/login', {
|
||||||
|
email: userData.email,
|
||||||
|
password: userData.password
|
||||||
|
})
|
||||||
|
|
||||||
|
const { token: authToken, user: userInfo } = loginResponse.data
|
||||||
|
setToken(authToken)
|
||||||
|
setUser(userInfo)
|
||||||
|
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (credentials) => {
|
||||||
|
return withLoading({ loading, error }, async () => {
|
||||||
|
const response = await apiClient.post('/auth/login', credentials)
|
||||||
|
const { token: authToken, user: userInfo } = response.data
|
||||||
|
|
||||||
|
setToken(authToken)
|
||||||
|
setUser(userInfo)
|
||||||
|
|
||||||
|
return { success: true, data: response.data }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
return withLoading({ loading, error }, async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.post('/auth/logout')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Logout error:', err)
|
||||||
|
} finally {
|
||||||
|
clearAuth()
|
||||||
|
}
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchProfile = async () => {
|
||||||
|
return withLoading({ loading, error }, async () => {
|
||||||
|
const response = await apiClient.get('/user/profile')
|
||||||
|
setUser(response.data)
|
||||||
|
return { success: true, data: response.data }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProfile = async (profileData) => {
|
||||||
|
return withLoading({ loading, error }, async () => {
|
||||||
|
const response = await apiClient.post('/user/editProfile', profileData)
|
||||||
|
setUser(response.data)
|
||||||
|
return { success: true, data: response.data }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializeAuth = async () => {
|
||||||
|
if (initialized.value || !token.value) return
|
||||||
|
|
||||||
|
initialized.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetchProfile()
|
||||||
|
console.log('Auth restored successfully')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Auth restoration failed:', err)
|
||||||
|
clearAuth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAvatar = async (avatarFile) => {
|
||||||
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (avatarFile.size > MAX_FILE_SIZE) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Размер файла не должен превышать 5MB'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ПРОВЕРКА ТИПА ФАЙЛА
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
if (!allowedTypes.includes(avatarFile.type)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Допустимые форматы: JPEG, PNG, GIF, '
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('avatar', avatarFile)
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
const response = await apiClient.post('/user/avatar/upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Avatar upload response:', response.data)
|
||||||
|
|
||||||
|
// Универсальная обработка ответа
|
||||||
|
let result
|
||||||
|
if (response.data.success !== undefined) {
|
||||||
|
result = response.data
|
||||||
|
} else {
|
||||||
|
// Если поле success отсутствует, считаем успешным
|
||||||
|
result = { success: true, ...response.data }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// ОБНОВЛЯЕМ ВЕСЬ ПРОФИЛЬ ПОЛЬЗОВАТЕЛЯ
|
||||||
|
await fetchProfile()
|
||||||
|
return { success: true, avatar: result.avatar }
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error || result.message }
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Avatar upload error:', error)
|
||||||
|
const result = handleApiError(error)
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAvatar = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.delete('/user/avatar/delete')
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
// Удаляем аватар из стора
|
||||||
|
if (user.value) {
|
||||||
|
user.value.avatar = null
|
||||||
|
}
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const result = handleApiError(error)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
initialized,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
isAuthenticated,
|
||||||
|
userFullName,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
fetchProfile,
|
||||||
|
updateProfile,
|
||||||
|
initializeAuth,
|
||||||
|
clearAuth,
|
||||||
|
updateAvatar,
|
||||||
|
deleteAvatar
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// stores/helpers/api.js
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const API_BASE_URL = 'https://begushiybashkir.ru/api/v1'
|
||||||
|
|
||||||
|
// Создаем экземпляр axios с базовой конфигурацией
|
||||||
|
export const apiClient = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Интерцептор для автоматического добавления токена
|
||||||
|
apiClient.interceptors.request.use((config) => {
|
||||||
|
const token = localStorage.getItem('auth_token')
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
})
|
||||||
|
|
||||||
|
// Интерцептор для обработки ошибок
|
||||||
|
apiClient.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('auth_token')
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Утилита для обработки ошибок
|
||||||
|
export const handleApiError = (error) => {
|
||||||
|
const message = error.response?.data?.message || error.message || 'Произошла ошибка'
|
||||||
|
return { success: false, error: message }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Утилита для выполнения запросов с loading state
|
||||||
|
export const withLoading = async (store, fn) => {
|
||||||
|
store.loading = true
|
||||||
|
store.error = ''
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (error) {
|
||||||
|
const result = handleApiError(error)
|
||||||
|
store.error = result.error
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
store.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createLoadingHandler = (store) => {
|
||||||
|
return async (fn) => {
|
||||||
|
if (store && typeof store.loading !== 'undefined') {
|
||||||
|
store.loading = true
|
||||||
|
}
|
||||||
|
if (store && typeof store.error !== 'undefined') {
|
||||||
|
store.error = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (error) {
|
||||||
|
const result = handleApiError(error)
|
||||||
|
if (store && typeof store.error !== 'undefined') {
|
||||||
|
store.error = result.error
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
if (store && typeof store.loading !== 'undefined') {
|
||||||
|
store.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = apiClient;
|
||||||
|
export default apiClient;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// stores/index.js
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
export default pinia
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// stores/plugins/persistence.js
|
||||||
|
export const authPersistPlugin = ({ store }) => {
|
||||||
|
// Восстанавливаем состояние при инициализации
|
||||||
|
if (store.$id === 'auth') {
|
||||||
|
store.initializeAuth()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
// stores/user.js
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { handleApiError } from './helpers/api'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
// State
|
||||||
|
const userStats = ref(null)
|
||||||
|
const userTraining = ref(null)
|
||||||
|
const userAchievements = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
const completedAchievements = computed(() =>
|
||||||
|
userAchievements.value.filter(achievement => achievement.achieved)
|
||||||
|
)
|
||||||
|
|
||||||
|
const pendingAchievements = computed(() =>
|
||||||
|
userAchievements.value.filter(achievement => !achievement.achieved)
|
||||||
|
)
|
||||||
|
|
||||||
|
const achievementProgress = computed(() => {
|
||||||
|
if (!userAchievements.value.length) return 0
|
||||||
|
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Вспомогательная функция для обработки loading/error
|
||||||
|
const withStoreLoading = async (fn) => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
return await fn()
|
||||||
|
} catch (err) {
|
||||||
|
const result = handleApiError(err)
|
||||||
|
error.value = result.error
|
||||||
|
return result
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const fetchUserStats = async () => {
|
||||||
|
return withStoreLoading(async () => {
|
||||||
|
// TODO: Заменить на реальный endpoint когда будет готов
|
||||||
|
// const response = await apiClient.get('/user/stats')
|
||||||
|
|
||||||
|
// Временные мок данные
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
userStats.value = {
|
||||||
|
totalDistance: 245,
|
||||||
|
bestResult: '10км - 48:15',
|
||||||
|
totalWorkouts: 36,
|
||||||
|
weeklyDistance: 25,
|
||||||
|
monthlyDistance: 98,
|
||||||
|
avgPace: '5:15',
|
||||||
|
caloriesBurned: 12450
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: userStats.value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserTraining = async () => {
|
||||||
|
return withStoreLoading(async () => {
|
||||||
|
// TODO: Заменить на реальный endpoint когда будет готов
|
||||||
|
// const response = await apiClient.get('/user/training')
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
userTraining.value = {
|
||||||
|
currentWeek: 4,
|
||||||
|
totalWeeks: 12,
|
||||||
|
nextWorkout: '2024-03-20T18:00:00',
|
||||||
|
workouts: [
|
||||||
|
{ id: 1, date: '2024-03-18', type: 'interval', distance: '8km', completed: true },
|
||||||
|
{ id: 2, date: '2024-03-20', type: 'tempo', distance: '10km', completed: false },
|
||||||
|
{ id: 3, date: '2024-03-22', type: 'long', distance: '15km', completed: false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, data: userTraining.value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserAchievements = async () => {
|
||||||
|
return withStoreLoading(async () => {
|
||||||
|
// TODO: Заменить на реальный endpoint когда будет готов
|
||||||
|
// const response = await apiClient.get('/user/achievements')
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
userAchievements.value = [
|
||||||
|
{ id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' },
|
||||||
|
{ id: 2, name: 'Неделя тренировок', description: 'Тренироваться 7 дней подряд', achieved: true, date: '2024-02-15' },
|
||||||
|
{ id: 3, name: '100 км', description: 'Пробежать 100 км', achieved: true, date: '2024-03-01' },
|
||||||
|
{ id: 4, name: 'Полумарафон', description: 'Пробежать 21.1 км', achieved: false },
|
||||||
|
{ id: 5, name: 'Скорость', description: 'Пробежать 5км быстрее 25 минут', achieved: false }
|
||||||
|
]
|
||||||
|
|
||||||
|
return { success: true, data: userAchievements.value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пакетная загрузка всех данных пользователя
|
||||||
|
const fetchAllUserData = async () => {
|
||||||
|
return withStoreLoading(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
fetchUserStats(),
|
||||||
|
fetchUserTraining(),
|
||||||
|
fetchUserAchievements()
|
||||||
|
])
|
||||||
|
return { success: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
userStats,
|
||||||
|
userTraining,
|
||||||
|
userAchievements,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
completedAchievements,
|
||||||
|
pendingAchievements,
|
||||||
|
achievementProgress,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
fetchUserStats,
|
||||||
|
fetchUserTraining,
|
||||||
|
fetchUserAchievements,
|
||||||
|
fetchAllUserData
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,775 @@
|
|||||||
|
<template>
|
||||||
|
<div class="about-page">
|
||||||
|
<!-- Герой-секция -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="hero-title">О беговом клубе "Бегущий Башкир"</h1>
|
||||||
|
<p class="hero-subtitle">Объединяем любителей бега в Уфе с 2022 года</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- О клубе -->
|
||||||
|
<section class="section club-info">
|
||||||
|
<div class="container">
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-content">
|
||||||
|
<h2>Наша философия</h2>
|
||||||
|
<p class="lead">
|
||||||
|
Мы верим, что бег — это не просто спорт, а образ жизни, который объединяет людей,
|
||||||
|
укрепляет здоровье и открывает новые горизонты.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mission-vision">
|
||||||
|
<div class="mission-card">
|
||||||
|
<h3>🎯 Наша миссия</h3>
|
||||||
|
<p>Сделать бег доступным и enjoyable для каждого жителя Уфы, независимо от возраста и уровня подготовки.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mission-card">
|
||||||
|
<h3>👁️ Наше видение</h3>
|
||||||
|
<p>Стать крупнейшим беговым сообществом в Башкортостане, которое вдохновляет на здоровый образ жизни.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">150+</div>
|
||||||
|
<div class="stat-label">Участников</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">25+</div>
|
||||||
|
<div class="stat-label">Мероприятий в год</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">42.2</div>
|
||||||
|
<div class="stat-label">км лучший марафон</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">100%</div>
|
||||||
|
<div class="stat-label">Дружеская атмосфера</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-image">
|
||||||
|
<img :src="getImageUrl('UMM2025.png')" alt="Команда бегового клуба Бегущий Башкир" class="club-image">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Наши ценности -->
|
||||||
|
<section class="section values-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Наши ценности</h2>
|
||||||
|
<div class="values-grid">
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">🤝</div>
|
||||||
|
<h3>Поддержка</h3>
|
||||||
|
<p>Мы поддерживаем друг друга на каждом километре, как на тренировках, так и на соревнованиях.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">📈</div>
|
||||||
|
<h3>Развитие</h3>
|
||||||
|
<p>Помогаем каждому участнику прогрессировать и достигать личных рекордов.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">🌿</div>
|
||||||
|
<h3>Единение с природой</h3>
|
||||||
|
<p>Тренируемся в парках и на природе, наслаждаясь свежим воздухом Уфы.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="value-card">
|
||||||
|
<div class="value-icon">🏆</div>
|
||||||
|
<h3>Спортивный дух</h3>
|
||||||
|
<p>Стремимся к победам, но ценим участие и личный прогресс выше медалей.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Тренер -->
|
||||||
|
<section class="section coach-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Наш тренер</h2>
|
||||||
|
|
||||||
|
<div class="coach-profile">
|
||||||
|
<div class="coach-image">
|
||||||
|
<img :src="getImageUrl('/ZagirTrainer3.jpg')" alt="Аминев Загир - тренер бегового клуба Бегущий Башкир"
|
||||||
|
class="coach-photo">
|
||||||
|
<div class="coach-badges">
|
||||||
|
<span class="badge">Мастер спорта</span>
|
||||||
|
<span class="badge">КМС</span>
|
||||||
|
<span class="badge">Опыт 10+ лет</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-details">
|
||||||
|
<h3>Аминев Загир Рамилевич</h3>
|
||||||
|
<p class="coach-title">Основатель и главный тренер клуба</p>
|
||||||
|
|
||||||
|
<div class="coach-achievements">
|
||||||
|
<h4>Спортивные достижения:</h4>
|
||||||
|
<ul class="achievements-list">
|
||||||
|
<li>🥇 Мастер спорта по полиатлону</li>
|
||||||
|
<li>🥈 Кандидат в мастера спорта по скайраннингу</li>
|
||||||
|
<li>🏆 Победитель всероссийских соревнований по горному бегу</li>
|
||||||
|
<li>🎯 Призер международных стартов по трейлу</li>
|
||||||
|
<li>📚 Сертифицированный тренер по бегу</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-philosophy">
|
||||||
|
<h4>Философия тренировок:</h4>
|
||||||
|
<blockquote>
|
||||||
|
"Бег должен приносить радость! Я помогаю каждому найти свой темп,
|
||||||
|
полюбить процесс и безопасно достигать целей. От первых 5 км до марафона —
|
||||||
|
мы пройдем этот путь вместе!"
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-stats">
|
||||||
|
<div class="coach-stat">
|
||||||
|
<div class="stat-value">500+</div>
|
||||||
|
<div class="stat-desc">подготовленных бегунов</div>
|
||||||
|
</div>
|
||||||
|
<div class="coach-stat">
|
||||||
|
<div class="stat-value">100+</div>
|
||||||
|
<div class="stat-desc">проведенных марафонов</div>
|
||||||
|
</div>
|
||||||
|
<div class="coach-stat">
|
||||||
|
<div class="stat-value">10+</div>
|
||||||
|
<div class="stat-desc">лет тренерского опыта</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Тренировочные локации -->
|
||||||
|
<section class="section locations-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Наши тренировочные базы</h2>
|
||||||
|
|
||||||
|
<div class="locations-grid">
|
||||||
|
<div class="location-card">
|
||||||
|
<div class="location-image">
|
||||||
|
<img :src="getImageUrl('UfaWhiteRiver.jpg')"
|
||||||
|
alt="Уфимская набережная (ост. Монумент) - основная тренировочная площадка">
|
||||||
|
</div>
|
||||||
|
<div class="location-info">
|
||||||
|
<h3>🏞️ Набережная </h3>
|
||||||
|
<p><strong>Основная площадка</strong></p>
|
||||||
|
<p>Идеальные беговые дорожки, освещенная трасса, прекрасный воздух</p>
|
||||||
|
<div class="location-features">
|
||||||
|
<span class="feature">📏 Круг 5 км</span>
|
||||||
|
<span class="feature">💡 Освещение</span>
|
||||||
|
<span class="feature">🚽 Уборная</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="location-card">
|
||||||
|
<div class="location-image">
|
||||||
|
<img :src="getImageUrl('dinamo.jpg')" alt="Стадион Динамо - для интервальных тренировок">
|
||||||
|
</div>
|
||||||
|
<div class="location-info">
|
||||||
|
<h3>🏟️ Стадион "Динамо"</h3>
|
||||||
|
<p><strong>Интервальные тренировки</strong></p>
|
||||||
|
<p>Профессиональное покрытие, идеально для работы над техникой и скоростью</p>
|
||||||
|
<div class="location-features">
|
||||||
|
<span class="feature">📏 Стандартная дорожка</span>
|
||||||
|
<span class="feature">🎯 Тренерский контроль</span>
|
||||||
|
<span class="feature">⚡ СБУ и темповая работа</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="location-card">
|
||||||
|
<div class="location-image">
|
||||||
|
<img :src="getImageUrl('i.webp')" alt="Лесопарковая зона - для длительных кроссов. Уфимское ожерелье">
|
||||||
|
</div>
|
||||||
|
<div class="location-info">
|
||||||
|
<h3>🌲 Лесопарковая зона</h3>
|
||||||
|
<p><strong>Длительные кроссы</strong></p>
|
||||||
|
<p>Живописные маршруты, подготовка к трейлам, бег по пересеченной местности</p>
|
||||||
|
<div class="location-features">
|
||||||
|
<span class="feature">🌳 Природа</span>
|
||||||
|
<span class="feature">🔄 Маршруты 5-20 км</span>
|
||||||
|
<span class="feature">🏔️ Перепады высот</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Призыв к действию -->
|
||||||
|
<section class="section cta-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Готовы начать бегать с нами?</h2>
|
||||||
|
<p>Присоединяйтесь к нашей беговой семье и откройте для себя мир возможностей!</p>
|
||||||
|
<div class="cta-buttons">
|
||||||
|
<router-link to="/register" class="btn btn-primary">
|
||||||
|
🏃 Стать участником
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/training" class="btn btn-secondary">
|
||||||
|
📅 Посмотреть расписание
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/contact" class="btn btn-outline">
|
||||||
|
📞 Связаться с нами
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Контакты -->
|
||||||
|
<section class="section contact-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Контакты</h2>
|
||||||
|
<div class="contact-grid">
|
||||||
|
<div class="contact-info">
|
||||||
|
<h3>📞 Свяжитесь с нами</h3>
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Телефон:</strong>
|
||||||
|
<a href="tel:+79273093095"> +7 (927) 30-93-095</a>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Email:</strong>
|
||||||
|
<a href="mailto:zog1r@mail.ru"> zog1r@mail.ru</a>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Telegram:</strong>
|
||||||
|
<a href="https://t.me/begushiybashkir" target="_blank"> @begushiybashkir</a>
|
||||||
|
</div>
|
||||||
|
<div class="contact-item">
|
||||||
|
<strong>Город:</strong>
|
||||||
|
Уфа, Республика Башкортостан
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="social-links">
|
||||||
|
<h3>📱 Мы в соцсетях</h3>
|
||||||
|
<div class="social-buttons">
|
||||||
|
<a href="https://www.instagram.com/begushiybashkir/" target="_blank" class="social-btn instagram">
|
||||||
|
📷 Instagram
|
||||||
|
</a>
|
||||||
|
<a href="https://www.youtube.com/channel/UCV45f8q172917848k05q6gA" target="_blank"
|
||||||
|
class="social-btn youtube">
|
||||||
|
🎥 YouTube
|
||||||
|
</a>
|
||||||
|
<a href="https://t.me/begushiybashkir" target="_blank" class="social-btn telegram">
|
||||||
|
✈️ Telegram
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'About',
|
||||||
|
metaInfo: {
|
||||||
|
title: 'О нас - Бегущий Башкир | Беговой клуб в Уфе',
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: 'Беговой клуб Бегущий Башкир в Уфе: профессиональный тренер Аминев Загир, тренировки в парках Уфы, достижения и философия клуба. Присоединяйтесь к нашему сообществу!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keywords',
|
||||||
|
content: 'беговой клуб Уфа, тренер по бегу Уфа, Аминев Загир, тренировки бег Уфа, марафон Уфа, бег в Башкортостане'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getImageUrl(path) {
|
||||||
|
// В продакшене замените на правильный путь
|
||||||
|
const baseUrl = import.meta.env.BASE_URL
|
||||||
|
// Путь от корня public/
|
||||||
|
console.log(`${baseUrl}images/${path}`)
|
||||||
|
return `${baseUrl}images/${path}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.about-page {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Герой-секция */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Основные секции */
|
||||||
|
.section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Информация о клубе */
|
||||||
|
.club-info {
|
||||||
|
background: #f8fff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-vision {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-card {
|
||||||
|
background: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mission-card h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.club-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ценности */
|
||||||
|
.values-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Тренер */
|
||||||
|
.coach-section {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-profile {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 400px 1fr;
|
||||||
|
gap: 3rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-image {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-photo {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-badges {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-details h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-achievements,
|
||||||
|
.coach-philosophy {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-achievements h4,
|
||||||
|
.coach-philosophy h4 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-list li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
background: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
font-style: italic;
|
||||||
|
color: #555;
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-stat {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Локации */
|
||||||
|
.locations-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-info h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-features {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
background: #f0f8f0;
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA секция */
|
||||||
|
.cta-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #e6c200;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
border: 2px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Контакты */
|
||||||
|
.contact-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a {
|
||||||
|
color: #2e8b57;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instagram {
|
||||||
|
background: #E4405F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.youtube {
|
||||||
|
background: #CD201F;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram {
|
||||||
|
background: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-profile {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locations-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,992 @@
|
|||||||
|
<template>
|
||||||
|
<div class="achievements-page">
|
||||||
|
<!-- Герой-секция -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="hero-title">🏆 Наши достижения</h1>
|
||||||
|
<p class="hero-subtitle">Гордимся каждым участником и каждым результатом!</p>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{{ totalAchievements }}</div>
|
||||||
|
<div class="stat-label">Достижений</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{{ activeMembers }}</div>
|
||||||
|
<div class="stat-label">Активных участников</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">{{ personalBests }}</div>
|
||||||
|
<div class="stat-label">Личных рекордов</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Фильтры и поиск -->
|
||||||
|
<section class="filters-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="filters-container">
|
||||||
|
<div class="search-box">
|
||||||
|
<input
|
||||||
|
v-model="searchQuery"
|
||||||
|
type="text"
|
||||||
|
placeholder="🔍 Поиск по имени или дистанции..."
|
||||||
|
class="search-input"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<button
|
||||||
|
v-for="filter in filters"
|
||||||
|
:key="filter.value"
|
||||||
|
:class="['filter-btn', { 'active': activeFilter === filter.value }]"
|
||||||
|
@click="setFilter(filter.value)"
|
||||||
|
>
|
||||||
|
{{ filter.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="view-toggle">
|
||||||
|
<button
|
||||||
|
:class="['view-btn', { 'active': viewMode === 'grid' }]"
|
||||||
|
@click="viewMode = 'grid'"
|
||||||
|
title="Сетка"
|
||||||
|
>
|
||||||
|
▦
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="['view-btn', { 'active': viewMode === 'list' }]"
|
||||||
|
@click="viewMode = 'list'"
|
||||||
|
title="Список"
|
||||||
|
>
|
||||||
|
≡
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Достижения -->
|
||||||
|
<section class="achievements-section">
|
||||||
|
<div class="container">
|
||||||
|
<!-- Командные достижения -->
|
||||||
|
<div class="category-section">
|
||||||
|
<h2 class="category-title">🏆 Командные достижения</h2>
|
||||||
|
<div class="achievements-grid" :class="viewMode">
|
||||||
|
<div
|
||||||
|
v-for="achievement in teamAchievements"
|
||||||
|
:key="achievement.id"
|
||||||
|
class="achievement-card team-card"
|
||||||
|
>
|
||||||
|
<div class="achievement-icon">🏅</div>
|
||||||
|
<h3>{{ achievement.title }}</h3>
|
||||||
|
<p>{{ achievement.description }}</p>
|
||||||
|
<div class="achievement-meta">
|
||||||
|
<span class="year">{{ achievement.year }}</span>
|
||||||
|
<span class="type">{{ achievement.type }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Личные достижения по дистанциям -->
|
||||||
|
<div
|
||||||
|
v-for="category in filteredCategories"
|
||||||
|
:key="category.id"
|
||||||
|
class="category-section"
|
||||||
|
>
|
||||||
|
<h2 class="category-title">{{ category.icon }} {{ category.title }}</h2>
|
||||||
|
<div class="achievements-grid" :class="viewMode">
|
||||||
|
<div
|
||||||
|
v-for="achievement in category.achievements"
|
||||||
|
:key="achievement.id"
|
||||||
|
class="achievement-card"
|
||||||
|
:class="getAchievementClass(achievement)"
|
||||||
|
>
|
||||||
|
<div class="achievement-header">
|
||||||
|
<h3>{{ achievement.name }}</h3>
|
||||||
|
<span class="result">{{ achievement.result }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="achievement-details">
|
||||||
|
<p class="pace" v-if="achievement.pace">Темп: {{ achievement.pace }}</p>
|
||||||
|
<p class="note" v-if="achievement.note">{{ achievement.note }}</p>
|
||||||
|
|
||||||
|
<div class="achievement-links" v-if="achievement.telegram">
|
||||||
|
<a
|
||||||
|
:href="achievement.telegram"
|
||||||
|
target="_blank"
|
||||||
|
class="telegram-link"
|
||||||
|
title="Написать в Telegram"
|
||||||
|
>
|
||||||
|
📱 @{{ getTelegramUsername(achievement.telegram) }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="achievement-meta">
|
||||||
|
<span
|
||||||
|
v-if="achievement.pb"
|
||||||
|
class="badge pb-badge"
|
||||||
|
title="Личный рекорд"
|
||||||
|
>
|
||||||
|
PB
|
||||||
|
</span>
|
||||||
|
<span class="distance">{{ category.distance }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Статистика -->
|
||||||
|
<div class="stats-section">
|
||||||
|
<h2 class="section-title">📊 Статистика клуба</h2>
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🚀</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h3>Самый быстрый марафон</h3>
|
||||||
|
<p class="stat-value">3:27:49</p>
|
||||||
|
<p class="stat-person">Сергей</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">🏔️</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h3>Самая длинная дистанция</h3>
|
||||||
|
<p class="stat-value">120 км</p>
|
||||||
|
<p class="stat-person">Даниил Хайбуллин</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-icon">⭐</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h3>Больше всего PB</h3>
|
||||||
|
<p class="stat-value">5 рекордов</p>
|
||||||
|
<p class="stat-person">Ғаяз</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Призыв к действию -->
|
||||||
|
<div class="cta-section">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Хочешь попасть в этот список?</h2>
|
||||||
|
<p>Присоединяйся к нашему клубу и начни свой путь к новым достижениям!</p>
|
||||||
|
<div class="cta-buttons">
|
||||||
|
<router-link to="/training" class="btn btn-primary">
|
||||||
|
📅 Начать тренироваться
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/register" class="btn btn-secondary">
|
||||||
|
🏃 Вступить в клуб
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Achievements',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchQuery: '',
|
||||||
|
activeFilter: 'all',
|
||||||
|
viewMode: 'grid',
|
||||||
|
filters: [
|
||||||
|
{ value: 'all', label: 'Все' },
|
||||||
|
{ value: 'marathon', label: 'Марафон' },
|
||||||
|
{ value: 'half', label: 'Полумарафон' },
|
||||||
|
{ value: 'short', label: 'Короткие' },
|
||||||
|
{ value: 'trail', label: 'Трейл' }
|
||||||
|
],
|
||||||
|
teamAchievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Эстафета 4×400м',
|
||||||
|
description: 'III место среди беговых клубов',
|
||||||
|
year: '2024, 2025',
|
||||||
|
type: 'Командное'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
achievementCategories: [
|
||||||
|
{
|
||||||
|
id: 'marathon',
|
||||||
|
title: 'Марафон 42.2 км',
|
||||||
|
icon: '🏃',
|
||||||
|
distance: '42.2 км',
|
||||||
|
achievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Сергей',
|
||||||
|
result: '3:27:49',
|
||||||
|
telegram: 'https://t.me/Sergeicortess',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Ғаяз',
|
||||||
|
result: '3:34:33',
|
||||||
|
telegram: 'https://t.me/GeniusUfa',
|
||||||
|
pb: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'half',
|
||||||
|
title: 'Полумарафон 21.1 км',
|
||||||
|
icon: '🎯',
|
||||||
|
distance: '21.1 км',
|
||||||
|
achievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Ильгам',
|
||||||
|
result: '1:23:33',
|
||||||
|
telegram: 'https://t.me/Ilgam25883',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Данил',
|
||||||
|
result: '1:30:40',
|
||||||
|
pace: "4'16",
|
||||||
|
telegram: 'https://t.me/Khaybullin_D',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Ғаяз',
|
||||||
|
result: '1:31:40',
|
||||||
|
pace: "4'20",
|
||||||
|
telegram: 'https://t.me/GeniusUfa',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Булат',
|
||||||
|
result: '1:45:48',
|
||||||
|
pace: "5'00",
|
||||||
|
telegram: 'https://t.me/Bulat_Vakhitov',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Ильвира',
|
||||||
|
result: '1:45:48',
|
||||||
|
pace: "5'00",
|
||||||
|
telegram: 'https://t.me/Yahina_Ilvira',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Булат',
|
||||||
|
result: '2:08:30',
|
||||||
|
pace: "6'05",
|
||||||
|
telegram: 'https://t.me/Bulatiwe',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ten',
|
||||||
|
title: '10 км',
|
||||||
|
icon: '⚡',
|
||||||
|
distance: '10 км',
|
||||||
|
achievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Эдуард',
|
||||||
|
result: '36:52',
|
||||||
|
pace: "3'41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Илһам',
|
||||||
|
result: '37:59',
|
||||||
|
pace: "3'47",
|
||||||
|
telegram: 'https://t.me/Ilgam25883'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Арыҫлан',
|
||||||
|
result: '38:25',
|
||||||
|
pace: "3'50",
|
||||||
|
telegram: 'https://t.me/Just_Aryslan'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Ибрагимов Ринат',
|
||||||
|
result: '38:49'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Гүзәл Гузель Ахмадуллина',
|
||||||
|
result: '53:25',
|
||||||
|
pace: "5'20"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Финат Гайфуллин',
|
||||||
|
result: '56:46',
|
||||||
|
pace: "5'40"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: 'Регина',
|
||||||
|
result: '59:43',
|
||||||
|
pace: "5'58",
|
||||||
|
telegram: 'https://t.me/massageregina'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'five',
|
||||||
|
title: '5 км',
|
||||||
|
icon: '🎽',
|
||||||
|
distance: '5 км',
|
||||||
|
achievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Ғәзиз',
|
||||||
|
result: '25:13',
|
||||||
|
pace: "5'02",
|
||||||
|
telegram: 'https://t.me/valitovgaziz',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Зарема',
|
||||||
|
result: '28:22',
|
||||||
|
pace: "5'40",
|
||||||
|
telegram: 'https://t.me/am1neva',
|
||||||
|
note: 'PB',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Камила',
|
||||||
|
result: '32:23',
|
||||||
|
pace: "6'28",
|
||||||
|
telegram: 'https://t.me/khayrutdinova_kamila'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Айгөл',
|
||||||
|
result: '37:23',
|
||||||
|
pace: "7'28",
|
||||||
|
telegram: 'https://t.me/Aigulika_Elis'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'trail',
|
||||||
|
title: 'Трейловые дистанции',
|
||||||
|
icon: '🏔️',
|
||||||
|
distance: 'Разные дистанции',
|
||||||
|
achievements: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Хайбуллин Даниил',
|
||||||
|
result: '120 км',
|
||||||
|
note: 'III место на Batyr BackYard Ultra',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Ибрагимов Ринат',
|
||||||
|
result: '22 км',
|
||||||
|
note: 'III место на ультрамарафоне «Мир!Труд! Май!»',
|
||||||
|
pb: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Хайбуллин Азамат',
|
||||||
|
result: '6 км',
|
||||||
|
note: 'III место на Karst trail',
|
||||||
|
pb: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
totalAchievements() {
|
||||||
|
return this.achievementCategories.reduce((total, category) => {
|
||||||
|
return total + category.achievements.length
|
||||||
|
}, 0) + this.teamAchievements.length
|
||||||
|
},
|
||||||
|
activeMembers() {
|
||||||
|
const members = new Set()
|
||||||
|
this.achievementCategories.forEach(category => {
|
||||||
|
category.achievements.forEach(achievement => {
|
||||||
|
members.add(achievement.name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return members.size
|
||||||
|
},
|
||||||
|
personalBests() {
|
||||||
|
let count = 0
|
||||||
|
this.achievementCategories.forEach(category => {
|
||||||
|
category.achievements.forEach(achievement => {
|
||||||
|
if (achievement.pb) count++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return count
|
||||||
|
},
|
||||||
|
filteredCategories() {
|
||||||
|
if (this.activeFilter === 'all' && !this.searchQuery) {
|
||||||
|
return this.achievementCategories
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.achievementCategories
|
||||||
|
.filter(category => {
|
||||||
|
if (this.activeFilter === 'all') return true
|
||||||
|
if (this.activeFilter === 'short') {
|
||||||
|
return category.id === 'five' || category.id === 'ten'
|
||||||
|
}
|
||||||
|
return category.id === this.activeFilter
|
||||||
|
})
|
||||||
|
.map(category => ({
|
||||||
|
...category,
|
||||||
|
achievements: category.achievements.filter(achievement => {
|
||||||
|
if (!this.searchQuery) return true
|
||||||
|
const query = this.searchQuery.toLowerCase()
|
||||||
|
return (
|
||||||
|
achievement.name.toLowerCase().includes(query) ||
|
||||||
|
category.title.toLowerCase().includes(query) ||
|
||||||
|
(achievement.note && achievement.note.toLowerCase().includes(query))
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
.filter(category => category.achievements.length > 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setFilter(filter) {
|
||||||
|
this.activeFilter = filter
|
||||||
|
},
|
||||||
|
getTelegramUsername(url) {
|
||||||
|
return url.split('/').pop()
|
||||||
|
},
|
||||||
|
getAchievementClass(achievement) {
|
||||||
|
const classes = []
|
||||||
|
if (achievement.pb) classes.push('pb-achievement')
|
||||||
|
if (achievement.telegram) classes.push('has-contact')
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.achievements-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fff8 0%, #f0f8f0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Герой-секция */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 80px 0 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffd700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Фильтры */
|
||||||
|
.filters-section {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
position: sticky;
|
||||||
|
top: 80px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
box-shadow: 0 0 0 3px rgba(46, 139, 87, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn:hover {
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn.active {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Достижения */
|
||||||
|
.achievements-section {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-section {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 3px solid #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Сетка достижений */
|
||||||
|
.achievements-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-grid.grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-grid.list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.team-card {
|
||||||
|
border-left-color: #ffd700;
|
||||||
|
background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card.pb-achievement {
|
||||||
|
border-left-color: #e74c3c;
|
||||||
|
background: linear-gradient(135deg, #ffe6e6 0%, #ffcccc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-header h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-details {
|
||||||
|
space-y: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pace {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
color: #e74c3c;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-links {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-link {
|
||||||
|
color: #0088cc;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.telegram-link:hover {
|
||||||
|
color: #005580;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pb-badge {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.distance {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Статистика */
|
||||||
|
.stats-section {
|
||||||
|
margin: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
text-align: center;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-person {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA секция */
|
||||||
|
.cta-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #e6c200;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-container {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-toggle {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-grid.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-section {
|
||||||
|
padding: 60px 0 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимации */
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievement-card {
|
||||||
|
animation: fadeInUp 0.6s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,963 @@
|
|||||||
|
<template>
|
||||||
|
<div class="gallery-page">
|
||||||
|
<!-- Герой-секция -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="hero-title">📸 Наша галерея</h1>
|
||||||
|
<p class="hero-subtitle">Запечатленные моменты тренировок, соревнований и дружеских встреч</p>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">24+</div>
|
||||||
|
<div class="stat-label">Фотографий</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">12+</div>
|
||||||
|
<div class="stat-label">Мероприятий</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">100%</div>
|
||||||
|
<div class="stat-label">Эмоций</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Основной слайдер -->
|
||||||
|
<section class="slider-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Лучшие моменты</h2>
|
||||||
|
<div class="slider-container">
|
||||||
|
<div class="slider" :style="{ transform: `translateX(-${currentSlide * 100}%)` }">
|
||||||
|
<div v-for="(slide, index) in slides" :key="index" class="slide"
|
||||||
|
:class="{ 'active': currentSlide === index }">
|
||||||
|
<img :src="getImageUrl(slide.src)" :alt="slide.alt" class="slide-image" @load="imageLoaded(index)">
|
||||||
|
<div class="slide-overlay">
|
||||||
|
<div class="slide-content">
|
||||||
|
<h3>{{ slide.title }}</h3>
|
||||||
|
<p>{{ slide.description }}</p>
|
||||||
|
<span class="slide-date">{{ slide.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Навигация слайдера -->
|
||||||
|
<button class="slider-nav prev" @click="prevSlide" aria-label="Предыдущее фото">
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
<button class="slider-nav next" @click="nextSlide" aria-label="Следующее фото">
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Индикаторы -->
|
||||||
|
<div class="slider-indicators">
|
||||||
|
<button v-for="(slide, index) in slides" :key="index"
|
||||||
|
:class="['indicator', { 'active': currentSlide === index }]" @click="goToSlide(index)"
|
||||||
|
:aria-label="`Перейти к фото ${index + 1}`"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Счетчик -->
|
||||||
|
<div class="slider-counter">
|
||||||
|
{{ currentSlide + 1 }} / {{ slides.length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Мини-галерея -->
|
||||||
|
<section class="gallery-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Все фотографии</h2>
|
||||||
|
|
||||||
|
<!-- Фильтры -->
|
||||||
|
<div class="gallery-filters">
|
||||||
|
<button v-for="filter in filters" :key="filter.value"
|
||||||
|
:class="['filter-btn', { 'active': activeFilter === filter.value }]" @click="setFilter(filter.value)">
|
||||||
|
{{ filter.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Сетка фотографий -->
|
||||||
|
<div class="gallery-grid">
|
||||||
|
<div v-for="(image, index) in filteredImages" :key="index" class="gallery-item" @click="openLightbox(index)">
|
||||||
|
<div>{{ getImageUrl(image.src) }}</div>
|
||||||
|
<img :src="getImageUrl(image.src)" :alt="image.alt" class="gallery-image" loading="lazy">
|
||||||
|
<div class="gallery-overlay">
|
||||||
|
<div class="overlay-content">
|
||||||
|
<span class="image-date">{{ image.date }}</span>
|
||||||
|
<button class="zoom-btn" aria-label="Увеличить фото">
|
||||||
|
🔍
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Кнопка загрузки еще -->
|
||||||
|
<div class="load-more" v-if="visibleImages < allImages.length">
|
||||||
|
<button class="btn btn-outline" @click="loadMore">
|
||||||
|
📁 Загрузить еще фото
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Лайтбокс -->
|
||||||
|
<div class="lightbox" :class="{ 'active': lightboxActive }" v-if="lightboxActive">
|
||||||
|
<div class="lightbox-content">
|
||||||
|
<button class="lightbox-close" @click="closeLightbox" aria-label="Закрыть">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<button class="lightbox-nav prev" @click="prevLightbox" aria-label="Предыдущее фото">
|
||||||
|
‹
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="lightbox-image-container">
|
||||||
|
<img :src="getImageUrl(currentLightboxImage.src)" :alt="currentLightboxImage.alt" class="lightbox-image">
|
||||||
|
<div class="lightbox-info">
|
||||||
|
<h3>{{ currentLightboxImage.title }}</h3>
|
||||||
|
<p>{{ currentLightboxImage.description }}</p>
|
||||||
|
<span class="lightbox-date">{{ currentLightboxImage.date }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="lightbox-nav next" @click="nextLightbox" aria-label="Следующее фото">
|
||||||
|
›
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="lightbox-overlay" @click="closeLightbox"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Призыв к действию -->
|
||||||
|
<section class="cta-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Хочешь попасть в нашу галерею?</h2>
|
||||||
|
<p>Присоединяйся к нашей беговой семье и стань частью этих ярких моментов!</p>
|
||||||
|
<div class="cta-buttons">
|
||||||
|
<router-link to="/training" class="btn btn-primary">
|
||||||
|
📅 Посмотреть расписание
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/register" class="btn btn-secondary">
|
||||||
|
🏃 Вступить в клуб
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="cta-features">
|
||||||
|
<div class="feature">✅ Бесплатная первая тренировка</div>
|
||||||
|
<div class="feature">✅ Профессиональный тренер</div>
|
||||||
|
<div class="feature">✅ Дружеская атмосфера</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Gallery',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentSlide: 0,
|
||||||
|
lightboxActive: false,
|
||||||
|
currentLightboxIndex: 0,
|
||||||
|
activeFilter: 'all',
|
||||||
|
visibleImages: 12,
|
||||||
|
slides: [
|
||||||
|
{
|
||||||
|
src: 'slider/slider24.jpg',
|
||||||
|
alt: 'Беговой клуб Бегущий Башкир, РосХим Стерлитамак Забег 2025',
|
||||||
|
title: 'РосХим Стерлитамак 2025',
|
||||||
|
description: 'Наши участники на крупном забеге',
|
||||||
|
date: 'Январь 2025'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'slider/slider23.png',
|
||||||
|
alt: 'Беговой клуб, общее фото УММ 2025',
|
||||||
|
title: 'Уфимский марафон 2025',
|
||||||
|
description: 'Командное фото после успешного забега',
|
||||||
|
date: 'Январь 2025'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'slider/slider1.jpg',
|
||||||
|
alt: 'Беговой клуб, общее фото',
|
||||||
|
title: 'Тренировка в парке',
|
||||||
|
description: 'Регулярные занятия на свежем воздухе',
|
||||||
|
date: 'Декабрь 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'slider/slider2.jpg',
|
||||||
|
alt: 'Беговой клуб, общее фото',
|
||||||
|
title: 'Техника бега',
|
||||||
|
description: 'Работа над правильной техникой',
|
||||||
|
date: 'Декабрь 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'slider/slider4.jpg',
|
||||||
|
alt: 'Беговой клуб, общее фото',
|
||||||
|
title: 'Групповая тренировка',
|
||||||
|
description: 'Поддержка и мотивация в команде',
|
||||||
|
date: 'Ноябрь 2024'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'slider/slider5.jpg',
|
||||||
|
alt: 'Беговой клуб, общее фото',
|
||||||
|
title: 'Соревнования',
|
||||||
|
description: 'Участие в городских забегах',
|
||||||
|
date: 'Ноябрь 2024'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
allImages: [
|
||||||
|
// Первые 6 - это слайды, остальные - дополнительные фото
|
||||||
|
{ src: 'slider/slider1.jpg', alt: 'Тренировка в парке', date: 'Декабрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider2.jpg', alt: 'Техника бега', date: 'Декабрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider4.jpg', alt: 'Групповая тренировка', date: 'Ноябрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider5.jpg', alt: 'Соревнования', date: 'Ноябрь 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider6.jpg', alt: 'Награждение', date: 'Ноябрь 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider7.jpg', alt: 'Командный дух', date: 'Октябрь 2024', category: 'community' },
|
||||||
|
{ src: 'slider/workout1.jpg', alt: 'Тренировка на набережной', date: 'Октябрь 2025', category: 'training' },
|
||||||
|
{ src: 'slider/workout2.jpg', alt: 'Тренировка на набережной', date: 'Октябрь 2025', category: 'training' },
|
||||||
|
{ src: 'slider/workout3.jpg', alt: 'Тренировка на набережной', date: 'Октябрь 2025', category: 'training' },
|
||||||
|
{ src: 'slider/slider8.jpg', alt: 'Вечерняя пробежка', date: 'Октябрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider9.jpg', alt: 'Растяжка после бега', date: 'Октябрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider10.jpg', alt: 'Общение после тренировки', date: 'Сентябрь 2024', category: 'community' },
|
||||||
|
{ src: 'slider/slider11.jpg', alt: 'Индивидуальные занятия', date: 'Сентябрь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider12.jpg', alt: 'Подготовка к старту', date: 'Сентябрь 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider13.jpg', alt: 'Победа!', date: 'Август 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider14.jpg', alt: 'Трейл раннинг', date: 'Август 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider15.jpg', alt: 'Горные маршруты', date: 'Июль 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider16.jpg', alt: 'Летние тренировки', date: 'Июль 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider17.jpg', alt: 'Восстановление', date: 'Июнь 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider18.jpg', alt: 'Мастер-класс', date: 'Июнь 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider19.jpg', alt: 'Новички клуба', date: 'Май 2024', category: 'community' },
|
||||||
|
{ src: 'slider/slider20.jpg', alt: 'Весенний забег', date: 'Май 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider21.jpg', alt: 'Работа в группе', date: 'Апрель 2024', category: 'training' },
|
||||||
|
{ src: 'slider/slider22.jpg', alt: 'Совместный отдых', date: 'Апрель 2024', category: 'community' },
|
||||||
|
{ src: 'slider/slider23.png', alt: 'Уфимский марафон', date: 'Март 2024', category: 'events' },
|
||||||
|
{ src: 'slider/slider24.jpg', alt: 'РосХим Стерлитамак', date: 'Январь 2025', category: 'events' }
|
||||||
|
],
|
||||||
|
filters: [
|
||||||
|
{ value: 'all', label: 'Все фото' },
|
||||||
|
{ value: 'training', label: 'Тренировки' },
|
||||||
|
{ value: 'events', label: 'Соревнования' },
|
||||||
|
{ value: 'community', label: 'Мероприятия' }
|
||||||
|
],
|
||||||
|
slideInterval: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredImages() {
|
||||||
|
let filtered = this.allImages
|
||||||
|
|
||||||
|
if (this.activeFilter !== 'all') {
|
||||||
|
filtered = filtered.filter(image => image.category === this.activeFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered.slice(0, this.visibleImages)
|
||||||
|
},
|
||||||
|
currentLightboxImage() {
|
||||||
|
return this.allImages[this.currentLightboxIndex] || {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getImageUrl(path) {
|
||||||
|
// В продакшене замените на правильный путь
|
||||||
|
const baseUrl = import.meta.env.BASE_URL
|
||||||
|
// Путь от корня public/
|
||||||
|
console.log(`${baseUrl}images/${path}`)
|
||||||
|
return `${baseUrl}images/${path}`
|
||||||
|
},
|
||||||
|
nextSlide() {
|
||||||
|
this.currentSlide = (this.currentSlide + 1) % this.slides.length
|
||||||
|
},
|
||||||
|
prevSlide() {
|
||||||
|
this.currentSlide = (this.currentSlide - 1 + this.slides.length) % this.slides.length
|
||||||
|
},
|
||||||
|
goToSlide(index) {
|
||||||
|
this.currentSlide = index
|
||||||
|
},
|
||||||
|
openLightbox(index) {
|
||||||
|
this.currentLightboxIndex = index
|
||||||
|
this.lightboxActive = true
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
},
|
||||||
|
closeLightbox() {
|
||||||
|
this.lightboxActive = false
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
},
|
||||||
|
nextLightbox() {
|
||||||
|
this.currentLightboxIndex = (this.currentLightboxIndex + 1) % this.allImages.length
|
||||||
|
},
|
||||||
|
prevLightbox() {
|
||||||
|
this.currentLightboxIndex = (this.currentLightboxIndex - 1 + this.allImages.length) % this.allImages.length
|
||||||
|
},
|
||||||
|
setFilter(filter) {
|
||||||
|
this.activeFilter = filter
|
||||||
|
this.visibleImages = 12
|
||||||
|
},
|
||||||
|
loadMore() {
|
||||||
|
this.visibleImages += 6
|
||||||
|
},
|
||||||
|
startAutoSlide() {
|
||||||
|
this.slideInterval = setInterval(() => {
|
||||||
|
this.nextSlide()
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
|
stopAutoSlide() {
|
||||||
|
if (this.slideInterval) {
|
||||||
|
clearInterval(this.slideInterval)
|
||||||
|
this.slideInterval = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageLoaded(index) {
|
||||||
|
console.log(`Image ${index} loaded successfully`)
|
||||||
|
},
|
||||||
|
handleKeydown(event) {
|
||||||
|
if (!this.lightboxActive) return
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
this.closeLightbox()
|
||||||
|
break
|
||||||
|
case 'ArrowLeft':
|
||||||
|
this.prevLightbox()
|
||||||
|
break
|
||||||
|
case 'ArrowRight':
|
||||||
|
this.nextLightbox()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.startAutoSlide()
|
||||||
|
document.addEventListener('keydown', this.handleKeydown)
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.stopAutoSlide()
|
||||||
|
document.removeEventListener('keydown', this.handleKeydown)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.gallery-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f8fff8 0%, #f0f8f0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Герой-секция */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 80px 0 60px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-subtitle {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffd700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Основные стили */
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Слайдер */
|
||||||
|
.slider-section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
position: relative;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
display: flex;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
flex: 0 0 100%;
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-overlay {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-content h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-content p {
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-date {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Навигация слайдера */
|
||||||
|
.slider-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: none;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-nav:hover {
|
||||||
|
background: white;
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-nav.prev {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-nav.next {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Индикаторы */
|
||||||
|
.slider-indicators {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator.active {
|
||||||
|
background: white;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-counter {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Галерея */
|
||||||
|
.gallery-section {
|
||||||
|
padding: 4rem 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-filters {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
background: white;
|
||||||
|
border-radius: 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover {
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-image {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(transparent, rgba(0, 0, 0, 0.7));
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item:hover .gallery-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-date {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Лайтбокс */
|
||||||
|
.lightbox {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1001;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1002;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1002;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
transform: translateY(-50%) scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav.prev {
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav.next {
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-image-container {
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-image {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-info {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 0 0 10px 10px;
|
||||||
|
margin-top: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-info h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-info p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-date {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA секция */
|
||||||
|
.cta-section {
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 4rem 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content h2 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-features {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопки */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #e6c200;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: #2e8b57;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-title {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-nav {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-nav {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-content {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.hero-section {
|
||||||
|
padding: 60px 0 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-filters {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-features {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Анимации */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gallery-item {
|
||||||
|
animation: fadeIn 0.6s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,749 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-page">
|
||||||
|
<!-- Герой-секция -->
|
||||||
|
<section class="hero-section">
|
||||||
|
<div class="hero-overlay">
|
||||||
|
<div class="container">
|
||||||
|
<div class="hero-content">
|
||||||
|
<h1 class="hero-title">
|
||||||
|
<span class="title-main">Стань лучшей версией себя</span>
|
||||||
|
<span class="title-sub">в беговом клубе "Бегущий Башкир"</span>
|
||||||
|
</h1>
|
||||||
|
<p class="hero-description">
|
||||||
|
Присоединяйся к самому дружному беговому сообществу Уфы.
|
||||||
|
Начни свой путь к здоровью, новым достижениям и знакомствам с единомышленниками.
|
||||||
|
</p>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<router-link to="/register" class="btn btn-primary btn-large">
|
||||||
|
🏃 Начать бегать
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/about" class="btn btn-outline btn-large">
|
||||||
|
👥 Узнать о клубе
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">80+</div>
|
||||||
|
<div class="stat-label">Участников</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">25+</div>
|
||||||
|
<div class="stat-label">Мероприятий в год</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-number">100%</div>
|
||||||
|
<div class="stat-label">Поддержка</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Преимущества -->
|
||||||
|
<section class="benefits-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Почему выбирают нас?</h2>
|
||||||
|
<div class="benefits-grid">
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-icon">👨🏫</div>
|
||||||
|
<h3>Профессиональный тренер</h3>
|
||||||
|
<p>Мастер спорта с индивидуальным подходом к каждому участнику</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-icon">🌳</div>
|
||||||
|
<h3>Тренировки на природе</h3>
|
||||||
|
<p>Занятия в лучших парках Уфы с чистым воздухом</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-icon">🤝</div>
|
||||||
|
<h3>Дружное сообщество</h3>
|
||||||
|
<p>Поддержка и мотивация от таких же любителей бега</p>
|
||||||
|
</div>
|
||||||
|
<div class="benefit-card">
|
||||||
|
<div class="benefit-icon">📈</div>
|
||||||
|
<h3>Личный прогресс</h3>
|
||||||
|
<p>От первых километров до марафонских дистанций</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Ближайшие события -->
|
||||||
|
<section class="events-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Ближайшие события</h2>
|
||||||
|
<div class="events-grid">
|
||||||
|
<div class="event-card">
|
||||||
|
<div class="event-date">
|
||||||
|
<span class="date-day">Пн</span>
|
||||||
|
<span class="date-month">Янв</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-info">
|
||||||
|
<h3>Открытая тренировка для новичков</h3>
|
||||||
|
<p>🏞️ Набережная (ост. Монумент) • 19:30</p>
|
||||||
|
<span class="event-tag free">Бесплатно</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="event-card">
|
||||||
|
<div class="event-date">
|
||||||
|
<span class="date-day">Ср</span>
|
||||||
|
<span class="date-month">Янв</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-info">
|
||||||
|
<h3>Техника бега + ОФП</h3>
|
||||||
|
<p>🏟️ Стадион Динамо • 19:30</p>
|
||||||
|
<span class="event-tag regular">Для всех уровней</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="event-card">
|
||||||
|
<div class="event-date">
|
||||||
|
<span class="date-day">Суб</span>
|
||||||
|
<span class="date-month">Янв</span>
|
||||||
|
</div>
|
||||||
|
<div class="event-info">
|
||||||
|
<h3>Воскресный длительный кросс</h3>
|
||||||
|
<p>🌲 Лесопарковая зона • 10:00</p>
|
||||||
|
<span class="event-tag long">5 - 10 км</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="events-actions">
|
||||||
|
<router-link to="/training" class="btn btn-secondary">
|
||||||
|
📅 Все тренировки
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- История успеха -->
|
||||||
|
<section class="success-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Истории успеха</h2>
|
||||||
|
<div class="success-stories">
|
||||||
|
<div class="story-card">
|
||||||
|
<div class="story-avatar">С</div>
|
||||||
|
<div class="story-content">
|
||||||
|
<h3>Сергей</h3>
|
||||||
|
<p class="story-achievement">Первый марафон за 3:27</p>
|
||||||
|
<p class="story-text">"Пришел в клуб с нуля, через год пробежал свой первый марафон. Спасибо тренеру и
|
||||||
|
команде за поддержку!"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="story-card">
|
||||||
|
<div class="story-avatar">Д</div>
|
||||||
|
<div class="story-content">
|
||||||
|
<h3>Данил</h3>
|
||||||
|
<p class="story-achievement">Ультрамарафон 120 км</p>
|
||||||
|
<p class="story-text">"Никогда не думал, что смогу пробежать 120 км. В клубе нашел не только тренера, но и
|
||||||
|
верных друзей."</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="story-card">
|
||||||
|
<div class="story-avatar">А</div>
|
||||||
|
<div class="story-content">
|
||||||
|
<h3>Анна</h3>
|
||||||
|
<p class="story-achievement">Похудение на 15 кг</p>
|
||||||
|
<p class="story-text">"Бег изменил мою жизнь! Сбросила вес, нашла новых друзей и полюбила активный образ
|
||||||
|
жизни."</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Призыв к действию -->
|
||||||
|
<section class="cta-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Готовы изменить свою жизнь?</h2>
|
||||||
|
<p>Присоединяйся к 80+ участникам, которые уже начали свой беговой путь</p>
|
||||||
|
<div class="cta-features">
|
||||||
|
<div class="cta-feature">✅ Первая пробная тренировка бесплатно</div>
|
||||||
|
<div class="cta-feature">✅ Подбор программы под ваш уровень</div>
|
||||||
|
<div class="cta-feature">✅ Поддержка тренера и сообщества</div>
|
||||||
|
</div>
|
||||||
|
<div class="cta-actions">
|
||||||
|
<router-link to="/register" class="btn btn-primary btn-cta">
|
||||||
|
🏃 Начать бесплатно
|
||||||
|
</router-link>
|
||||||
|
<div class="cta-contacts">
|
||||||
|
<p>Или напиши нам:</p>
|
||||||
|
<div class="contact-links">
|
||||||
|
<a href="https://t.me/zagir_aminev" class="contact-link">📱 Telegram</a>
|
||||||
|
<a href="tel:+79273093095" class="contact-link" aria-label="Позвонить по номеру +7 (927) 30-93-095"
|
||||||
|
tabindex="0">
|
||||||
|
📞 Связаться
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Быстрые ссылки -->
|
||||||
|
<section class="quick-links-section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Исследуйте клуб</h2>
|
||||||
|
<div class="quick-links-grid">
|
||||||
|
<router-link to="/achievements" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">🏆</div>
|
||||||
|
<h3>Наши достижения</h3>
|
||||||
|
<p>Узнайте о рекордах и победах участников клуба</p>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/gallery" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">📸</div>
|
||||||
|
<h3>Фотогалерея</h3>
|
||||||
|
<p>Яркие моменты тренировок и соревнований</p>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/members" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">👥</div>
|
||||||
|
<h3>Участники</h3>
|
||||||
|
<p>Познакомьтесь с нашей беговой семьей</p>
|
||||||
|
</router-link>
|
||||||
|
<router-link to="/training" class="quick-link-card">
|
||||||
|
<div class="quick-link-icon">📅</div>
|
||||||
|
<h3>Тренировки</h3>
|
||||||
|
<p>Расписание и программы занятий</p>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Home',
|
||||||
|
methods: {
|
||||||
|
getImageUrl(path) {
|
||||||
|
// В продакшене замените на правильный путь
|
||||||
|
const baseUrl = import.meta.env.BASE_URL
|
||||||
|
// Путь от корня public/
|
||||||
|
console.log(`${baseUrl}images/${path}`)
|
||||||
|
return `${baseUrl}images/${path}`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-page {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Герой-секция */
|
||||||
|
.hero-section {
|
||||||
|
background: linear-gradient(135deg, rgba(46, 139, 86, 0.555) 0%, rgba(38, 115, 74, 0.477) 100%),
|
||||||
|
url('@/public/images/Roshim2025_3.png') center/cover no-repeat;
|
||||||
|
color: white;
|
||||||
|
padding: 120px 0 80px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-overlay {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-main {
|
||||||
|
display: block;
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-sub {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
max-width: 600px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
padding: 18px 35px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
border-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #5a6268;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 3rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffd700;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Секции */
|
||||||
|
.section-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Преимущества */
|
||||||
|
.benefits-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
background: #f8fff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card {
|
||||||
|
background: white;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefit-card p {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* События */
|
||||||
|
.events-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
display: flex;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-date {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-day {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-month {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info {
|
||||||
|
padding: 1.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-info p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tag.free {
|
||||||
|
background: #e8f5e8;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tag.regular {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-tag.long {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Истории успеха */
|
||||||
|
.success-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
background: #f8fff8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-stories {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-avatar {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-content h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-achievement {
|
||||||
|
color: #ffd700;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-text {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA секция */
|
||||||
|
.cta-section {
|
||||||
|
padding: 100px 0;
|
||||||
|
background: linear-gradient(135deg, #2e8b57 0%, #26734a 100%);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content>p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-features {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-feature {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cta {
|
||||||
|
padding: 20px 40px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-contacts p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-link {
|
||||||
|
color: #ffd700;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-link:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Быстрые ссылки */
|
||||||
|
.quick-links-section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-links-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link-card {
|
||||||
|
background: white;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: #2e8b57;
|
||||||
|
box-shadow: 0 10px 30px rgba(46, 139, 87, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link-icon {
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link-card h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-link-card p {
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-section {
|
||||||
|
padding: 80px 0 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-main {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-sub {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-large {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-stats {
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-number {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-date {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.story-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-links {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.title-main {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits-grid,
|
||||||
|
.events-grid,
|
||||||
|
.success-stories,
|
||||||
|
.quick-links-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<h1>🔐 Вход в систему</h1>
|
||||||
|
<p>Войдите в свой личный кабинет</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleLogin" class="login-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="email" placeholder="Email" class="form-input" v-model="credentials.email" required
|
||||||
|
:disabled="loading">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="password" placeholder="Пароль" class="form-input" v-model="credentials.password" required
|
||||||
|
:disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="loading">
|
||||||
|
{{ loading ? 'Вход...' : 'Войти' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="login-links">
|
||||||
|
<div class="register-link">
|
||||||
|
<p>Нет аккаунта? <router-link to="/register" class="link">Зарегистрируйтесь здесь</router-link></p>
|
||||||
|
</div>
|
||||||
|
<p><a href="#" class="link">Забыли пароль?</a></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Login',
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return { authStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading() {
|
||||||
|
return this.authStore.loading
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
return this.authStore.error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleLogin() {
|
||||||
|
const result = await this.authStore.login(this.credentials)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Показываем уведомление об успешном входе
|
||||||
|
this.showSuccessNotification()
|
||||||
|
|
||||||
|
// Редиректим после небольшой задержки
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$router.push('/profile')
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showSuccessNotification() {
|
||||||
|
const notification = document.createElement('div')
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
`
|
||||||
|
notification.textContent = '✅ Вход выполнен успешно!'
|
||||||
|
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Добавляем проверку при монтировании компонента
|
||||||
|
mounted() {
|
||||||
|
// Если пользователь уже авторизован, показываем уведомление
|
||||||
|
if (this.authStore.isAuthenticated) {
|
||||||
|
this.showAlreadyLoggedInNotification()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showAlreadyLoggedInNotification() {
|
||||||
|
const notification = document.createElement('div')
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #ffd700;
|
||||||
|
color: #333;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||||
|
z-index: 10000;
|
||||||
|
max-width: 300px;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
`
|
||||||
|
notification.textContent = 'ℹ️ Вы уже авторизованы!'
|
||||||
|
|
||||||
|
document.body.appendChild(notification)
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification)
|
||||||
|
}
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-form {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: #26734a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-links {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #2e8b57;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<h1>🚪 Выход из системы</h1>
|
||||||
|
<div class="logout-content">
|
||||||
|
<p>Выполняется выход из системы...</p>
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Logout',
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return { authStore }
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
// Выполняем выход
|
||||||
|
await this.authStore.logout()
|
||||||
|
|
||||||
|
// Редиректим на главную страницу
|
||||||
|
this.$router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.logout-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #2e8b57;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="document-container">
|
||||||
|
<div class="document-header">
|
||||||
|
<h1>🔒 Политика конфиденциальности</h1>
|
||||||
|
<div class="document-meta">
|
||||||
|
<p>Дата последнего обновления: {{ lastUpdated }}</p>
|
||||||
|
<button class="btn btn-secondary" @click="downloadPDF">
|
||||||
|
📥 Скачать PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-content">
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>1. Общие положения</h2>
|
||||||
|
<p>1.1. Настоящая Политика конфиденциальности регулирует порядок сбора, хранения и использования персональных данных пользователей бегового клуба "Бегущий Башкир".</p>
|
||||||
|
<p>1.2. Используя наш сайт и услуги, вы соглашаетесь с условиями настоящей Политики.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>2. Собираемая информация</h2>
|
||||||
|
<p>2.1. Мы собираем следующую информацию:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Персональные данные:</strong> имя, фамилия, email, телефон</li>
|
||||||
|
<li><strong>Данные для тренировок:</strong> уровень подготовки, цели, спортивные результаты</li>
|
||||||
|
<li><strong>Технические данные:</strong> IP-адрес, данные cookies, информация о браузере</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>3. Цели использования данных</h2>
|
||||||
|
<p>3.1. Собранные данные используются для:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Регистрации и идентификации пользователей</li>
|
||||||
|
<li>Предоставления персонализированных тренировочных программ</li>
|
||||||
|
<li>Организации мероприятий и забегов</li>
|
||||||
|
<li>Отправки информационных материалов (при согласии)</li>
|
||||||
|
<li>Улучшения качества наших услуг</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>4. Защита данных</h2>
|
||||||
|
<p>4.1. Мы принимаем все необходимые меры для защиты ваших персональных данных от несанкционированного доступа.</p>
|
||||||
|
<p>4.2. Данные хранятся на защищенных серверах и передаются в зашифрованном виде.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>5. Передача данных третьим лицам</h2>
|
||||||
|
<p>5.1. Мы не передаем ваши персональные данные третьим лицам, за исключением:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Случаев, предусмотренных законодательством РФ</li>
|
||||||
|
<li>Партнеров по организации мероприятий (только с вашего согласия)</li>
|
||||||
|
<li>Сервисных провайдеров, обеспечивающих работу нашего сайта</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>6. Cookies и аналитика</h2>
|
||||||
|
<p>6.1. Мы используем cookies для улучшения работы сайта и сбора аналитической информации.</p>
|
||||||
|
<p>6.2. Вы можете отключить cookies в настройках браузера.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>7. Ваши права</h2>
|
||||||
|
<p>7.1. Вы имеете право:</p>
|
||||||
|
<ul>
|
||||||
|
<li>На доступ к вашим персональным данным</li>
|
||||||
|
<li>На исправление неточных данных</li>
|
||||||
|
<li>На удаление ваших данных</li>
|
||||||
|
<li>На отзыв согласия на обработку данных</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>8. Контакты</h2>
|
||||||
|
<p>По вопросам, связанным с обработкой персональных данных, обращайтесь:</p>
|
||||||
|
<p>📧 Email: privacy@begushiybashkir.ru<br>
|
||||||
|
📞 Телефон: +7 (XXX) XXX-XX-XX</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-actions">
|
||||||
|
<button class="btn btn-primary" @click="$router.back()">
|
||||||
|
← Назад к регистрации
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" @click="downloadPDF">
|
||||||
|
📥 Скачать политику
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'PrivacyPolicy',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdated: '10 октября 2024 года'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
downloadPDF() {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = '/documents/privacy-policy.pdf'
|
||||||
|
link.download = 'politika-konfidencialnosti.pdf'
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Стили такие же как в TermsOfService.vue */
|
||||||
|
.document-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-header {
|
||||||
|
background: linear-gradient(135deg, #2e8b57, #3da56a);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-header h1 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-content {
|
||||||
|
padding: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section h2 {
|
||||||
|
color: #2e8b57;
|
||||||
|
border-bottom: 2px solid #e8f5e8;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-actions {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: #f8fff8;
|
||||||
|
border-top: 1px solid #e8f5e8;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.document-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,618 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<h1>👤 Личный кабинет</h1>
|
||||||
|
|
||||||
|
<div v-if="authLoading" class="loading">Загрузка профиля...</div>
|
||||||
|
|
||||||
|
<div v-else-if="user" class="profile-content">
|
||||||
|
<div class="profile-header">
|
||||||
|
<!-- Обновленная секция аватара -->
|
||||||
|
<div class="avatar-section">
|
||||||
|
<div class="avatar-preview">
|
||||||
|
<img v-if="user.avatar" :src="avatarUrl" :alt="`Аватар ${user.firstName} ${user.lastName}`"
|
||||||
|
class="avatar-image" @error="handleAvatarError">
|
||||||
|
<div v-else class="avatar-placeholder">
|
||||||
|
👤
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AvatarUpload :user="user" @avatar-updated="onAvatarUpdated" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>{{ user.firstName }} {{ user.lastName }}</h2>
|
||||||
|
<p>Участник с {{ joinDate }}</p>
|
||||||
|
<p class="user-email">{{ user.email }}</p>
|
||||||
|
<p v-if="user.phone" class="user-phone">📱 {{ user.phone }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Остальной код остается без изменений -->
|
||||||
|
<div class="profile-info">
|
||||||
|
<h3>📋 Информация о пользователе</h3>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Уровень подготовки:</label>
|
||||||
|
<span class="info-value">{{ experienceLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Цели:</label>
|
||||||
|
<span class="info-value">{{ goalsLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Рассылка:</label>
|
||||||
|
<span class="info-value">{{ user.newsletter ? '✅ Подключена' : '❌ Отключена' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>Роль:</label>
|
||||||
|
<span class="info-value role-badge">{{ user.role }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-stats">
|
||||||
|
<div class="stats-header">
|
||||||
|
<h3>📊 Моя статистика</h3>
|
||||||
|
<button class="btn-refresh" @click="refreshStats" :disabled="statsLoading">
|
||||||
|
{{ statsLoading ? '⟳' : '🔄' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="statsError" class="error-message">
|
||||||
|
{{ statsError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>🏃 Всего пробег</h4>
|
||||||
|
<p>{{ userStats?.totalDistance || 0 }} км</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>⭐ Лучший результат</h4>
|
||||||
|
<p>{{ userStats?.bestResult || 'Нет данных' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>📅 Тренировок</h4>
|
||||||
|
<p>{{ userStats?.totalWorkouts || 0 }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>🔥 Сожжено калорий</h4>
|
||||||
|
<p>{{ userStats?.caloriesBurned || 0 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="achievements-preview">
|
||||||
|
<h3>🏆 Достижения</h3>
|
||||||
|
<div class="achievements-progress">
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: achievementProgress + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<span>{{ achievementProgress }}% выполнено</span>
|
||||||
|
</div>
|
||||||
|
<div class="achievements-count">
|
||||||
|
<span>Получено: {{ completedAchievements.length }} из {{ userAchievements.length }}</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline" @click="$router.push('/achievements')">
|
||||||
|
📜 Все достижения
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-actions">
|
||||||
|
<button class="btn" @click="editProfile">✏️ Редактировать профиль</button>
|
||||||
|
<button class="btn" @click="viewDetailedStats">📊 Подробная статистика</button>
|
||||||
|
<button class="btn" @click="$router.push('/training')">📅 Мой план тренировок</button>
|
||||||
|
<button class="btn btn-logout" @click="handleLogout" :disabled="authLoading">
|
||||||
|
{{ authLoading ? 'Выход...' : '🚪 Выйти' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="error-message">
|
||||||
|
Не удалось загрузить данные профиля.
|
||||||
|
<router-link to="/login" class="link">Войдите</router-link> снова.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import { useUserStore } from '../stores/user'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Profile',
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
return { authStore, userStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
authLoading: false,
|
||||||
|
statsLoading: false,
|
||||||
|
avatarLoadError: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user() {
|
||||||
|
return this.authStore.user
|
||||||
|
},
|
||||||
|
userStats() {
|
||||||
|
return this.userStore.userStats
|
||||||
|
},
|
||||||
|
userAchievements() {
|
||||||
|
return this.userStore.userAchievements
|
||||||
|
},
|
||||||
|
completedAchievements() {
|
||||||
|
return this.userStore.completedAchievements
|
||||||
|
},
|
||||||
|
achievementProgress() {
|
||||||
|
return this.userStore.achievementProgress
|
||||||
|
},
|
||||||
|
statsError() {
|
||||||
|
return this.userStore.error
|
||||||
|
},
|
||||||
|
// Вычисляем полный URL аватара
|
||||||
|
avatarUrl() {
|
||||||
|
if (!this.user?.avatar) return null;
|
||||||
|
|
||||||
|
let filename = this.user.avatar.trim('/').split('/').pop();
|
||||||
|
|
||||||
|
// Иначе формируем полный URL
|
||||||
|
const baseUrl = 'https://begushiybashkir.ru/api/v1/user/avatars/';
|
||||||
|
|
||||||
|
return baseUrl + filename;
|
||||||
|
},
|
||||||
|
joinDate() {
|
||||||
|
if (!this.user?.createdAt) return 'января 2024';
|
||||||
|
|
||||||
|
const date = new Date(this.user.createdAt);
|
||||||
|
const month = date.toLocaleString('ru-RU', { month: 'long' });
|
||||||
|
const year = date.getFullYear();
|
||||||
|
return `${month} ${year}`;
|
||||||
|
},
|
||||||
|
experienceLabel() {
|
||||||
|
const experienceMap = {
|
||||||
|
'beginner': 'Начинающий (0-6 месяцев)',
|
||||||
|
'intermediate': 'Любитель (6-24 месяцев)',
|
||||||
|
'advanced': 'Опытный (2+ лет)',
|
||||||
|
'professional': 'Профессионал'
|
||||||
|
};
|
||||||
|
return experienceMap[this.user?.experience] || 'Не указан';
|
||||||
|
},
|
||||||
|
goalsLabel() {
|
||||||
|
const goalsMap = {
|
||||||
|
'health': 'Улучшить здоровье',
|
||||||
|
'weight': 'Сбросить вес',
|
||||||
|
'first5k': 'Пробежать первые 5 км',
|
||||||
|
'first10k': 'Пробежать первые 10 км',
|
||||||
|
'halfMarathon': 'Подготовиться к полумарафону',
|
||||||
|
'marathon': 'Подготовиться к марафону',
|
||||||
|
'improve': 'Улучшить результаты',
|
||||||
|
'social': 'Общение и компания'
|
||||||
|
};
|
||||||
|
return goalsMap[this.user?.goals] || 'Не указана';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onAvatarUpdated() {
|
||||||
|
// Сбрасываем флаг ошибки при обновлении аватара
|
||||||
|
this.avatarLoadError = false;
|
||||||
|
// Принудительно обновляем профиль
|
||||||
|
await this.authStore.fetchProfile();
|
||||||
|
console.log('Avatar updated, user data:', this.authStore.user);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Обработчик ошибки загрузки изображения
|
||||||
|
handleAvatarError() {
|
||||||
|
console.error('Ошибка загрузки аватара:', this.avatarUrl);
|
||||||
|
this.avatarLoadError = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadUserData() {
|
||||||
|
this.authLoading = true;
|
||||||
|
this.avatarLoadError = false;
|
||||||
|
try {
|
||||||
|
await this.authStore.fetchProfile();
|
||||||
|
await this.loadStats();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки данных:', error);
|
||||||
|
} finally {
|
||||||
|
this.authLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadStats() {
|
||||||
|
this.statsLoading = true;
|
||||||
|
try {
|
||||||
|
const [statsResult, achievementsResult] = await Promise.all([
|
||||||
|
this.userStore.fetchUserStats(),
|
||||||
|
this.userStore.fetchUserAchievements()
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!statsResult.success) {
|
||||||
|
console.error('Ошибка загрузки статистики:', statsResult.error);
|
||||||
|
}
|
||||||
|
if (!achievementsResult.success) {
|
||||||
|
console.error('Ошибка загрузки достижений:', achievementsResult.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка загрузки статистики:', error);
|
||||||
|
} finally {
|
||||||
|
this.statsLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshStats() {
|
||||||
|
await this.loadStats();
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLogout() {
|
||||||
|
await this.authStore.logout();
|
||||||
|
this.$router.push('/');
|
||||||
|
},
|
||||||
|
|
||||||
|
editProfile() {
|
||||||
|
this.$router.push('/profile/edit');
|
||||||
|
},
|
||||||
|
|
||||||
|
viewDetailedStats() {
|
||||||
|
// TODO: Переход на страницу детальной статистики
|
||||||
|
alert('Функция в разработке');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
if (!this.user) {
|
||||||
|
await this.loadUserData();
|
||||||
|
} else {
|
||||||
|
await this.loadStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-section {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
margin: 0 auto 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 4px solid #2e8b57;
|
||||||
|
background: linear-gradient(135deg, #f5f5f5, #e0e0e0);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-image:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 4rem;
|
||||||
|
background: linear-gradient(135deg, #2e8b57, #3cb371);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header h2 {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-email,
|
||||||
|
.user-phone {
|
||||||
|
color: #666;
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info,
|
||||||
|
.profile-stats,
|
||||||
|
.achievements-preview {
|
||||||
|
background: white;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh:hover:not(:disabled) {
|
||||||
|
background-color: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-refresh:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
|
||||||
|
padding: 1.5rem 1rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h4 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-preview {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 300px;
|
||||||
|
height: 12px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #2e8b57, #3cb371);
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-count {
|
||||||
|
margin: 1rem 0;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover:not(:disabled) {
|
||||||
|
background: #26734d;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(46, 139, 87, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: white;
|
||||||
|
color: #2e8b57;
|
||||||
|
border: 2px solid #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout {
|
||||||
|
background-color: #dc3545;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-logout:hover:not(:disabled) {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #2e8b57;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-actions {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.achievements-progress {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-preview {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<h1>✏️ Редактирование профиля</h1>
|
||||||
|
|
||||||
|
<div class="avatar-section">
|
||||||
|
<h3>Фотография профиля</h3>
|
||||||
|
<AvatarUpload :user="user" :show-actions="true" @avatar-updated="onAvatarUpdated" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && !user" class="loading">Загрузка...</div>
|
||||||
|
|
||||||
|
<form v-else @submit.prevent="handleSubmit" class="profile-edit-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="firstName">Имя *</label>
|
||||||
|
<input id="firstName" v-model="formData.firstName" type="text" class="form-input"
|
||||||
|
placeholder="Введите ваше имя" required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lastName">Фамилия *</label>
|
||||||
|
<input id="lastName" v-model="formData.lastName" type="text" class="form-input"
|
||||||
|
placeholder="Введите вашу фамилию" required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email *</label>
|
||||||
|
<input id="email" v-model="formData.email" type="email" class="form-input" placeholder="example@mail.ru"
|
||||||
|
required readonly disabled style="background-color: #f5f5f5; cursor: not-allowed;">
|
||||||
|
<small style="color: #666; font-size: 0.9rem;">
|
||||||
|
Email нельзя изменить. Для смены email обратитесь в поддержку.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="experience">Уровень подготовки</label>
|
||||||
|
<select id="experience" v-model="formData.experience" class="form-input" :disabled="loading">
|
||||||
|
<option value="">Выберите уровень</option>
|
||||||
|
<option value="beginner">Начинающий (0-6 месяцев)</option>
|
||||||
|
<option value="intermediate">Любитель (6-24 месяцев)</option>
|
||||||
|
<option value="advanced">Опытный (2+ лет)</option>
|
||||||
|
<option value="professional">Профессионал</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goals">Цели</label>
|
||||||
|
<select id="goals" v-model="formData.goals" class="form-input" :disabled="loading">
|
||||||
|
<option value="">Выберите цель</option>
|
||||||
|
<option value="health">Улучшить здоровье</option>
|
||||||
|
<option value="weight">Сбросить вес</option>
|
||||||
|
<option value="first5k">Пробежать первые 5 км</option>
|
||||||
|
<option value="first10k">Пробежать первые 10 км</option>
|
||||||
|
<option value="halfMarathon">Подготовиться к полумарафону</option>
|
||||||
|
<option value="marathon">Подготовиться к марафону</option>
|
||||||
|
<option value="improve">Улучшить результаты</option>
|
||||||
|
<option value="social">Общение и компания</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="formData.newsletter" type="checkbox" class="checkbox" :disabled="loading">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Хочу получать новости о тренировках и мероприятиях
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="loading || !isFormChanged">
|
||||||
|
{{ loading ? 'Сохранение...' : '💾 Сохранить изменения' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary" @click="cancelEdit" :disabled="loading">
|
||||||
|
❌ Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="success" class="success-message">
|
||||||
|
✅ Профиль успешно обновлен!
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="navigation-actions">
|
||||||
|
<button class="btn btn-secondary" @click="$router.push('/profile')">← Назад к профилю</button>
|
||||||
|
<button class="btn btn-secondary" @click="$router.push('/')">🏠 На главную</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
import AvatarUpload from '../components/AvatarUpload.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ProfileEdit',
|
||||||
|
components: {
|
||||||
|
AvatarUpload
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return { authStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '', // Только для отображения, не для изменения
|
||||||
|
phone: '',
|
||||||
|
experience: '',
|
||||||
|
goals: '',
|
||||||
|
newsletter: false
|
||||||
|
},
|
||||||
|
originalData: {},
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user() {
|
||||||
|
return this.authStore.user
|
||||||
|
},
|
||||||
|
isFormChanged() {
|
||||||
|
// Исключаем email из сравнения, так как он не изменяется
|
||||||
|
const formDataCopy = { ...this.formData }
|
||||||
|
const originalDataCopy = { ...this.originalData }
|
||||||
|
delete formDataCopy.email
|
||||||
|
delete originalDataCopy.email
|
||||||
|
return JSON.stringify(formDataCopy) !== JSON.stringify(originalDataCopy)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async onAvatarUpdated() {
|
||||||
|
// Обновляем данные пользователя после загрузки аватара
|
||||||
|
await this.authStore.fetchProfile()
|
||||||
|
this.initializeForm()
|
||||||
|
},
|
||||||
|
initializeForm() {
|
||||||
|
if (this.user) {
|
||||||
|
this.formData = {
|
||||||
|
firstName: this.user.firstName || '',
|
||||||
|
lastName: this.user.lastName || '',
|
||||||
|
email: this.user.email || '', // Только для отображения
|
||||||
|
phone: this.user.phone || '',
|
||||||
|
experience: this.user.experience || '',
|
||||||
|
goals: this.user.goals || '',
|
||||||
|
newsletter: this.user.newsletter || false
|
||||||
|
}
|
||||||
|
this.originalData = { ...this.formData }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleSubmit() {
|
||||||
|
this.loading = true
|
||||||
|
this.error = ''
|
||||||
|
this.success = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Используем метод updateProfile из authStore
|
||||||
|
const result = await this.authStore.updateProfile({
|
||||||
|
firstName: this.formData.firstName,
|
||||||
|
lastName: this.formData.lastName,
|
||||||
|
phone: this.formData.phone,
|
||||||
|
experience: this.formData.experience,
|
||||||
|
goals: this.formData.goals,
|
||||||
|
newsletter: this.formData.newsletter
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
this.originalData = { ...this.formData }
|
||||||
|
this.success = true
|
||||||
|
|
||||||
|
// Принудительно обновляем профиль в сторе
|
||||||
|
await this.authStore.fetchProfile()
|
||||||
|
} else {
|
||||||
|
this.error = result.error
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
this.error = err.response?.data?.message || 'Ошибка обновления профиля'
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cancelEdit() {
|
||||||
|
this.initializeForm()
|
||||||
|
this.error = ''
|
||||||
|
this.success = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.user) {
|
||||||
|
this.initializeForm()
|
||||||
|
} else {
|
||||||
|
// Если пользователь не загружен, загружаем данные
|
||||||
|
this.loading = true
|
||||||
|
this.authStore.fetchProfile().finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
this.initializeForm()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-edit-form {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:checked+.checkmark {
|
||||||
|
background-color: #2e8b57;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:checked+.checkmark::after {
|
||||||
|
content: '✓';
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
flex: 1;
|
||||||
|
background-color: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: #26734a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-message {
|
||||||
|
background-color: #efe;
|
||||||
|
color: #2e8b57;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-edit-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="register-container">
|
||||||
|
<h1>👤 Регистрация</h1>
|
||||||
|
<p>Присоединяйтесь к нашему беговому сообществу</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleRegister" class="register-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="firstName">Имя *</label>
|
||||||
|
<input id="firstName" v-model="formData.firstName" type="text" class="form-input"
|
||||||
|
placeholder="Введите ваше имя" required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="lastName">Фамилия *</label>
|
||||||
|
<input id="lastName" v-model="formData.lastName" type="text" class="form-input"
|
||||||
|
placeholder="Введите вашу фамилию" required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Email *</label>
|
||||||
|
<input id="email" v-model="formData.email" type="email" class="form-input" placeholder="example@mail.ru"
|
||||||
|
required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="phone">Телефон</label>
|
||||||
|
<input id="phone" v-model="formData.phone" type="tel" class="form-input" placeholder="+7 (999) 123-45-67"
|
||||||
|
:disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">Пароль *</label>
|
||||||
|
<input id="password" v-model="formData.password" type="password" class="form-input"
|
||||||
|
placeholder="Не менее 6 символов" required minlength="6" :disabled="loading">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">Подтверждение пароля *</label>
|
||||||
|
<input id="confirmPassword" v-model="formData.confirmPassword" type="password" class="form-input"
|
||||||
|
placeholder="Повторите пароль" required :disabled="loading">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="experience">Уровень подготовки</label>
|
||||||
|
<select id="experience" v-model="formData.experience" class="form-input" :disabled="loading">
|
||||||
|
<option value="">Выберите уровень</option>
|
||||||
|
<option value="beginner">Начинающий (0-6 месяцев)</option>
|
||||||
|
<option value="intermediate">Любитель (6-24 месяцев)</option>
|
||||||
|
<option value="advanced">Опытный (2+ лет)</option>
|
||||||
|
<option value="professional">Профессионал</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="goals">Цели</label>
|
||||||
|
<select id="goals" v-model="formData.goals" class="form-input" :disabled="loading">
|
||||||
|
<option value="">Выберите цель</option>
|
||||||
|
<option value="health">Улучшить здоровье</option>
|
||||||
|
<option value="weight">Сбросить вес</option>
|
||||||
|
<option value="first5k">Пробежать первые 5 км</option>
|
||||||
|
<option value="first10k">Пробежать первые 10 км</option>
|
||||||
|
<option value="halfMarathon">Подготовиться к полумарафону</option>
|
||||||
|
<option value="marathon">Подготовиться к марафону</option>
|
||||||
|
<option value="improve">Улучшить результаты</option>
|
||||||
|
<option value="social">Общение и компания</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="formData.agreeTerms" type="checkbox" class="checkbox" required :disabled="loading">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Я соглашаюсь с
|
||||||
|
|
||||||
|
<router-link to="/terms" class="link" target="_blank">правилами клуба</router-link> и
|
||||||
|
<router-link to="/privacy" class="link" target="_blank">политикой конфиденциальности</router-link> *
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<input v-model="formData.newsletter" type="checkbox" class="checkbox" :disabled="loading">
|
||||||
|
<span class="checkmark"></span>
|
||||||
|
Хочу получать новости о тренировках и мероприятиях
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary" :disabled="!formData.agreeTerms || loading">
|
||||||
|
{{ loading ? 'Регистрация...' : '🏃 Зарегистрироваться' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div v-if="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="login-link">
|
||||||
|
<p>Уже есть аккаунт? <router-link to="/login" class="link">Войдите здесь</router-link></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="benefits">
|
||||||
|
<h3>Что вы получите после регистрации:</h3>
|
||||||
|
<ul class="benefits-list">
|
||||||
|
<li>✅ Доступ к расписанию тренировок</li>
|
||||||
|
<li>✅ Персональный трекер прогресса</li>
|
||||||
|
<li>✅ Общение с тренером и участниками</li>
|
||||||
|
<li>✅ Участие в клубных мероприятиях</li>
|
||||||
|
<li>✅ Скидки на стартовые взносы</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-secondary" @click="$router.push('/')">← На главную</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// eslint-disable-next-line vue/multi-word-component-names
|
||||||
|
name: 'Register',
|
||||||
|
setup() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
return { authStore }
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
experience: '',
|
||||||
|
goals: '',
|
||||||
|
agreeTerms: false,
|
||||||
|
newsletter: true
|
||||||
|
},
|
||||||
|
showDebugInfo: import.meta.env.DEV // Показывать отладочную информацию только в development
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
loading() {
|
||||||
|
return this.authStore.loading
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
return this.authStore.error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async handleRegister() {
|
||||||
|
// Валидация
|
||||||
|
if (this.formData.password !== this.formData.confirmPassword) {
|
||||||
|
this.authStore.error = 'Пароли не совпадают'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formData.password.length < 6) {
|
||||||
|
this.authStore.error = 'Пароль должен содержать не менее 6 символов'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.formData.agreeTerms) {
|
||||||
|
this.authStore.error = 'Необходимо согласие с правилами клуба'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Подготовка данных для API
|
||||||
|
const registerData = {
|
||||||
|
email: this.formData.email,
|
||||||
|
password: this.formData.password,
|
||||||
|
firstName: this.formData.firstName,
|
||||||
|
lastName: this.formData.lastName,
|
||||||
|
phone: this.formData.phone,
|
||||||
|
experience: this.formData.experience,
|
||||||
|
goals: this.formData.goals,
|
||||||
|
newsletter: this.formData.newsletter
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Отправка данных регистрации:', { ...registerData, password: '***' })
|
||||||
|
alert("|" + registerData.email + "|" + registerData.password + "|")
|
||||||
|
const result = await this.authStore.register(registerData)
|
||||||
|
alert("register seccess=" + result.success + "| data=" + result.data)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Перенаправляем на страницу профиля после успешной регистрации
|
||||||
|
this.$router.push('/profile')
|
||||||
|
} else {
|
||||||
|
console.error('Ошибка регистрации:', result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Стили остаются без изменений */
|
||||||
|
.register-container {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-form {
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e1e5e9;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkmark {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:checked+.checkmark {
|
||||||
|
background-color: #2e8b57;
|
||||||
|
border-color: #2e8b57;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox:checked+.checkmark::after {
|
||||||
|
content: '✓';
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #2e8b57;
|
||||||
|
color: white;
|
||||||
|
padding: 15px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover:not(:disabled) {
|
||||||
|
background-color: #26734a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-left: 4px solid #c33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-link {
|
||||||
|
text-align: center;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: #2e8b57;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits {
|
||||||
|
background-color: #f8fff8;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #2e8b57;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits h3 {
|
||||||
|
color: #2e8b57;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefits-list li {
|
||||||
|
padding: 0.3rem 0;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="document-container">
|
||||||
|
<div class="document-header">
|
||||||
|
<h1>📋 Правила бегового клуба "Бегущий Башкир"</h1>
|
||||||
|
<div class="document-meta">
|
||||||
|
<p>Дата последнего обновления: {{ lastUpdated }}</p>
|
||||||
|
<button class="btn btn-secondary" @click="downloadPDF">
|
||||||
|
📥 Скачать PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-content">
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>1. Общие положения</h2>
|
||||||
|
<p>1.1. Беговой клуб "Бегущий Башкир" (далее – "Клуб") – это сообщество любителей бега, созданное для популяризации здорового образа жизни и развития беговой культуры в Республике Башкортостан.</p>
|
||||||
|
<p>1.2. Участником Клуба может стать любой желающий, достигший 18 лет и согласившийся с настоящими Правилами.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>2. Членство в Клубе</h2>
|
||||||
|
<p>2.1. Для вступления в Клуб необходимо:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Заполнить регистрационную форму на сайте</li>
|
||||||
|
<li>Ознакомиться и принять настоящие Правила</li>
|
||||||
|
<li>Оплатить членский взнос (при наличии)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>2.2. Участники Клуба имеют право:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Участвовать в регулярных тренировках Клуба</li>
|
||||||
|
<li>Получать консультации тренеров</li>
|
||||||
|
<li>Участвовать в клубных мероприятиях и забегах</li>
|
||||||
|
<li>Получать скидки от партнеров Клуба</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>3. Обязанности участников</h2>
|
||||||
|
<p>3.1. Участники обязаны:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Соблюдать технику безопасности во время тренировок</li>
|
||||||
|
<li>Быть пунктуальными</li>
|
||||||
|
<li>Уважительно относиться к другим участникам и тренерам</li>
|
||||||
|
<li>Следовать указаниям тренера</li>
|
||||||
|
<li>Сообщать тренеру о проблемах со здоровьем</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>4. Тренировочный процесс</h2>
|
||||||
|
<p>4.1. Расписание тренировок публикуется на сайте Клуба и в официальных группах в социальных сетях.</p>
|
||||||
|
<p>4.2. Участники обязаны предупреждать тренера о невозможности посетить тренировку.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>5. Безопасность</h2>
|
||||||
|
<p>5.1. Участники несут ответственность за свое здоровье и безопасность во время тренировок.</p>
|
||||||
|
<p>5.2. Клуб не несет ответственности за травмы, полученные в результате несоблюдения техники безопасности.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="document-section">
|
||||||
|
<h2>6. Конфиденциальность</h2>
|
||||||
|
<p>6.1. Клуб обязуется не передавать персональные данные участников третьим лицам.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="document-actions">
|
||||||
|
<button class="btn btn-primary" @click="$router.back()">
|
||||||
|
← Назад к регистрации
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" @click="downloadPDF">
|
||||||
|
📥 Скачать правила
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TermsOfService',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastUpdated: '10 октября 2024 года'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
downloadPDF() {
|
||||||
|
// Временная реализация - можно заменить на реальный PDF
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = '/documents/terms-of-service.pdf'
|
||||||
|
link.download = 'pravila-begovogo-kluba.pdf'
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.document-container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-header {
|
||||||
|
background: linear-gradient(135deg, #2e8b57, #3da56a);
|
||||||
|
color: white;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-header h1 {
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-content {
|
||||||
|
padding: 2rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section h2 {
|
||||||
|
color: #2e8b57;
|
||||||
|
border-bottom: 2px solid #e8f5e8;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section ul {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-actions {
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
background: #f8fff8;
|
||||||
|
border-top: 1px solid #e8f5e8;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Адаптивность */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.document-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
vueDevTools(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./', import.meta.url))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3001,
|
||||||
|
host: true
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -33,7 +33,7 @@ services:
|
|||||||
- ../yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
|
- ../yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
|
||||||
- ../valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
|
- ../valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
|
||||||
- ../yalarba/easySite/easy-site/prod:/usr/share/nginx/easysite102/html
|
- ../yalarba/easySite/easy-site/prod:/usr/share/nginx/easysite102/html
|
||||||
- ../begushiybashkir/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
- ./begushiybashkir/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
||||||
networks:
|
networks:
|
||||||
- web-network
|
- web-network
|
||||||
- internal
|
- internal
|
||||||
|
|||||||