feat(ui): integrate login/signup into navbar, enhance login flow, and set dynamic titles
- Removed outdated comments and unused <LoggedinPage> component - Cleaned up login.css header comment - Updated Home.vue navigation to use <router-link> for Login/Sign Up - Enhanced UserLogin.vue: • Added loginSuccess state, success styling, and green “Success! Redirecting” feedback • Redirect to homepage (/) after sign-in • Persist user data & simple auth token in local/session storage • Updated API call URL to /auth/login - Added route definitions for /signup in router - Added meta.title to Home/Login/Sign Up routes and global afterEach hook to update document.title - Simplified unused methods and cleaned up commented code in Home.vue
This commit is contained in:
		
							parent
							
								
									6422189f6c
								
							
						
					
					
						commit
						aa8dad9809
					
				
					 7 changed files with 924 additions and 73 deletions
				
			
		|  | @ -1,4 +1,3 @@ | |||
| /* login.css */ | ||||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|  |  | |||
							
								
								
									
										517
									
								
								src/assets/css/components/signup.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										517
									
								
								src/assets/css/components/signup.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,517 @@ | |||
| * { | ||||
|   margin: 0; | ||||
|   padding: 0; | ||||
|   box-sizing: border-box; | ||||
| } | ||||
| 
 | ||||
| .signup-page { | ||||
|   min-height: 100vh; | ||||
|   background: linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 50%, #16213e 100%); | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; | ||||
| } | ||||
| 
 | ||||
| .signup-bg { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   left: 0; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   background: radial-gradient(circle at 20% 80%, rgba(0, 212, 255, 0.05) 0%, transparent 50%), | ||||
|               radial-gradient(circle at 80% 20%, rgba(124, 58, 237, 0.05) 0%, transparent 50%); | ||||
| } | ||||
| 
 | ||||
| .floating-elements { | ||||
|   position: absolute; | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   pointer-events: none; | ||||
| } | ||||
| 
 | ||||
| .floating-element { | ||||
|   position: absolute; | ||||
|   width: 3px; | ||||
|   height: 3px; | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
|   border-radius: 50%; | ||||
|   animation: float 8s ease-in-out infinite; | ||||
| } | ||||
| 
 | ||||
