page redesigns and init calendar work
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,6 +6,7 @@ node_modules
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
|
||||
@@ -1,19 +1,259 @@
|
||||
<template>
|
||||
<div class="logged-in">
|
||||
<h2>You have successfully logged in!</h2>
|
||||
<p>Welcome back to RideAware!</p>
|
||||
<div class="logged-in-container">
|
||||
<div class="success-card">
|
||||
<div class="success-icon">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m9 12 2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Login Successful!</h1>
|
||||
<p class="welcome-message">Welcome back to RideAware</p>
|
||||
<p class="subtitle">You have successfully logged in. Get ready to train with focus and ride with awareness.</p>
|
||||
|
||||
<div class="action-buttons">
|
||||
<RouterLink to="/dashboard" class="btn-primary">
|
||||
Go to Dashboard
|
||||
</RouterLink>
|
||||
<RouterLink to="/profile" class="btn-secondary">
|
||||
Update Profile
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoggedInPage',
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logged-in {
|
||||
text-align: center;
|
||||
|
||||
<div class="background-decoration">
|
||||
<div class="decoration-circle circle-1"></div>
|
||||
<div class="decoration-circle circle-2"></div>
|
||||
<div class="decoration-circle circle-3"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoggedInPage',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logged-in-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.success-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
animation: slideUp 0.6s ease-out;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 2rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #34d399, #10b981);
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
animation: checkmark 0.8s ease-out 0.3s both;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.success-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #1f2937;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
animation: fadeInUp 0.6s ease-out 0.4s both;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #667eea;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
animation: fadeInUp 0.6s ease-out 0.5s both;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0 0 2rem 0;
|
||||
color: #6b7280;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
animation: fadeInUp 0.6s ease-out 0.6s both;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
animation: fadeInUp 0.6s ease-out 0.7s both;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
border-radius: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.decoration-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.circle-1 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 10%;
|
||||
left: -5%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.circle-2 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 60%;
|
||||
right: -10%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.circle-3 {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
bottom: 10%;
|
||||
left: 20%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
transform: translateY(30px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes checkmark {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.success-card {
|
||||
padding: 2rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-secondary {
|
||||
padding: 0.875rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
502
src/components/ModernNavbar.vue
Normal file
502
src/components/ModernNavbar.vue
Normal file
@@ -0,0 +1,502 @@
|
||||
<template>
|
||||
<nav class="modern-navbar">
|
||||
<div class="navbar-container">
|
||||
<RouterLink to="/dashboard" class="navbar-brand">
|
||||
<h1>RideAware</h1>
|
||||
<span class="tagline">Train Smart. Ride Aware.</span>
|
||||
</RouterLink>
|
||||
|
||||
<button class="mobile-menu-toggle" @click="toggleMobileMenu" :class="{ active: mobileMenuOpen }">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
|
||||
<div class="navbar-menu" :class="{ active: mobileMenuOpen }">
|
||||
<div class="nav-links">
|
||||
<RouterLink to="/dashboard" class="nav-link" @click="closeMobileMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Dashboard</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/profile" class="nav-link" @click="closeMobileMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Profile</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/equipment" class="nav-link" @click="closeMobileMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 1V3M12 21V23M4.22 4.22L5.64 5.64M18.36 18.36L19.78 19.78M1 12H3M21 12H23M4.22 19.78L5.64 18.36M18.36 5.64L19.78 4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span>Equipment</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/zones" class="nav-link" @click="closeMobileMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="19" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="5" cy="12" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>Zones</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/calendar" class="nav-link" @click="closeMobileMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<span>Calendar</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="nav-actions">
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="isDarkMode ? 'Light mode' : 'Dark mode'">
|
||||
<svg v-if="isDarkMode" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="21" x2="12" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="1" y1="12" x2="3" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="21" y1="12" x2="23" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="user-menu">
|
||||
<button class="user-avatar" @click="toggleUserMenu">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 14C14.21 14 16 12.21 16 10C16 7.79 14.21 6 12 6C9.79 6 8 7.79 8 10C8 12.21 9.79 14 12 14Z" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 14C10.5 15.5 8 16.5 8 18.5V20C8 20.5304 8.21071 21.0391 8.58579 21.4142C8.96086 21.7893 9.46957 22 10 22H14C14.5304 22 15.0391 21.7893 15.4142 21.4142C15.7893 21.0391 16 20.5304 16 20V18.5C16 16.5 13.5 15.5 12 14Z" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="user-dropdown" :class="{ active: userMenuOpen }">
|
||||
<div class="user-info">
|
||||
<p class="username">{{ auth.user?.username || 'User' }}</p>
|
||||
<small>{{ auth.user?.email || 'user@example.com' }}</small>
|
||||
</div>
|
||||
<hr>
|
||||
<RouterLink to="/profile" class="dropdown-item" @click="closeMenus">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
Edit Profile
|
||||
</RouterLink>
|
||||
<RouterLink to="/zones" class="dropdown-item" @click="closeMenus">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="19" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="5" cy="12" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
Training Zones
|
||||
</RouterLink>
|
||||
<button @click="handleLogout" class="logout-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 21H5C4.46957 21 3.96086 20.7893 3.58579 20.4142C3.21071 20.0391 3 19.5304 3 19V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="16,17 21,12 16,7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="21" y1="12" x2="9" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const router = useRouter()
|
||||
const auth = useAuth()
|
||||
|
||||
const mobileMenuOpen = ref(false)
|
||||
const userMenuOpen = ref(false)
|
||||
const isDarkMode = ref(false)
|
||||
|
||||
function toggleMobileMenu() {
|
||||
mobileMenuOpen.value = !mobileMenuOpen.value
|
||||
if (mobileMenuOpen.value) {
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeMobileMenu() {
|
||||
mobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
function closeMenus() {
|
||||
mobileMenuOpen.value = false
|
||||
userMenuOpen.value = false
|
||||
}
|
||||
|
||||
function toggleUserMenu() {
|
||||
userMenuOpen.value = !userMenuOpen.value
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
isDarkMode.value = !isDarkMode.value
|
||||
document.documentElement.classList.toggle('dark', isDarkMode.value)
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
auth.logout()
|
||||
closeMenus()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modern-navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.navbar-brand h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle span {
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: #1f2937;
|
||||
border-radius: 1px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle.active span:nth-child(1) {
|
||||
transform: rotate(45deg) translate(8px, 8px);
|
||||
}
|
||||
|
||||
.mobile-menu-toggle.active span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle.active span:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(7px, -7px);
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.75rem;
|
||||
text-decoration: none;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.nav-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
color: #667eea;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.nav-link.router-link-active {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 0.75rem;
|
||||
background: #f3f4f6;
|
||||
border: none;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-toggle svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
background: #e5e7eb;
|
||||
transform: rotate(20deg);
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 0.75rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.user-avatar svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
padding: 0;
|
||||
min-width: 250px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.user-dropdown.active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.user-info .username {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.user-info small {
|
||||
display: block;
|
||||
color: #6b7280;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.dropdown-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: #f8fafc;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #ef4444;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.logout-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.navbar-container {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.mobile-menu-toggle {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
position: fixed;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
backdrop-filter: blur(20px);
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
max-height: calc(100vh - 60px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.navbar-menu.active {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.user-dropdown {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
background: #f9fafb;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,174 +1,875 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1>Login to RideAware</h1>
|
||||
<p class="subtitle">Train with Focus. Ride with Awareness</p>
|
||||
<div class="card-header">
|
||||
<div class="logo-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="13" r="2" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>Welcome Back</h1>
|
||||
<p class="subtitle">Train with Focus. Ride with Awareness.</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<!-- Username Field -->
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
id="username"
|
||||
v-model="form.username"
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
required
|
||||
:disabled="auth.loading"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<svg class="input-icon" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20 21V19C20 17.9391 19.5786 16.9217 18.8284 16.1716C18.0783 15.4214 17.0609 15 16 15H8C6.93913 15 5.92172 15.4214 5.17157 16.1716C4.42143 16.9217 4 17.9391 4 19V21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Field -->
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
/>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
id="password"
|
||||
v-model="form.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
placeholder="Enter your password"
|
||||
required
|
||||
:disabled="auth.loading"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="password-toggle"
|
||||
:title="showPassword ? 'Hide password' : 'Show password'"
|
||||
>
|
||||
<svg v-if="showPassword" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M1 12S6 4 12 4C18 4 23 12 23 12S18 20 12 20C6 20 1 12 1 12Z" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 24 24" fill="none">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20C7 20 2.73 16.46 1 11.64M9.9 4.24C10.94 4.09 11.97 4 12 4C17 4 21.27 7.54 23 12.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me & Forgot Password -->
|
||||
<div class="form-options">
|
||||
<label class="remember-me">
|
||||
<input v-model="rememberMe" type="checkbox" />
|
||||
<span>Remember me</span>
|
||||
</label>
|
||||
<RouterLink to="/password-reset" class="forgot-password">
|
||||
Forgot password?
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- Login Button -->
|
||||
<button type="submit" :disabled="auth.loading" class="btn-primary">
|
||||
{{ auth.loading ? 'Logging in...' : 'Login' }}
|
||||
<span v-if="auth.loading" class="loading-content">
|
||||
<svg class="spinner" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" opacity="0.25"/>
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" stroke-dasharray="31.416" stroke-dashoffset="31.416" opacity="0.75"/>
|
||||
</svg>
|
||||
Logging in...
|
||||
</span>
|
||||
<span v-else>Login</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div v-if="auth.error" class="error-message">
|
||||
{{ auth.error }}
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p class="error-title">Login Failed</p>
|
||||
<p class="error-text">{{ auth.error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="links">
|
||||
<RouterLink to="/signup">Don't have an account? Sign up</RouterLink>
|
||||
<RouterLink to="/password-reset">Forgot password?</RouterLink>
|
||||
<!-- Divider -->
|
||||
<div class="divider">
|
||||
<span>Or</span>
|
||||
</div>
|
||||
|
||||
<!-- Sign Up Link -->
|
||||
<div class="signup-section">
|
||||
<p>Don't have an account?</p>
|
||||
<RouterLink to="/signup" class="signup-link">
|
||||
Sign up now
|
||||
<svg viewBox="0 0 24 24" fill="none">
|
||||
<line x1="5" y1="12" x2="19" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<polyline points="12 5 19 12 12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Background Decorations -->
|
||||
<div class="background-decoration">
|
||||
<div class="decoration-blob blob-1"></div>
|
||||
<div class="decoration-blob blob-2"></div>
|
||||
<div class="decoration-blob blob-3"></div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Link -->
|
||||
<div class="bottom-link">
|
||||
<RouterLink to="/">Back to Home</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useAuth } from '@/composables/useAuth';
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuth } from '@/composables/useAuth'
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const router = useRouter()
|
||||
const auth = useAuth()
|
||||
|
||||
const showPassword = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
username: '',
|
||||
password: '',
|
||||
});
|
||||
password: ''
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
try {
|
||||
await auth.login(form.username, form.password);
|
||||
router.push('/dashboard');
|
||||
await auth.login(form.username, form.password)
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem('rememberMe', 'true')
|
||||
localStorage.setItem('username', form.username)
|
||||
} else {
|
||||
localStorage.removeItem('rememberMe')
|
||||
localStorage.removeItem('username')
|
||||
}
|
||||
router.push('/dashboard')
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
console.error('Login error:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 1rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOGIN CARD
|
||||
============================================ */
|
||||
.login-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
animation: slideUp 0.6s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CARD HEADER
|
||||
============================================ */
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
padding: 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2rem;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: logoFloat 0.8s ease-out 0.2s both;
|
||||
}
|
||||
|
||||
.logo-wrapper svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.8rem;
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
color: #1f2937;
|
||||
animation: titleSlide 0.6s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 1.5rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
animation: subtitleFade 0.6s ease-out 0.4s both;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LOGIN FORM
|
||||
============================================ */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
animation: formSlide 0.6s ease-out 0.5s both;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 0.95rem;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 4px;
|
||||
padding: 1rem 1rem 1rem 3rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 1rem;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
font-family: inherit;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: white;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: #f9fafb;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.password-toggle svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
FORM OPTIONS
|
||||
============================================ */
|
||||
.form-options {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.remember-me input {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
accent-color: #667eea;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.remember-me input:checked {
|
||||
background: #667eea;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.forgot-password:hover {
|
||||
color: #764ba2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SUBMIT BUTTON
|
||||
============================================ */
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
padding: 1.125rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border-radius: 1rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background-color: #fee;
|
||||
color: #c33;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.links {
|
||||
.loading-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1.5rem;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ERROR MESSAGE
|
||||
============================================ */
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: #fef2f2;
|
||||
border: 2px solid #fecaca;
|
||||
border-radius: 1rem;
|
||||
color: #dc2626;
|
||||
animation: slideInError 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.error-message svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
DIVIDER
|
||||
============================================ */
|
||||
.divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.divider span {
|
||||
position: relative;
|
||||
background: white;
|
||||
padding: 0 1rem;
|
||||
color: #9ca3af;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
SIGNUP SECTION
|
||||
============================================ */
|
||||
.signup-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.links a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
.signup-section p {
|
||||
margin: 0 0 0.75rem 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.links a:hover {
|
||||
.signup-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.signup-link svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.signup-link:hover {
|
||||
color: #764ba2;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BACKGROUND DECORATIONS
|
||||
============================================ */
|
||||
.background-decoration {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.decoration-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
animation: float 8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
top: -10%;
|
||||
left: -10%;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
bottom: -5%;
|
||||
right: -5%;
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 50%;
|
||||
left: -5%;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
BOTTOM LINK
|
||||
============================================ */
|
||||
.bottom-link {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.bottom-link a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.bottom-link a:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ANIMATIONS
|
||||
============================================ */
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes logoFloat {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes titleSlide {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes subtitleFade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes formSlide {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInError {
|
||||
from {
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translateY(0px) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-30px) rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE DESIGN
|
||||
============================================ */
|
||||
@media (max-width: 640px) {
|
||||
.login-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
padding: 2rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.form-options {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.875rem 0.875rem 0.875rem 2.75rem;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
left: 0.75rem;
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
right: 0.75rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
left: -50px;
|
||||
right: -50px;
|
||||
}
|
||||
|
||||
.signup-section p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.blob-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.blob-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.blob-3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.bottom-link a {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-card {
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-options {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.remember-me,
|
||||
.forgot-password {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.divider span {
|
||||
font-size: 0.85rem;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.signup-section p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.signup-link {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.error-message svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.bottom-link {
|
||||
bottom: 1rem;
|
||||
}
|
||||
|
||||
.bottom-link a {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.login-card,
|
||||
.logo-wrapper,
|
||||
h1,
|
||||
.subtitle,
|
||||
.login-form,
|
||||
.error-message,
|
||||
.decoration-blob {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
.btn-primary:hover,
|
||||
.password-toggle:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
677
src/components/WorkoutCalendar.vue
Normal file
677
src/components/WorkoutCalendar.vue
Normal file
@@ -0,0 +1,677 @@
|
||||
<template>
|
||||
<div class="calendar-page">
|
||||
<ModernNavbar />
|
||||
|
||||
<div class="calendar-content">
|
||||
<div class="calendar-header">
|
||||
<h2>Workout Calendar</h2>
|
||||
<div class="header-controls">
|
||||
<button @click="previousMonth" class="btn-nav">← Previous</button>
|
||||
<span class="current-month">{{ monthYear }}</span>
|
||||
<button @click="nextMonth" class="btn-nav">Next →</button>
|
||||
<button @click="showAddWorkout = true" class="btn-primary">+ Add Workout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Workout Modal -->
|
||||
<div v-if="showAddWorkout" class="modal-overlay" @click.self="showAddWorkout = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add Workout</h3>
|
||||
<button @click="showAddWorkout = false" class="btn-close">✕</button>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleAddWorkout" class="workout-form">
|
||||
<div class="form-group">
|
||||
<label>Title *</label>
|
||||
<input v-model="newWorkout.title" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Date *</label>
|
||||
<input v-model="newWorkout.scheduled_date" type="date" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Type</label>
|
||||
<select v-model="newWorkout.type">
|
||||
<option value="">Select type</option>
|
||||
<option v-for="type in workoutTypes" :key="type.id" :value="type.name">
|
||||
{{ type.icon }} {{ type.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Duration (minutes)</label>
|
||||
<input v-model.number="newWorkout.duration" type="number" step="5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea v-model="newWorkout.description" placeholder="Workout details..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>File Upload (ZWO/FIT/TCX/GPX)</label>
|
||||
<input @change="handleFileUpload" type="file" accept=".zwo,.fit,.tcx,.gpx" />
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" :disabled="loading" class="btn-primary">
|
||||
{{ loading ? 'Adding...' : 'Add Workout' }}
|
||||
</button>
|
||||
<button type="button" @click="showAddWorkout = false" class="btn-secondary">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="error" class="error-message">{{ error }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Calendar Grid -->
|
||||
<div class="calendar-grid">
|
||||
<div class="calendar-header-row">
|
||||
<div class="day-header">Sun</div>
|
||||
<div class="day-header">Mon</div>
|
||||
<div class="day-header">Tue</div>
|
||||
<div class="day-header">Wed</div>
|
||||
<div class="day-header">Thu</div>
|
||||
<div class="day-header">Fri</div>
|
||||
<div class="day-header">Sat</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-days">
|
||||
<div
|
||||
v-for="day in calendarDays"
|
||||
:key="day.date"
|
||||
class="calendar-day"
|
||||
:class="{ 'other-month': !day.isCurrentMonth, 'today': day.isToday }"
|
||||
>
|
||||
<div class="day-number">{{ day.day }}</div>
|
||||
<div class="day-workouts">
|
||||
<div
|
||||
v-for="workout in day.workouts"
|
||||
:key="workout.id"
|
||||
class="workout-badge"
|
||||
:style="{ backgroundColor: getWorkoutColor(workout.type) }"
|
||||
@click="selectWorkout(workout)"
|
||||
:title="workout.title"
|
||||
>
|
||||
{{ workout.title.substring(0, 10) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workout Details Component -->
|
||||
<WorkoutDetail
|
||||
v-if="selectedWorkout"
|
||||
:workout="selectedWorkout"
|
||||
@close="selectedWorkout = null"
|
||||
@updated="loadWorkouts"
|
||||
@deleted="loadWorkouts"
|
||||
/>
|
||||
|
||||
<!-- Workouts List -->
|
||||
<div class="workouts-list">
|
||||
<h3>Upcoming Workouts</h3>
|
||||
<div v-if="upcomingWorkouts.length > 0" class="workouts">
|
||||
<div v-for="workout in upcomingWorkouts.slice(0, 5)" :key="workout.id" class="workout-item">
|
||||
<div class="workout-item-header">
|
||||
<span class="workout-date">{{ formatDate(workout.scheduled_date) }}</span>
|
||||
<span class="workout-title">{{ workout.title }}</span>
|
||||
<span class="workout-type" v-if="workout.type">{{ workout.type }}</span>
|
||||
</div>
|
||||
<div class="workout-item-footer">
|
||||
<span class="workout-duration">{{ workout.duration }}min</span>
|
||||
<span :class="['workout-status', workout.status]">{{ workout.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
No upcoming workouts
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import api from '@/services/api'
|
||||
import ModernNavbar from '@/components/ModernNavbar.vue'
|
||||
import WorkoutDetail from './WorkoutDetail.vue'
|
||||
|
||||
const currentDate = ref(new Date())
|
||||
const workouts = ref([])
|
||||
const selectedWorkout = ref(null)
|
||||
const showAddWorkout = ref(false)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const workoutTypes = ref([])
|
||||
|
||||
const newWorkout = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
scheduled_date: new Date().toISOString().split('T')[0],
|
||||
duration: 60,
|
||||
})
|
||||
|
||||
const monthYear = computed(() => {
|
||||
return currentDate.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
|
||||
})
|
||||
|
||||
const calendarDays = computed(() => {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth()
|
||||
|
||||
const firstDay = new Date(year, month, 1)
|
||||
const lastDay = new Date(year, month + 1, 0)
|
||||
const startDate = new Date(firstDay)
|
||||
startDate.setDate(startDate.getDate() - firstDay.getDay())
|
||||
|
||||
const days = []
|
||||
let currentDay = new Date(startDate)
|
||||
|
||||
while (currentDay <= lastDay || currentDay.getDay() !== 0) {
|
||||
const dateStr = currentDay.toISOString().split('T')[0]
|
||||
const dayWorkouts = workouts.value.filter(w =>
|
||||
w.scheduled_date.split('T')[0] === dateStr
|
||||
)
|
||||
|
||||
days.push({
|
||||
date: dateStr,
|
||||
day: currentDay.getDate(),
|
||||
isCurrentMonth: currentDay.getMonth() === month,
|
||||
isToday: isToday(currentDay),
|
||||
workouts: dayWorkouts,
|
||||
})
|
||||
|
||||
currentDay.setDate(currentDay.getDate() + 1)
|
||||
}
|
||||
|
||||
return days
|
||||
})
|
||||
|
||||
const upcomingWorkouts = computed(() => {
|
||||
return workouts.value
|
||||
.filter(w => new Date(w.scheduled_date) >= new Date())
|
||||
.sort((a, b) => new Date(a.scheduled_date) - new Date(b.scheduled_date))
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadWorkouts()
|
||||
await loadWorkoutTypes()
|
||||
})
|
||||
|
||||
async function loadWorkouts() {
|
||||
try {
|
||||
const { data } = await api.get('/api/protected/workouts')
|
||||
workouts.value = data || []
|
||||
} catch (err) {
|
||||
console.error('Failed to load workouts:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkoutTypes() {
|
||||
try {
|
||||
const { data } = await api.get('/api/protected/workout-types')
|
||||
workoutTypes.value = data || []
|
||||
} catch (err) {
|
||||
console.error('Failed to load workout types:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddWorkout() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const { data } = await api.post('/api/protected/workouts', {
|
||||
title: newWorkout.value.title,
|
||||
description: newWorkout.value.description,
|
||||
type: newWorkout.value.type,
|
||||
scheduled_date: newWorkout.value.scheduled_date,
|
||||
duration: newWorkout.value.duration,
|
||||
})
|
||||
|
||||
workouts.value.push(data)
|
||||
showAddWorkout.value = false
|
||||
newWorkout.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
type: '',
|
||||
scheduled_date: new Date().toISOString().split('T')[0],
|
||||
duration: 60,
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || 'Failed to add workout'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileUpload(event) {
|
||||
const file = event.target.files[0]
|
||||
if (file) {
|
||||
newWorkout.value.file_name = file.name
|
||||
newWorkout.value.file_type = file.name.split('.').pop().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
function previousMonth() {
|
||||
currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() - 1)
|
||||
}
|
||||
|
||||
function nextMonth() {
|
||||
currentDate.value = new Date(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
|
||||
}
|
||||
|
||||
function selectWorkout(workout) {
|
||||
selectedWorkout.value = workout
|
||||
}
|
||||
|
||||
function isToday(date) {
|
||||
const today = new Date()
|
||||
return date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function getWorkoutColor(type) {
|
||||
const typeObj = workoutTypes.value.find(t => t.name === type)
|
||||
return typeObj?.color || '#667eea'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.calendar-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.calendar-content {
|
||||
max-width: 1400px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.calendar-header h2 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.current-month {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
min-width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-nav {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #ecf0f1;
|
||||
color: #2c3e50;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-nav:hover {
|
||||
background: #bdc3c7;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.workout-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin-top: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.calendar-header-row,
|
||||
.calendar-days {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1px;
|
||||
background: #ecf0f1;
|
||||
}
|
||||
|
||||
.day-header {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
min-height: 120px;
|
||||
border: 1px solid #ecf0f1;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.calendar-day:hover {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.calendar-day.other-month {
|
||||
background: #fafafa;
|
||||
color: #bdc3c7;
|
||||
}
|
||||
|
||||
.calendar-day.today {
|
||||
background: #f0f4ff;
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.day-workouts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.workout-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.workout-badge:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.workouts-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.workouts-list h3 {
|
||||
margin-top: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.workouts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.workout-item {
|
||||
padding: 1rem;
|
||||
border-left: 4px solid #667eea;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.workout-item:hover {
|
||||
background: #f0f4ff;
|
||||
}
|
||||
|
||||
.workout-item-header {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workout-date {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.workout-title {
|
||||
flex: 1;
|
||||
color: #2c3e50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.workout-type {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.workout-item-footer {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.workout-duration {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.workout-status {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.workout-status.planned {
|
||||
background: #FBBC04;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.workout-status.completed {
|
||||
background: #34A853;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.workout-status.skipped {
|
||||
background: #EA4335;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.calendar-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
461
src/components/WorkoutDetail.vue
Normal file
461
src/components/WorkoutDetail.vue
Normal file
@@ -0,0 +1,461 @@
|
||||
<template>
|
||||
<div class="workout-detail-modal" v-if="workout">
|
||||
<div class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-window">
|
||||
<div class="detail-header">
|
||||
<div class="header-info">
|
||||
<h2>{{ workout.title }}</h2>
|
||||
<p class="workout-meta">
|
||||
{{ formatDate(workout.scheduled_date) }} • {{ formatDuration(workout.duration) }}
|
||||
</p>
|
||||
</div>
|
||||
<button @click="closeModal" class="btn-close">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="detail-body">
|
||||
<!-- Workout Info -->
|
||||
<div class="info-section">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">Type</span>
|
||||
<span class="value">{{ workout.type || 'Custom' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Duration</span>
|
||||
<span class="value">{{ formatDuration(workout.duration) }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">Status</span>
|
||||
<span class="value status" :class="workout.status">{{ workout.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workout Segments (if parsed from ZWO) -->
|
||||
<div v-if="workout.workout_data && workout.workout_data.segments" class="segments-section">
|
||||
<h3>Workout Segments</h3>
|
||||
<div class="segments-list">
|
||||
<div v-for="(segment, idx) in workout.workout_data.segments" :key="idx" class="segment-card">
|
||||
<div class="segment-header">
|
||||
<span class="segment-type">{{ formatSegmentType(segment.type) }}</span>
|
||||
<span class="segment-duration">{{ formatDuration(segment.duration) }}</span>
|
||||
</div>
|
||||
<div class="segment-details">
|
||||
<div v-if="segment.power" class="detail-row">
|
||||
<span>Power:</span>
|
||||
<strong>{{ (segment.power * 100).toFixed(0) }}% FTP</strong>
|
||||
</div>
|
||||
<div v-if="segment.power_low && segment.power_high" class="detail-row">
|
||||
<span>Power Range:</span>
|
||||
<strong>{{ (segment.power_low * 100).toFixed(0) }}% - {{ (segment.power_high * 100).toFixed(0) }}% FTP</strong>
|
||||
</div>
|
||||
<div v-if="segment.cadence" class="detail-row">
|
||||
<span>Cadence:</span>
|
||||
<strong>{{ segment.cadence }} RPM</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Metrics -->
|
||||
<div v-if="workout.status === 'completed'" class="metrics-section">
|
||||
<h3>Completed Metrics</h3>
|
||||
<div class="metrics-grid">
|
||||
<div v-if="workout.distance" class="metric">
|
||||
<span class="metric-label">Distance</span>
|
||||
<span class="metric-value">{{ workout.distance }} km</span>
|
||||
</div>
|
||||
<div v-if="workout.avg_power" class="metric">
|
||||
<span class="metric-label">Avg Power</span>
|
||||
<span class="metric-value">{{ workout.avg_power }}W</span>
|
||||
</div>
|
||||
<div v-if="workout.avg_hr" class="metric">
|
||||
<span class="metric-label">Avg HR</span>
|
||||
<span class="metric-value">{{ workout.avg_hr }} bpm</span>
|
||||
</div>
|
||||
<div v-if="workout.calories_burned" class="metric">
|
||||
<span class="metric-label">Calories</span>
|
||||
<span class="metric-value">{{ workout.calories_burned }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div v-if="workout.description" class="description-section">
|
||||
<h3>Description</h3>
|
||||
<p>{{ workout.description }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Author/Source -->
|
||||
<div v-if="workout.workout_data && workout.workout_data.author" class="source-section">
|
||||
<small>Source: {{ workout.workout_data.author }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-footer">
|
||||
<select @change="updateStatus($event.target.value)" class="status-select">
|
||||
<option value="planned">Planned</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="skipped">Skipped</option>
|
||||
</select>
|
||||
<button @click="deleteWorkout" class="btn-danger">Delete</button>
|
||||
<button @click="closeModal" class="btn-secondary">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
const props = defineProps({
|
||||
workout: Object,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'updated', 'deleted'])
|
||||
|
||||
function closeModal() {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function updateStatus(newStatus) {
|
||||
try {
|
||||
await api.put(`/api/protected/workouts?id=${props.workout.id}`, {
|
||||
status: newStatus,
|
||||
})
|
||||
emit('updated')
|
||||
} catch (error) {
|
||||
console.error('Failed to update workout:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteWorkout() {
|
||||
if (!confirm('Delete this workout?')) return
|
||||
try {
|
||||
await api.delete(`/api/protected/workouts?id=${props.workout.id}`)
|
||||
emit('deleted')
|
||||
emit('close')
|
||||
} catch (error) {
|
||||
console.error('Failed to delete workout:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
return new Date(dateStr).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
function formatDuration(seconds) {
|
||||
const hours = Math.floor(seconds / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
return `${minutes}m`
|
||||
}
|
||||
|
||||
function formatSegmentType(type) {
|
||||
return type.charAt(0).toUpperCase() + type.slice(1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.workout-detail-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.modal-window {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 2rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.header-info h2 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.workout-meta {
|
||||
margin: 0;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #7f8c8d;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
background: #f9f9f9;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 0.8rem;
|
||||
color: #7f8c8d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 1rem;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.value.status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.value.status.planned {
|
||||
background: #FBBC04;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.value.status.completed {
|
||||
background: #34A853;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.value.status.skipped {
|
||||
background: #EA4335;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.segments-section h3,
|
||||
.metrics-section h3,
|
||||
.description-section h3 {
|
||||
margin: 0 0 1rem 0;
|
||||
color: #2c3e50;
|
||||
font-size: 1.1rem;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.segments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.segment-card {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
|
||||
border: 1px solid #e0e6ed;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.segment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.segment-type {
|
||||
display: inline-block;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.segment-duration {
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.segment-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.detail-row span {
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.detail-row strong {
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.metric {
|
||||
background: #f9f9f9;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: #7f8c8d;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: block;
|
||||
font-size: 1.25rem;
|
||||
color: #667eea;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.description-section p {
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.source-section {
|
||||
color: #95a5a6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 1.5rem 2rem;
|
||||
border-top: 1px solid #ecf0f1;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.status-select {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #bdc3c7;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.modal-window {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-footer {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.detail-footer button,
|
||||
.detail-footer select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ import UserDashboard from '@/components/UserDashboard.vue'
|
||||
import UserProfile from '@/components/UserProfile.vue'
|
||||
import UserEquipment from '@/components/UserEquipment.vue'
|
||||
import TrainingZones from '@/components/TrainingZones.vue'
|
||||
import WorkoutCalendar from '@/components/WorkoutCalendar.vue'
|
||||
import PasswordReset from '@/components/PasswordReset.vue'
|
||||
|
||||
const routes = [
|
||||
@@ -52,6 +53,12 @@ const routes = [
|
||||
component: TrainingZones,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/calendar',
|
||||
name: 'Calendar',
|
||||
component: WorkoutCalendar,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/dashboard',
|
||||
|
||||
Reference in New Issue
Block a user