| @keyframes float { | ||||
|   0%, 100% {  | ||||
|     transform: translateY(0px) rotate(0deg);  | ||||
|     opacity: 0.3;  | ||||
|   } | ||||
|   50% {  | ||||
|     transform: translateY(-30px) rotate(180deg);  | ||||
|     opacity: 0.8;  | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .signup-container { | ||||
|   position: relative; | ||||
|   z-index: 2; | ||||
|   width: 100%; | ||||
|   max-width: 480px; | ||||
|   padding: 2rem; | ||||
| } | ||||
| 
 | ||||
| .signup-card { | ||||
|   background: rgba(255, 255, 255, 0.08); | ||||
|   backdrop-filter: blur(20px); | ||||
|   border: 1px solid rgba(255, 255, 255, 0.1); | ||||
|   border-radius: 24px; | ||||
|   padding: 3rem; | ||||
|   box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | ||||
|   animation: slideInUp 0.8s ease-out; | ||||
| } | ||||
| 
 | ||||
| @keyframes slideInUp { | ||||
|   from { | ||||
|     opacity: 0; | ||||
|     transform: translateY(50px); | ||||
|   } | ||||
|   to { | ||||
|     opacity: 1; | ||||
|     transform: translateY(0); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .signup-header { | ||||
|   text-align: center; | ||||
|   margin-bottom: 2.5rem; | ||||
| } | ||||
| 
 | ||||
| .logo { | ||||
|   font-size: 2rem; | ||||
|   font-weight: 800; | ||||
|   color: #ffffff; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .logo-accent { | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
|   background-clip: text; | ||||
|   -webkit-background-clip: text; | ||||
|   color: transparent; | ||||
| } | ||||
| 
 | ||||
| .signup-title { | ||||
|   font-size: 1.75rem; | ||||
|   font-weight: 700; | ||||
|   color: #ffffff; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .signup-subtitle { | ||||
|   color: #a0a0a0; | ||||
|   font-size: 0.95rem; | ||||
| } | ||||
| 
 | ||||
| .signup-form { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 1.5rem; | ||||
| } | ||||
| 
 | ||||
| .form-row { | ||||
|   display: flex; | ||||
|   gap: 1rem; | ||||
| } | ||||
| 
 | ||||
| .form-group { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 0.5rem; | ||||
|   margin-bottom: 1rem; | ||||
| } | ||||
| 
 | ||||
| .form-group.half-width { | ||||
|   flex: 1; | ||||
|   margin-bottom: 0; | ||||
| } | ||||
| 
 | ||||
| .form-label { | ||||
|   font-weight: 600; | ||||
|   color: #ffffff; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
| 
 | ||||
| .input-wrapper { | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .input-icon { | ||||
|   position: absolute; | ||||
|   left: 1rem; | ||||
|   top: 50%; | ||||
|   transform: translateY(-50%); | ||||
|   color: #888; | ||||
|   font-size: 0.9rem; | ||||
| } | ||||
| 
 | ||||
| .form-input { | ||||
|   width: 100%; | ||||
|   padding: 1rem 1rem 1rem 2.75rem; | ||||
|   background: rgba(255, 255, 255, 0.05); | ||||
|   border: 2px solid rgba(255, 255, 255, 0.1); | ||||
|   border-radius: 12px; | ||||
|   color: #ffffff; | ||||
|   font-size: 1rem; | ||||
|   transition: all 0.3s ease; | ||||
|   backdrop-filter: blur(10px); | ||||
| } | ||||
| 
 | ||||
| .form-input:focus { | ||||
|   outline: none; | ||||
|   border-color: #00d4ff; | ||||
|   box-shadow: 0 0 20px rgba(0, 212, 255, 0.2); | ||||
|   background: rgba(255, 255, 255, 0.08); | ||||
| } | ||||
| 
 | ||||
| .form-input.error { | ||||
|   border-color: #ff4757; | ||||
|   box-shadow: 0 0 20px rgba(255, 71, 87, 0.2); | ||||
| } | ||||
| 
 | ||||
| .form-input::placeholder { | ||||
|   color: #666; | ||||
| } | ||||
| 
 | ||||
| .password-toggle { | ||||
|   position: absolute; | ||||
|   right: 1rem; | ||||
|   top: 50%; | ||||
|   transform: translateY(-50%); | ||||
|   background: none; | ||||
|   border: none; | ||||
|   color: #888; | ||||
|   cursor: pointer; | ||||
|   padding: 0.25rem; | ||||
|   transition: color 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .password-toggle:hover { | ||||
|   color: #00d4ff; | ||||
| } | ||||
| 
 | ||||
| .password-strength { | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .strength-bar { | ||||
|   width: 100%; | ||||
|   height: 4px; | ||||
|   background-color: rgba(255, 255, 255, 0.1); | ||||
|   border-radius: 2px; | ||||
|   overflow: hidden; | ||||
|   margin-bottom: 0.25rem; | ||||
| } | ||||
| 
 | ||||
| .strength-fill { | ||||
|   height: 100%; | ||||
|   transition: width 0.3s ease, background-color 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .strength-fill.weak { | ||||
|   background: linear-gradient(45deg, #ff4757, #ff6b7a); | ||||
| } | ||||
| 
 | ||||
| .strength-fill.fair { | ||||
|   background: linear-gradient(45deg, #ffa502, #ffb347); | ||||
| } | ||||
| 
 | ||||
| .strength-fill.good { | ||||
|   background: linear-gradient(45deg, #2ed573, #7bed9f); | ||||
| } | ||||
| 
 | ||||
| .strength-fill.strong { | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
| } | ||||
| 
 | ||||
| .strength-text { | ||||
|   font-size: 0.75rem; | ||||
|   font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| .strength-text.weak { | ||||
|   color: #ff4757; | ||||
| } | ||||
| 
 | ||||
| .strength-text.fair { | ||||
|   color: #ffa502; | ||||
| } | ||||
| 
 | ||||
| .strength-text.good { | ||||
|   color: #2ed573; | ||||
| } | ||||
| 
 | ||||
| .strength-text.strong { | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
|   background-clip: text; | ||||
|   -webkit-background-clip: text; | ||||
|   color: transparent; | ||||
| } | ||||
| 
 | ||||
| .password-mismatch { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; | ||||
|   margin-top: 0.5rem; | ||||
|   font-size: 0.75rem; | ||||
|   color: #ff4757; | ||||
|   animation: shake 0.5s ease-in-out; | ||||
| } | ||||
| 
 | ||||
| .form-options { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 1rem; | ||||
|   margin-bottom: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .checkbox-wrapper { | ||||
|   display: flex; | ||||
|   align-items: flex-start; | ||||
|   cursor: pointer; | ||||
|   gap: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .checkbox-input { | ||||
|   display: none; | ||||
| } | ||||
| 
 | ||||
| .checkbox-custom { | ||||
|   width: 18px; | ||||
|   height: 18px; | ||||
|   border: 2px solid rgba(255, 255, 255, 0.3); | ||||
|   border-radius: 4px; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   transition: all 0.3s ease; | ||||
|   flex-shrink: 0; | ||||
|   margin-top: 1px; | ||||
| } | ||||
| 
 | ||||
| .checkbox-input:checked + .checkbox-custom { | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
|   border-color: transparent; | ||||
| } | ||||
| 
 | ||||
| .checkbox-input:checked + .checkbox-custom::after { | ||||
|   content: '✓'; | ||||
|   color: white; | ||||
|   font-size: 12px; | ||||
|   font-weight: bold; | ||||
| } | ||||
| 
 | ||||
| .checkbox-label { | ||||
|   color: #a0a0a0; | ||||
|   font-size: 0.9rem; | ||||
|   line-height: 1.4; | ||||
| } | ||||
| 
 | ||||
| .link { | ||||
|   color: #00d4ff; | ||||
|   text-decoration: none; | ||||
|   transition: color 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .link:hover { | ||||
|   color: #7c3aed; | ||||
|   text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| .signup-button { | ||||
|   width: 100%; | ||||
|   padding: 1rem; | ||||
|   background: linear-gradient(45deg, #00d4ff, #7c3aed); | ||||
|   color: white; | ||||
|   border: none; | ||||
|   border-radius: 12px; | ||||
|   font-weight: 600; | ||||
|   font-size: 1.1rem; | ||||
|   cursor: pointer; | ||||
|   transition: all 0.3s ease; | ||||
|   margin-top: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .signup-button:hover:not(:disabled) { | ||||
|   transform: translateY(-2px); | ||||
|   box-shadow: 0 15px 40px rgba(0, 212, 255, 0.4); | ||||
| } | ||||
| 
 | ||||
| .signup-button:disabled { | ||||
|   opacity: 0.5; | ||||
|   cursor: not-allowed; | ||||
|   transform: none; | ||||
|   background: rgba(255, 255, 255, 0.1); | ||||
| } | ||||
| 
 | ||||
| .loading-text { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   gap: 0.5rem; | ||||
| } | ||||
| 
 | ||||
| .divider { | ||||
|   position: relative; | ||||
|   text-align: center; | ||||
|   margin: 1.5rem 0; | ||||
|   color: #666; | ||||
| } | ||||
| 
 | ||||
| .divider::before { | ||||
|   content: ''; | ||||
|   position: absolute; | ||||
|   top: 50%; | ||||
|   left: 0; | ||||
|   right: 0; | ||||
|   height: 1px; | ||||
|   background: rgba(255, 255, 255, 0.1); | ||||
| } | ||||
| 
 | ||||
| .divider span { | ||||
|   background: rgba(255, 255, 255, 0.08); | ||||
|   padding: 0 1rem; | ||||
|   position: relative; | ||||
|   z-index: 1; | ||||
| } | ||||
| 
 | ||||
| .social-signup { | ||||
|   width: 100%; | ||||
|   padding: 0.875rem; | ||||
|   background: rgba(255, 255, 255, 0.05); | ||||
|   border: 2px solid rgba(255, 255, 255, 0.1); | ||||
|   border-radius: 12px; | ||||
|   color: #ffffff; | ||||
|   font-weight: 600; | ||||
|   cursor: pointer; | ||||
|   transition: all 0.3s ease; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   gap: 0.75rem; | ||||
|   margin-bottom: 0.75rem; | ||||
|   backdrop-filter: blur(10px); | ||||
| } | ||||
| 
 | ||||
| .social-signup:hover { | ||||
|   background: rgba(255, 255, 255, 0.08); | ||||
|   border-color: rgba(255, 255, 255, 0.2); | ||||
| } | ||||
| 
 | ||||
| .social-signup.google:hover { | ||||
|   border-color: rgba(219, 68, 55, 0.5); | ||||
|   box-shadow: 0 0 20px rgba(219, 68, 55, 0.1); | ||||
| } | ||||
| 
 | ||||
| .social-signup.github:hover { | ||||
|   border-color: rgba(255, 255, 255, 0.5); | ||||
|   box-shadow: 0 0 20px rgba(255, 255, 255, 0.1); | ||||
| } | ||||
| 
 | ||||
| .error-message { | ||||
|   background: rgba(255, 71, 87, 0.1); | ||||
|   border: 1px solid rgba(255, 71, 87, 0.3); | ||||
|   border-radius: 12px; | ||||
|   padding: 1rem; | ||||
|   color: #ff4757; | ||||
|   font-size: 0.9rem; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; | ||||
|   margin-top: 1rem; | ||||
|   animation: shake 0.5s ease-in-out; | ||||
| } | ||||
| 
 | ||||
| .success-message { | ||||
|   background: rgba(46, 213, 115, 0.1); | ||||
|   border: 1px solid rgba(46, 213, 115, 0.3); | ||||
|   border-radius: 12px; | ||||
|   padding: 1rem; | ||||
|   color: #2ed573; | ||||
|   font-size: 0.9rem; | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   gap: 0.5rem; | ||||
|   margin-top: 1rem; | ||||
|   animation: slideInUp 0.5s ease-out; | ||||
| } | ||||
| 
 | ||||
| @keyframes shake { | ||||
|   0%, 100% { transform: translateX(0); } | ||||
|   25% { transform: translateX(-5px); } | ||||
|   75% { transform: translateX(5px); } | ||||
| } | ||||
| 
 | ||||
| .login-prompt { | ||||
|   text-align: center; | ||||
|   margin-top: 2rem; | ||||
|   padding-top: 2rem; | ||||
|   border-top: 1px solid rgba(255, 255, 255, 0.1); | ||||
| } | ||||
| 
 | ||||
| .login-prompt p { | ||||
|   color: #a0a0a0; | ||||
| } | ||||
| 
 | ||||
| .login-link { | ||||
|   color: #00d4ff; | ||||
|   text-decoration: none; | ||||
|   font-weight: 600; | ||||
|   transition: color 0.3s ease; | ||||
| } | ||||
| 
 | ||||
| .login-link:hover { | ||||
|   color: #7c3aed; | ||||
| } | ||||
| 
 | ||||
| /* Responsive */ | ||||
| @media (max-width: 640px) { | ||||
|   .signup-container { | ||||
|     padding: 1rem; | ||||
|     max-width: 100%; | ||||
|   } | ||||
|    | ||||
|   .signup-card { | ||||
|     padding: 2rem; | ||||
|   } | ||||
|    | ||||
|   .form-row { | ||||
|     flex-direction: column; | ||||
|     gap: 0; | ||||
|   } | ||||
|    | ||||
|   .form-group.half-width { | ||||
|     margin-bottom: 1rem; | ||||
|   } | ||||
|    | ||||
|   .checkbox-wrapper { | ||||
|     align-items: flex-start; | ||||
|   } | ||||
|    | ||||
|   .checkbox-label { | ||||
|     font-size: 0.85rem; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| @media (max-width: 480px) { | ||||
|   .signup-card { | ||||
|     padding: 1.5rem; | ||||
|   } | ||||
|    | ||||
|   .signup-title { | ||||
|     font-size: 1.5rem; | ||||
|   } | ||||
|    | ||||
|   .logo { | ||||
|     font-size: 1.75rem; | ||||
|   } | ||||
| } | ||||
|  | @ -1,19 +1,24 @@ | |||
| <template> | ||||
|   <div class="homepage"> | ||||
|     <!-- Navigation --> | ||||
|     <nav class="navbar"> | ||||
|       <div class="nav-content"> | ||||
|         <div class="logo" @click="scrollTo('hero')"> | ||||
|           Ride<span class="logo-accent">Aware</span> | ||||
|         </div> | ||||
|         <ul class="nav-links"> | ||||
|           <li><a href="#" @click.prevent="scrollTo('features')">Features</a></li> | ||||
|           <li><a href="/newsletters" @click.prevent="goToNewsletters">Newsletters</a></li> | ||||
|           <li> | ||||
|             <a href="#" @click.prevent="scrollTo('features')">Features</a> | ||||
|           </li> | ||||
|           <li> | ||||
|             <router-link to="/login" class="login-link">Login</router-link> | ||||
|           </li> | ||||
|           <li> | ||||
|             <router-link to="/signup" class="signup-btn">Sign Up</router-link> | ||||
|           </li> | ||||
|         </ul> | ||||
|       </div> | ||||
|     </nav> | ||||
| 
 | ||||
|     <!-- Hero Section --> | ||||
|     <section id="hero" class="hero"> | ||||
|       <div class="hero-bg"></div> | ||||
|       <div class="floating-elements"> | ||||
|  | @ -72,7 +77,6 @@ | |||
|       </div> | ||||
|     </section> | ||||
| 
 | ||||
|     <!-- Features Section --> | ||||
|     <section id="features" class="features"> | ||||
|       <div class="section-header"> | ||||
|         <h2 class="section-title">{{ featuresTitle }}</h2> | ||||
|  | @ -101,7 +105,6 @@ | |||
|       </div> | ||||
|     </section> | ||||
| 
 | ||||
|     <!-- Stats Section --> | ||||
|     <section class="stats"> | ||||
|       <div class="container"> | ||||
|         <div class="stats-grid"> | ||||
|  | @ -113,7 +116,6 @@ | |||
|       </div> | ||||
|     </section> | ||||
| 
 | ||||
|     <!-- Footer --> | ||||
|     <footer class="footer"> | ||||
|       <div class="container"> | ||||
|         <div class="footer-content"> | ||||
|  | @ -225,7 +227,6 @@ export default { | |||
|     const animatedStats = reactive({}) | ||||
|     const floatingElements = reactive([]) | ||||
| 
 | ||||
|     // Generate floating elements | ||||
|     const generateFloatingElements = () => { | ||||
|       for (let i = 0; i < 15; i++) { | ||||
|         floatingElements.push({ | ||||
|  | @ -239,7 +240,6 @@ export default { | |||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // Animate stats on scroll | ||||
|     const animateStats = () => { | ||||
|       platformStats.forEach(stat => { | ||||
|         let current = 0 | ||||
|  | @ -264,14 +264,11 @@ export default { | |||
|     } | ||||
| 
 | ||||
|     const goToNewsletters = () => { | ||||
|       // Handle navigation to newsletters page | ||||
|       // This could be a router push in a real Vue app | ||||
|       window.location.href = '/newsletters' | ||||
|     } | ||||
| 
 | ||||
|     const handleNotifyMe = () => { | ||||
|       if (isValidEmail.value) { | ||||
|         // Here you would typically send the email to your backend | ||||
|         alert(`Thanks! We'll notify you at ${emailInput.value} when RideAware launches.`) | ||||
|         emailInput.value = '' | ||||
|       } else { | ||||
|  | @ -293,7 +290,6 @@ export default { | |||
|     onMounted(() => { | ||||
|       generateFloatingElements() | ||||
|        | ||||
|       // Intersection Observer for stats animation | ||||
|       const observer = new IntersectionObserver((entries) => { | ||||
|         entries.forEach(entry => { | ||||
|           if (entry.isIntersecting) { | ||||
|  | @ -308,7 +304,6 @@ export default { | |||
|         observer.observe(statsSection) | ||||
|       } | ||||
| 
 | ||||
|       // Parallax effect for hero background | ||||
|       window.addEventListener('scroll', () => { | ||||
|         const scrolled = window.pageYOffset | ||||
|         const parallax = document.querySelector('.hero-bg') | ||||
|  | @ -318,7 +313,6 @@ export default { | |||
|         } | ||||
|       }) | ||||
| 
 | ||||
|       // Animate mockup stats | ||||
|       let statIndex = 0 | ||||
|       setInterval(() => { | ||||
|         activeStat.value = mockupStats[statIndex].id | ||||
|  |  | |||
|  | @ -1,19 +0,0 @@ | |||
| <template> | ||||
|     <div class="logged-in"> | ||||
|       <h2>You have successfully logged in!</h2> | ||||
|       <p>Welcome back to RideAware!</p> | ||||
|     </div> | ||||
|   </template> | ||||
|    | ||||
|   <script> | ||||
|   export default { | ||||
|     name: 'LoggedInPage', | ||||
|   }; | ||||
|   </script> | ||||
|    | ||||
|   <style scoped> | ||||
|   .logged-in { | ||||
|     text-align: center; | ||||
|   } | ||||
|   </style> | ||||
|    | ||||
|  | @ -1,6 +1,5 @@ | |||
| <template> | ||||
|   <div class="login-page"> | ||||
|     <!-- Background Elements --> | ||||
|     <div class="login-bg"> | ||||
|       <div class="floating-elements"> | ||||
|         <div  | ||||
|  | @ -12,10 +11,8 @@ | |||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <!-- Login Container --> | ||||
|     <div class="login-container"> | ||||
|       <div class="login-card"> | ||||
|         <!-- Logo/Header --> | ||||
|         <div class="login-header"> | ||||
|           <div class="logo"> | ||||
|             Ride<span class="logo-accent">Aware</span> | ||||
|  | @ -24,7 +21,6 @@ | |||
|           <p class="login-subtitle">Sign in to continue your cycling journey</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Login Form --> | ||||
|         <form @submit.prevent="login" class="login-form"> | ||||
|           <div class="form-group"> | ||||
|             <label for="username" class="form-label">Username</label> | ||||
|  | @ -77,14 +73,18 @@ | |||
|           <button  | ||||
|             type="submit"  | ||||
|             class="login-button" | ||||
|             :class="{ 'loading': isLoading }" | ||||
|             :class="{ 'loading': isLoading, 'success': loginSuccess }" | ||||
|             :disabled="isLoading" | ||||
|           > | ||||
|             <span v-if="!isLoading">Sign In</span> | ||||
|             <span v-else class="loading-text"> | ||||
|             <span v-if="!isLoading && !loginSuccess">Sign In</span> | ||||
|             <span v-else-if="isLoading" class="loading-text"> | ||||
|               <i class="fas fa-spinner fa-spin"></i> | ||||
|               Signing In... | ||||
|             </span> | ||||
|             <span v-else-if="loginSuccess" class="success-text"> | ||||
|               <i class="fas fa-check"></i> | ||||
|               Success! Redirecting... | ||||
|             </span> | ||||
|           </button> | ||||
| 
 | ||||
|           <div class="divider"> | ||||
|  | @ -102,13 +102,16 @@ | |||
|           </button> | ||||
|         </form> | ||||
| 
 | ||||
|         <!-- Error Message --> | ||||
|         <div v-if="error" class="error-message"> | ||||
|           <i class="fas fa-exclamation-triangle"></i> | ||||
|           {{ error }} | ||||
|         </div> | ||||
| 
 | ||||
|         <!-- Sign Up Link --> | ||||
|         <div v-if="loginSuccess" class="success-message"> | ||||
|           <i class="fas fa-check-circle"></i> | ||||
|           Login successful! Redirecting to homepage... | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="signup-prompt"> | ||||
|           <p>Don't have an account? <a href="/signup" class="signup-link">Sign up</a></p> | ||||
|         </div> | ||||
|  | @ -129,6 +132,7 @@ export default { | |||
|       password: '', | ||||
|       error: null, | ||||
|       isLoading: false, | ||||
|       loginSuccess: false, | ||||
|       showPassword: false, | ||||
|       rememberMe: false, | ||||
|       floatingElements: [] | ||||
|  | @ -146,26 +150,35 @@ export default { | |||
| 
 | ||||
|       this.isLoading = true; | ||||
|       this.error = null; | ||||
|       this.loginSuccess = false; | ||||
| 
 | ||||
|       try { | ||||
|         const response = await axios.post('http://127.0.0.1:5000/login', { | ||||
|         const response = await axios.post('http://localhost:5000/auth/login', { | ||||
|           username: this.username, | ||||
|           password: this.password, | ||||
|         }); | ||||
|          | ||||
|         console.log('Login successful:', response.data); | ||||
|          | ||||
|         // Store user data if remember me is checked | ||||
|         const userData = { | ||||
|           user_id: response.data.user_id, | ||||
|           username: this.username, | ||||
|           loginTime: new Date().toISOString() | ||||
|         }; | ||||
| 
 | ||||
|         if (this.rememberMe) { | ||||
|           localStorage.setItem('user', JSON.stringify(response.data)); | ||||
|           localStorage.setItem('user', JSON.stringify(userData)); | ||||
|           localStorage.setItem('authToken', 'authenticated'); | ||||
|         } else { | ||||
|           sessionStorage.setItem('user', JSON.stringify(response.data)); | ||||
|           sessionStorage.setItem('user', JSON.stringify(userData)); | ||||
|           sessionStorage.setItem('authToken', 'authenticated'); | ||||
|         } | ||||
|          | ||||
|         // Add success animation before redirect | ||||
|         this.loginSuccess = true; | ||||
|          | ||||
|         setTimeout(() => { | ||||
|           this.$router.push('/logged-in'); | ||||
|         }, 500); | ||||
|           this.$router.push('/'); | ||||
|         }, 1500); | ||||
|          | ||||
|       } catch (error) { | ||||
|         console.error('Login failed:', error.response?.data); | ||||
|  |  | |||
							
								
								
									
										344
									
								
								src/components/UserSignup.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										344
									
								
								src/components/UserSignup.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,344 @@ | |||
| <template> | ||||
|   <div class="signup-page"> | ||||
|     <div class="signup-bg"> | ||||
|       <div class="floating-elements"> | ||||
|         <div  | ||||
|           v-for="(element, index) in floatingElements"  | ||||
|           :key="index" | ||||
|           class="floating-element" | ||||
|           :style="element.style"> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="signup-container"> | ||||
|       <div class="signup-card"> | ||||
|         <div class="signup-header"> | ||||
|           <div class="logo"> | ||||
|             Ride<span class="logo-accent">Aware</span> | ||||
|           </div> | ||||
|           <h2 class="signup-title">Join RideAware</h2> | ||||
|           <p class="signup-subtitle">Create your account to start tracking your cycling journey</p> | ||||
|         </div> | ||||
| 
 | ||||
|         <form @submit.prevent="signup" class="signup-form"> | ||||
|           <div class="form-row"> | ||||
|             <div class="form-group half-width"> | ||||
|               <label for="firstName" class="form-label">First Name</label> | ||||
|               <div class="input-wrapper"> | ||||
|                 <i class="fas fa-user input-icon"></i> | ||||
|                 <input  | ||||
|                   type="text"  | ||||
|                   id="firstName"  | ||||
|                   v-model="firstName"  | ||||
|                   class="form-input" | ||||
|                   :class="{ 'error': error && !firstName }" | ||||
|                   placeholder="First name" | ||||
|                   required  | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="form-group half-width"> | ||||
|               <label for="lastName" class="form-label">Last Name</label> | ||||
|               <div class="input-wrapper"> | ||||
|                 <i class="fas fa-user input-icon"></i> | ||||
|                 <input  | ||||
|                   type="text"  | ||||
|                   id="lastName"  | ||||
|                   v-model="lastName"  | ||||
|                   class="form-input" | ||||
|                   :class="{ 'error': error && !lastName }" | ||||
|                   placeholder="Last name" | ||||
|                   required  | ||||
|                 /> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-group"> | ||||
|             <label for="username" class="form-label">Username</label> | ||||
|             <div class="input-wrapper"> | ||||
|               <i class="fas fa-at input-icon"></i> | ||||
|               <input  | ||||
|                 type="text"  | ||||
|                 id="username"  | ||||
|                 v-model="username"  | ||||
|                 class="form-input" | ||||
|                 :class="{ 'error': error && !username }" | ||||
|                 placeholder="Choose a username" | ||||
|                 required  | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-group"> | ||||
|             <label for="email" class="form-label">Email</label> | ||||
|             <div class="input-wrapper"> | ||||
|               <i class="fas fa-envelope input-icon"></i> | ||||
|               <input  | ||||
|                 type="email"  | ||||
|                 id="email"  | ||||
|                 v-model="email"  | ||||
|                 class="form-input" | ||||
|                 :class="{ 'error': error && !email }" | ||||
|                 placeholder="Enter your email" | ||||
|                 required  | ||||
|               /> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-group"> | ||||
|             <label for="password" class="form-label">Password</label> | ||||
|             <div class="input-wrapper"> | ||||
|               <i class="fas fa-lock input-icon"></i> | ||||
|               <input  | ||||
|                 :type="showPassword ? 'text' : 'password'"  | ||||
|                 id="password"  | ||||
|                 v-model="password"  | ||||
|                 class="form-input" | ||||
|                 :class="{ 'error': error && !password }" | ||||
|                 placeholder="Create a password" | ||||
|                 required  | ||||
|               /> | ||||
|               <button  | ||||
|                 type="button"  | ||||
|                 class="password-toggle" | ||||
|                 @click="showPassword = !showPassword" | ||||
|               > | ||||
|                 <i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i> | ||||
|               </button> | ||||
|             </div> | ||||
|             <div class="password-strength" v-if="password"> | ||||
|               <div class="strength-bar"> | ||||
|                 <div  | ||||
|                   class="strength-fill"  | ||||
|                   :class="passwordStrength.class" | ||||
|                   :style="{ width: passwordStrength.width }" | ||||
|                 ></div> | ||||
|               </div> | ||||
|               <span class="strength-text" :class="passwordStrength.class"> | ||||
|                 {{ passwordStrength.text }} | ||||
|               </span> | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-group"> | ||||
|             <label for="confirmPassword" class="form-label">Confirm Password</label> | ||||
|             <div class="input-wrapper"> | ||||
|               <i class="fas fa-lock input-icon"></i> | ||||
|               <input  | ||||
|                 :type="showConfirmPassword ? 'text' : 'password'"  | ||||
|                 id="confirmPassword"  | ||||
|                 v-model="confirmPassword"  | ||||
|                 class="form-input" | ||||
|                 :class="{ 'error': error && !confirmPassword || passwordMismatch }" | ||||
|                 placeholder="Confirm your password" | ||||
|                 required  | ||||
|               /> | ||||
|               <button  | ||||
|                 type="button"  | ||||
|                 class="password-toggle" | ||||
|                 @click="showConfirmPassword = !showConfirmPassword" | ||||
|               > | ||||
|                 <i :class="showConfirmPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i> | ||||
|               </button> | ||||
|             </div> | ||||
|             <div v-if="passwordMismatch" class="password-mismatch"> | ||||
|               <i class="fas fa-exclamation-triangle"></i> | ||||
|               Passwords do not match | ||||
|             </div> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-options"> | ||||
|             <label class="checkbox-wrapper"> | ||||
|               <input type="checkbox" v-model="agreeToTerms" class="checkbox-input" required> | ||||
|               <span class="checkbox-custom"></span> | ||||
|               <span class="checkbox-label"> | ||||
|                 I agree to the <a href="/terms" class="link">Terms of Service</a>  | ||||
|                 and <a href="/privacy" class="link">Privacy Policy</a> | ||||
|               </span> | ||||
|             </label> | ||||
|           </div> | ||||
| 
 | ||||
|           <div class="form-options"> | ||||
|             <label class="checkbox-wrapper"> | ||||
|               <input type="checkbox" v-model="subscribeNewsletter" class="checkbox-input"> | ||||
|               <span class="checkbox-custom"></span> | ||||
|               <span class="checkbox-label"> | ||||
|                 Subscribe to newsletter for cycling tips and updates | ||||
|               </span> | ||||
|             </label> | ||||
|           </div> | ||||
| 
 | ||||
|           <button  | ||||
|             type="submit"  | ||||
|             class="signup-button" | ||||
|             :class="{ 'loading': isLoading }" | ||||
|             :disabled="isLoading || !isFormValid" | ||||
|           > | ||||
|             <span v-if="!isLoading">Create Account</span> | ||||
|             <span v-else class="loading-text"> | ||||
|               <i class="fas fa-spinner fa-spin"></i> | ||||
|               Creating Account... | ||||
|             </span> | ||||
|           </button> | ||||
| 
 | ||||
|           <div class="divider"> | ||||
|             <span>or</span> | ||||
|           </div> | ||||
| 
 | ||||
|           <button type="button" class="social-signup google"> | ||||
|             <i class="fab fa-google"></i> | ||||
|             Sign up with Google | ||||
|           </button> | ||||
| 
 | ||||
|           <button type="button" class="social-signup github"> | ||||
|             <i class="fab fa-github"></i> | ||||
|             Sign up with GitHub | ||||
|           </button> | ||||
|         </form> | ||||
| 
 | ||||
|         <div v-if="error" class="error-message"> | ||||
|           <i class="fas fa-exclamation-triangle"></i> | ||||
|           {{ error }} | ||||
|         </div> | ||||
| 
 | ||||
|         <div v-if="successMessage" class="success-message"> | ||||
|           <i class="fas fa-check-circle"></i> | ||||
|           {{ successMessage }} | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="login-prompt"> | ||||
|           <p>Already have an account? <a href="/login" class="login-link">Sign in</a></p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| import axios from 'axios'; | ||||
| import '@/assets/css/components/signup.css'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'UserSignup', | ||||
|   data() { | ||||
|     return { | ||||
|       firstName: '', | ||||
|       lastName: '', | ||||
|       username: '', | ||||
|       email: '', | ||||
|       password: '', | ||||
|       confirmPassword: '', | ||||
|       error: null, | ||||
|       successMessage: null, | ||||
|       isLoading: false, | ||||
|       showPassword: false, | ||||
|       showConfirmPassword: false, | ||||
|       agreeToTerms: false, | ||||
|       subscribeNewsletter: false, | ||||
|       floatingElements: [] | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     passwordMismatch() { | ||||
|       return this.confirmPassword && this.password !== this.confirmPassword; | ||||
|     }, | ||||
|     passwordStrength() { | ||||
|       if (!this.password) return { width: '0%', class: '', text: '' }; | ||||
|        | ||||
|       let score = 0; | ||||
|       let feedback = []; | ||||
|        | ||||
|       if (this.password.length >= 8) score += 1; | ||||
|       else feedback.push('at least 8 characters'); | ||||
|        | ||||
|       if (/[A-Z]/.test(this.password)) score += 1; | ||||
|       else feedback.push('uppercase letter'); | ||||
|        | ||||
|       if (/[a-z]/.test(this.password)) score += 1; | ||||
|       else feedback.push('lowercase letter'); | ||||
| 
 | ||||
|       if (/\d/.test(this.password)) score += 1; | ||||
|       else feedback.push('number'); | ||||
|        | ||||
|       if (/[!@#$%^&*(),.?":{}|<>]/.test(this.password)) score += 1; | ||||
|       else feedback.push('special character'); | ||||
|        | ||||
|       if (score < 2) { | ||||
|         return { width: '25%', class: 'weak', text: 'Weak' }; | ||||
|       } else if (score < 4) { | ||||
|         return { width: '50%', class: 'fair', text: 'Fair' }; | ||||
|       } else if (score < 5) { | ||||
|         return { width: '75%', class: 'good', text: 'Good' }; | ||||
|       } else { | ||||
|         return { width: '100%', class: 'strong', text: 'Strong' }; | ||||
|       } | ||||
|     }, | ||||
|     isFormValid() { | ||||
|       return this.firstName &&  | ||||
|              this.lastName &&  | ||||
|              this.username &&  | ||||
|              this.email &&  | ||||
|              this.password &&  | ||||
|              this.confirmPassword &&  | ||||
|              !this.passwordMismatch &&  | ||||
|              this.agreeToTerms; | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.generateFloatingElements(); | ||||
|   }, | ||||
|   methods: { | ||||
|     async signup() { | ||||
|       if (!this.isFormValid) { | ||||
|         this.error = 'Please fill in all required fields correctly'; | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       this.isLoading = true; | ||||
|       this.error = null; | ||||
|       this.successMessage = null; | ||||
| 
 | ||||
|       try { | ||||
|         const response = await axios.post('http://localhost:5000/auth/signup', { | ||||
|           firstName: this.firstName, | ||||
|           lastName: this.lastName, | ||||
|           username: this.username, | ||||
|           email: this.email, | ||||
|           password: this.password, | ||||
|           subscribeNewsletter: this.subscribeNewsletter | ||||
|         }); | ||||
|          | ||||
|         console.log('Signup successful:', response.data); | ||||
|          | ||||
|         this.successMessage = 'Account created successfully! Please check your email to verify your account.'; | ||||
|          | ||||
|         setTimeout(() => { | ||||
|           this.$router.push('/login'); | ||||
|         }, 2000); | ||||
|          | ||||
|       } catch (error) { | ||||
|         console.error('Signup failed:', error.response?.data); | ||||
|         this.error = error.response?.data?.error || 'Account creation failed. Please try again.'; | ||||
|       } finally { | ||||
|         this.isLoading = false; | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|     generateFloatingElements() { | ||||
|       for (let i = 0; i < 12; i++) { | ||||
|         this.floatingElements.push({ | ||||
|           style: { | ||||
|             left: Math.random() * 100 + '%', | ||||
|             top: Math.random() * 100 + '%', | ||||
|             animationDelay: Math.random() * 8 + 's', | ||||
|             animationDuration: (6 + Math.random() * 4) + 's' | ||||
|           } | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
|  | @ -1,43 +1,46 @@ | |||
| import { createRouter, createWebHistory } from 'vue-router'; | ||||
| import { createRouter, createWebHistory } from 'vue-router' | ||||
| 
 | ||||
| // Lazy load components for better performance
 | ||||
| const routes = [ | ||||
|   {  | ||||
|     path: '/',  | ||||
|     name: 'Home', | ||||
|     component: () => import('../components/Home.vue')  | ||||
|     component: () => import('../components/Home.vue'), | ||||
|     meta: { title: 'RideAware – Home' }  | ||||
|   }, | ||||
|   {  | ||||
|     path: '/login',  | ||||
|     name: 'Login', | ||||
|     component: () => import('../components/UserLogin.vue')  | ||||
|     component: () => import('../components/UserLogin.vue'), | ||||
|     meta: { title: 'RideAware – Sign In' }  | ||||
|   }, | ||||
|   {  | ||||
|     path: '/logged-in',  | ||||
|     name: 'LoggedIn', | ||||
|     component: () => import('../components/LoggedinPage.vue'), | ||||
|     // Optional: Add route guard to check if user is actually logged in
 | ||||
|     meta: { requiresAuth: true } | ||||
|   } | ||||
| ]; | ||||
|   { | ||||
|     path: '/signup', | ||||
|     name: 'SignUp', | ||||
|     component: () => import('../components/UserSignup.vue'), | ||||
|     meta: { title: 'RideAware – Sign Up' } | ||||
|   }, | ||||
|    | ||||
| ] | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|   history: createWebHistory(), | ||||
|   routes, | ||||
| }); | ||||
| }) | ||||
| 
 | ||||
| // Optional: Navigation guard example
 | ||||
| router.beforeEach((to, from, next) => { | ||||
|   // Check if route requires authentication
 | ||||
|   if (to.meta.requiresAuth) { | ||||
|     // Check if user is logged in (implement your auth logic)
 | ||||
|     const isLoggedIn = localStorage.getItem('user') || sessionStorage.getItem('user'); | ||||
|     const isLoggedIn = !!(localStorage.getItem('user') || sessionStorage.getItem('user')) | ||||
|     if (!isLoggedIn) { | ||||
|       next('/login'); | ||||
|       return; | ||||
|       next({ name: 'Login' }) | ||||
|       return | ||||
|     } | ||||
|   } | ||||
|   next(); | ||||
| }); | ||||
|   next() | ||||
| }) | ||||
| 
 | ||||
| export default router; | ||||
| router.afterEach((to) => { | ||||
|   const defaultTitle = 'RideAware' | ||||
|   document.title = to.meta.title || defaultTitle | ||||
| }) | ||||
| 
 | ||||
| export default router | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Cipher Vance
						Cipher Vance