Changeset 3ebe47c
- Timestamp:
- 02/09/26 21:36:40 (5 months ago)
- Branches:
- master
- Children:
- 8615885
- Parents:
- 140d098
- Files:
-
- 4 added
- 1 deleted
- 20 edited
-
backend/env (deleted)
-
backend/pom.xml (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/BackendApplication.java (modified) (3 diffs)
-
backend/src/main/java/com/trekr/backend/config/SecurityConfig.java (modified) (3 diffs)
-
backend/src/main/java/com/trekr/backend/controller/AuthController.java (modified) (3 diffs)
-
backend/src/main/java/com/trekr/backend/dto/auth/AuthResponse.java (modified) (2 diffs)
-
backend/src/main/java/com/trekr/backend/dto/auth/LoginRequest.java (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/dto/auth/RegisterRequest.java (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/entity/User.java (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/resources/application.properties (modified) (1 diff)
-
backend/src/main/java/com/trekr/backend/security/UserPrincipal.java (modified) (2 diffs)
-
backend/src/main/java/com/trekr/backend/service/AuthService.java (modified) (3 diffs)
-
backend/src/main/resources/application.properties (modified) (1 diff)
-
backend/src/test/resources/application.properties (added)
-
backend/src/test/resources/schema.sql (added)
-
db-scripts/ddl.sql (modified) (1 diff)
-
db-scripts/dml.sql (modified) (1 diff)
-
frontend/src/App.jsx (modified) (1 diff)
-
frontend/src/api/axios.js (modified) (1 diff)
-
frontend/src/main.jsx (modified) (1 diff)
-
frontend/src/pages/Dashboard.jsx (modified) (1 diff)
-
frontend/src/pages/LandingPage/components/Hero.jsx (modified) (2 diffs)
-
frontend/src/pages/LandingPage/components/NavbarLanding.jsx (modified) (1 diff)
-
frontend/src/pages/Login/Login.jsx (added)
-
frontend/src/pages/Register/Register.jsx (added)
Legend:
- Unmodified
- Added
- Removed
-
backend/pom.xml
r140d098 r3ebe47c 99 99 <scope>test</scope> 100 100 </dependency> 101 102 <dependency> 103 <groupId>com.h2database</groupId> 104 <artifactId>h2</artifactId> 105 <scope>test</scope> 106 </dependency> 101 107 </dependencies> 102 108 -
backend/src/main/java/com/trekr/backend/BackendApplication.java
r140d098 r3ebe47c 12 12 // This ensures system properties are available for ${} placeholder resolution 13 13 Dotenv dotenv = loadEnvironmentVariables(); 14 14 15 15 // Create SpringApplication instance 16 16 SpringApplication app = new SpringApplication(BackendApplication.class); 17 17 18 18 // Set system properties and default properties from .env file 19 19 if (dotenv != null) { 20 20 java.util.Map<String, Object> defaultProps = new java.util.HashMap<>(); 21 21 22 22 dotenv.entries().forEach(entry -> { 23 23 String key = entry.getKey(); … … 30 30 } 31 31 }); 32 32 33 33 // Set all default properties at once 34 34 if (!defaultProps.isEmpty()) { … … 36 36 } 37 37 } 38 38 39 39 app.run(args); 40 40 } 41 41 42 42 private static Dotenv loadEnvironmentVariables() { 43 43 Dotenv dotenv = null; 44 try { 45 // Try current directory (backend/) 46 dotenv = Dotenv.configure() 47 .directory("./") 48 .ignoreIfMissing() 49 .load(); 50 } catch (Exception e) { 51 // Try parent directory if current doesn't work 44 45 // Prefer a standard ".env" file, but also support the existing "env" file. 46 // We search multiple locations so it works whether the app is launched from: 47 // - backend/ (common in IDE) 48 // - repo root (common when running a built JAR) 49 String[][] candidates = new String[][] { 50 { "./", ".env" }, 51 { "./", "env" }, 52 { "./backend", ".env" }, 53 { "./backend", "env" }, 54 { "../", ".env" }, 55 { "../", "env" }, 56 { "../backend", ".env" }, 57 { "../backend", "env" }, 58 }; 59 60 for (String[] candidate : candidates) { 52 61 try { 53 dotenv = Dotenv.configure() 54 .directory("../") 62 Dotenv loaded = Dotenv.configure() 63 .directory(candidate[0]) 64 .filename(candidate[1]) 55 65 .ignoreIfMissing() 56 66 .load(); 57 } catch (Exception e2) { 58 System.err.println("WARNING: Could not load .env file. Make sure .env exists in backend/ directory."); 59 System.err.println("Error: " + e2.getMessage()); 60 return null; 67 if (loaded != null && !loaded.entries().isEmpty()) { 68 dotenv = loaded; 69 break; 70 } 71 } catch (Exception ignored) { 72 // keep searching 61 73 } 62 74 } 63 64 // Verify critical properties are loaded 65 if (dotenv != null) { 66 String url = dotenv.get("SPRING_DATASOURCE_URL"); 67 String username = dotenv.get("SPRING_DATASOURCE_USERNAME"); 68 String password = dotenv.get("SPRING_DATASOURCE_PASSWORD"); 69 70 if (url != null && !url.trim().isEmpty()) { 71 System.out.println("✓ Database URL loaded from .env"); 72 } else { 73 System.err.println("✗ ERROR: SPRING_DATASOURCE_URL not found in .env file!"); 74 } 75 if (username != null && !username.trim().isEmpty()) { 76 System.out.println("✓ Database username loaded from .env"); 77 } else { 78 System.err.println("✗ ERROR: SPRING_DATASOURCE_USERNAME not found in .env file!"); 79 } 80 if (password != null && !password.trim().isEmpty()) { 81 System.out.println("✓ Database password loaded from .env"); 82 } else { 83 System.err.println("✗ ERROR: SPRING_DATASOURCE_PASSWORD not found in .env file!"); 84 } 85 } 86 75 87 76 return dotenv; 88 77 } -
backend/src/main/java/com/trekr/backend/config/SecurityConfig.java
r140d098 r3ebe47c 3 3 import org.springframework.context.annotation.Bean; 4 4 import org.springframework.context.annotation.Configuration; 5 import org.springframework.security.config.Customizer; 5 6 import org.springframework.security.config.annotation.web.builders.HttpSecurity; 6 7 import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; … … 8 9 import org.springframework.security.crypto.password.PasswordEncoder; 9 10 import org.springframework.security.web.SecurityFilterChain; 11 import org.springframework.web.cors.CorsConfiguration; 12 import org.springframework.web.cors.CorsConfigurationSource; 13 import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 14 15 import java.util.List; 10 16 11 17 @Configuration … … 17 23 http 18 24 .csrf(AbstractHttpConfigurer::disable) 25 .cors(Customizer.withDefaults()) 19 26 .authorizeHttpRequests(auth -> auth 20 27 .requestMatchers("/api/auth/**").permitAll() 21 .anyRequest().authenticated() 22 ); 28 .anyRequest().authenticated()); 23 29 return http.build(); 30 } 31 32 @Bean 33 public CorsConfigurationSource corsConfigurationSource() { 34 CorsConfiguration config = new CorsConfiguration(); 35 config.setAllowedOrigins(List.of( 36 "http://localhost:5173", 37 "http://127.0.0.1:5173")); 38 config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); 39 config.setAllowedHeaders(List.of("Authorization", "Content-Type")); 40 41 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 42 source.registerCorsConfiguration("/**", config); 43 return source; 24 44 } 25 45 -
backend/src/main/java/com/trekr/backend/controller/AuthController.java
r140d098 r3ebe47c 6 6 import com.trekr.backend.service.AuthService; 7 7 import jakarta.validation.Valid; 8 import lombok.RequiredArgsConstructor;9 8 import org.springframework.http.HttpStatus; 10 9 import org.springframework.http.ResponseEntity; … … 13 12 @RestController 14 13 @RequestMapping("/api/auth") 15 @RequiredArgsConstructor16 14 public class AuthController { 17 15 18 16 private final AuthService authService; 19 17 18 public AuthController(AuthService authService) { 19 this.authService = authService; 20 } 21 20 22 @PostMapping("/register") 21 23 public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest request) { … … 23 25 return ResponseEntity.status(HttpStatus.CREATED).body(response); 24 26 } 25 27 26 28 @PostMapping("/login") 27 29 public ResponseEntity<AuthResponse> login(@Valid @RequestBody LoginRequest request) { -
backend/src/main/java/com/trekr/backend/dto/auth/AuthResponse.java
r140d098 r3ebe47c 1 1 package com.trekr.backend.dto.auth; 2 2 3 import lombok.AllArgsConstructor;4 import lombok.Data;5 import lombok.NoArgsConstructor;6 7 @Data8 @NoArgsConstructor9 @AllArgsConstructor10 3 public class AuthResponse { 11 4 private String token; … … 14 7 private String username; 15 8 private String email; 9 10 public AuthResponse() { 11 } 12 13 public AuthResponse(String token, String type, Long userId, String username, String email) { 14 this.token = token; 15 this.type = type; 16 this.userId = userId; 17 this.username = username; 18 this.email = email; 19 } 20 21 public String getToken() { 22 return token; 23 } 24 25 public void setToken(String token) { 26 this.token = token; 27 } 28 29 public String getType() { 30 return type; 31 } 32 33 public void setType(String type) { 34 this.type = type; 35 } 36 37 public Long getUserId() { 38 return userId; 39 } 40 41 public void setUserId(Long userId) { 42 this.userId = userId; 43 } 44 45 public String getUsername() { 46 return username; 47 } 48 49 public void setUsername(String username) { 50 this.username = username; 51 } 52 53 public String getEmail() { 54 return email; 55 } 56 57 public void setEmail(String email) { 58 this.email = email; 59 } 16 60 } -
backend/src/main/java/com/trekr/backend/dto/auth/LoginRequest.java
r140d098 r3ebe47c 2 2 3 3 import jakarta.validation.constraints.NotBlank; 4 import lombok.Data;5 4 6 @Data7 5 public class LoginRequest { 8 6 9 7 @NotBlank(message = "Username or email is required") 10 8 private String usernameOrEmail; 11 9 12 10 @NotBlank(message = "Password is required") 13 11 private String password; 12 13 public LoginRequest() { 14 } 15 16 public String getUsernameOrEmail() { 17 return usernameOrEmail; 18 } 19 20 public void setUsernameOrEmail(String usernameOrEmail) { 21 this.usernameOrEmail = usernameOrEmail; 22 } 23 24 public String getPassword() { 25 return password; 26 } 27 28 public void setPassword(String password) { 29 this.password = password; 30 } 14 31 } -
backend/src/main/java/com/trekr/backend/dto/auth/RegisterRequest.java
r140d098 r3ebe47c 4 4 import jakarta.validation.constraints.NotBlank; 5 5 import jakarta.validation.constraints.Size; 6 import lombok.Data;7 6 8 @Data9 7 public class RegisterRequest { 10 8 11 9 @NotBlank(message = "Email is required") 12 10 @Email(message = "Email must be valid") 13 11 private String email; 14 12 15 13 @NotBlank(message = "Username is required") 16 14 @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") 17 15 private String username; 18 16 19 17 @NotBlank(message = "Password is required") 20 18 @Size(min = 6, message = "Password must be at least 6 characters") 21 19 private String password; 20 21 public RegisterRequest() { 22 } 23 24 public String getEmail() { 25 return email; 26 } 27 28 public void setEmail(String email) { 29 this.email = email; 30 } 31 32 public String getUsername() { 33 return username; 34 } 35 36 public void setUsername(String username) { 37 this.username = username; 38 } 39 40 public String getPassword() { 41 return password; 42 } 43 44 public void setPassword(String password) { 45 this.password = password; 46 } 22 47 } -
backend/src/main/java/com/trekr/backend/entity/User.java
r140d098 r3ebe47c 2 2 3 3 import jakarta.persistence.*; 4 import lombok.AllArgsConstructor;5 import lombok.Getter;6 import lombok.NoArgsConstructor;7 import lombok.Setter;8 4 9 5 @Entity 10 6 @Table(name = "USERS", schema = "trekr") 11 @Getter12 @Setter13 @NoArgsConstructor14 @AllArgsConstructor15 7 public class User { 16 8 17 9 @Id 18 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "user_seq") 19 @SequenceGenerator(name = "user_seq", sequenceName = "user_id_seq", allocationSize = 1) 10 @GeneratedValue(strategy = GenerationType.IDENTITY) 20 11 @Column(name = "user_id") 21 12 private Long userId; 22 13 23 14 @Column(name = "email", nullable = false, unique = true) 24 15 private String email; 25 16 26 17 @Column(name = "username", nullable = false, unique = true) 27 18 private String username; 28 19 29 20 @Column(name = "password", nullable = false) 30 21 private String password; 22 23 public User() { 24 } 25 26 public User(Long userId, String email, String username, String password) { 27 this.userId = userId; 28 this.email = email; 29 this.username = username; 30 this.password = password; 31 } 32 33 public Long getUserId() { 34 return userId; 35 } 36 37 public void setUserId(Long userId) { 38 this.userId = userId; 39 } 40 41 public String getEmail() { 42 return email; 43 } 44 45 public void setEmail(String email) { 46 this.email = email; 47 } 48 49 public String getUsername() { 50 return username; 51 } 52 53 public void setUsername(String username) { 54 this.username = username; 55 } 56 57 public String getPassword() { 58 return password; 59 } 60 61 public void setPassword(String password) { 62 this.password = password; 63 } 31 64 } -
backend/src/main/java/com/trekr/backend/resources/application.properties
r140d098 r3ebe47c 1 # application.properties 1 ### NOTE 2 ### This file is intentionally NOT used by Spring Boot. 3 ### The active config is in: src/main/resources/application.properties 2 4 3 # General app config4 spring.application.name=trekr5 6 #spring.datasource.hikari.data-source-properties.useServerPrepStmts=false7 #spring.datasource.hikari.data-source-properties.prepareThreshold=08 9 logging.level.org.springframework.security=DEBUG10 11 # JPA & Hibernate12 spring.jpa.hibernate.ddl-auto=update13 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect14 spring.jpa.show-sql=false15 spring.jpa.properties.hibernate.format_sql=true16 spring.jpa.properties.hibernate.default_schema=trekr17 18 #logging.level.org.hibernate.SQL=DEBUG19 #logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE20 21 22 # HikariCP settings23 spring.datasource.hikari.pool-name=trekrHikariPool24 25 # Maximum connections in the pool26 # Rule of thumb: (CPU cores × 2) + effective_spindle_count27 # For most apps: 10-20 is good28 spring.datasource.hikari.maximum-pool-size=2029 30 # Minimum idle connections always ready31 # Keep at least 5 ready for quick response32 spring.datasource.hikari.minimum-idle=533 34 # How long to wait for a connection (before blocking the request) - 30sec35 spring.datasource.hikari.connection-timeout=3000036 37 # How long a connection can sit idle (before being closed) - 10min38 spring.datasource.hikari.idle-timeout=60000039 40 # Maximum lifetime of a connection (before being replaced with a new one) - 30min41 # Prevents stale connections42 spring.datasource.hikari.max-lifetime=180000043 44 45 # JWT Configuration (supports JWT_SECRET or JWT_CONFIG_SECRET from .env)46 jwt.secret=${JWT_SECRET:${JWT_CONFIG_SECRET:dev-fallback-secret-min-32-chars-required}}47 48 # Base API path49 api.base.path=/api50 api.admin.path=/api/admin51 52 #spring.profiles.active=dev53 54 # Max file size55 server.tomcat.max-part-count=5056 57 logging.level.org.springframework.web=DEBUG58 logging.level.org.apache.coyote.http11=DEBUG59 60 61 # PostgreSQL connection62 spring.datasource.driver-class-name=org.postgresql.Driver63 spring.datasource.url=${SPRING_DATASOURCE_URL}64 spring.datasource.username=${SPRING_DATASOURCE_USERNAME}65 spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}66 -
backend/src/main/java/com/trekr/backend/security/UserPrincipal.java
r140d098 r3ebe47c 2 2 3 3 import lombok.Getter; 4 import lombok.RequiredArgsConstructor;5 4 6 5 import java.io.Serializable; … … 11 10 */ 12 11 @Getter 13 @RequiredArgsConstructor14 12 public class UserPrincipal implements Serializable { 13 private static final long serialVersionUID = 1L; 14 15 15 private final Long userId; 16 16 private final String username; 17 17 private final String email; 18 19 public UserPrincipal(Long userId, String username, String email) { 20 this.userId = userId; 21 this.username = username; 22 this.email = email; 23 } 18 24 } -
backend/src/main/java/com/trekr/backend/service/AuthService.java
r140d098 r3ebe47c 6 6 import com.trekr.backend.entity.User; 7 7 import com.trekr.backend.repository.UserRepository; 8 import lombok.RequiredArgsConstructor;9 8 import org.springframework.security.crypto.password.PasswordEncoder; 10 9 import org.springframework.stereotype.Service; … … 12 11 13 12 @Service 14 @RequiredArgsConstructor15 13 public class AuthService { 16 14 … … 18 16 private final PasswordEncoder passwordEncoder; 19 17 private final JwtService jwtService; 18 19 public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService) { 20 this.userRepository = userRepository; 21 this.passwordEncoder = passwordEncoder; 22 this.jwtService = jwtService; 23 } 20 24 21 25 @Transactional -
backend/src/main/resources/application.properties
r140d098 r3ebe47c 1 spring.application.name=backend 1 spring.application.name=trekr 2 3 # ============================ 4 # JPA & Hibernate 5 # ============================ 6 spring.jpa.hibernate.ddl-auto=update 7 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect 8 spring.jpa.show-sql=false 9 spring.jpa.properties.hibernate.format_sql=true 10 spring.jpa.properties.hibernate.default_schema=trekr 11 spring.jpa.properties.hibernate.hbm2ddl.create_namespaces=true 12 13 # ============================ 14 # PostgreSQL connection (loaded from backend/.env or backend/env) 15 # ============================ 16 spring.datasource.driver-class-name=org.postgresql.Driver 17 spring.datasource.url=${SPRING_DATASOURCE_URL} 18 spring.datasource.username=${SPRING_DATASOURCE_USERNAME} 19 spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} 20 21 # ============================ 22 # JWT Configuration (supports JWT_SECRET or JWT_CONFIG_SECRET) 23 # ============================ 24 jwt.secret=${JWT_SECRET:${JWT_CONFIG_SECRET:dev-fallback-secret-min-32-chars-required}} 25 26 # ============================ 27 # Logging 28 # ============================ 29 logging.level.org.springframework.security=DEBUG 30 31 # ============================ 32 # HikariCP 33 # ============================ 34 spring.datasource.hikari.pool-name=trekrHikariPool 35 spring.datasource.hikari.maximum-pool-size=20 36 spring.datasource.hikari.minimum-idle=5 37 spring.datasource.hikari.connection-timeout=30000 38 spring.datasource.hikari.idle-timeout=600000 39 spring.datasource.hikari.max-lifetime=1800000 40 41 # Max multipart parts 42 server.tomcat.max-part-count=50 -
db-scripts/ddl.sql
r140d098 r3ebe47c 20 20 21 21 CREATE TABLE USERS ( 22 user_id BIGINT PRIMARY KEY,22 user_id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, 23 23 email TEXT NOT NULL, 24 24 username TEXT NOT NULL, -
db-scripts/dml.sql
r140d098 r3ebe47c 32 32 (29, 'petra@example.com', 'petra_kostova', '1234'), 33 33 (30, 'vlado@example.com', 'vlado_ilievski', '1234'); 34 35 -- If USERS.user_id is an IDENTITY column, ensure its backing sequence is advanced 36 -- past the inserted dummy ids so future inserts (e.g., app registration) don't 37 -- try to reuse ids and fail with duplicate key errors. 38 SELECT setval( 39 pg_get_serial_sequence('trekr.users', 'user_id'), 40 (SELECT COALESCE(MAX(user_id), 0) FROM trekr.users) 41 ); 34 42 35 43 INSERT INTO FINANCE_USERS (user_id, spending_budget, saving_budget, investing_budget, donation_budget, credit) VALUES -
frontend/src/App.jsx
r140d098 r3ebe47c 1 import { Navigate, Route, Routes } from "react-router-dom"; 2 1 3 import LandingPage from "./pages/LandingPage/LandingPage.jsx"; 4 import Login from "./pages/Login/Login.jsx"; 5 import Register from "./pages/Register/Register.jsx"; 6 import Dashboard from "./pages/Dashboard.jsx"; 7 8 function RequireAuth({ children }) { 9 const token = localStorage.getItem("authToken"); 10 if (!token) return <Navigate to="/login" replace />; 11 return children; 12 } 2 13 3 14 function App() { 4 return <LandingPage />; 15 return ( 16 <div data-theme="forest" className="min-h-screen w-full"> 17 <Routes> 18 <Route path="/" element={<LandingPage />} /> 19 <Route path="/login" element={<Login />} /> 20 <Route path="/register" element={<Register />} /> 21 <Route 22 path="/dashboard" 23 element={ 24 <RequireAuth> 25 <Dashboard /> 26 </RequireAuth> 27 } 28 /> 29 <Route path="*" element={<Navigate to="/" replace />} /> 30 </Routes> 31 </div> 32 ); 5 33 } 6 34 -
frontend/src/api/axios.js
r140d098 r3ebe47c 1 1 import axios from "axios"; 2 2 3 export default axios.create({ 4 baseURL: "http://localhost:8080/api", 5 withCredentials: true, // important if using cookies 3 const api = axios.create({ 4 baseURL: import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080/api", 6 5 }); 6 7 api.interceptors.request.use((config) => { 8 const token = localStorage.getItem("authToken"); 9 if (token) { 10 config.headers = config.headers ?? {}; 11 config.headers.Authorization = `Bearer ${token}`; 12 } 13 return config; 14 }); 15 16 export default api; -
frontend/src/main.jsx
r140d098 r3ebe47c 1 import { StrictMode } from 'react' 2 import { createRoot } from 'react-dom/client' 3 import './index.css' 4 import App from './App.jsx' 1 import { StrictMode } from "react"; 2 import { createRoot } from "react-dom/client"; 3 import { BrowserRouter } from "react-router-dom"; 5 4 6 createRoot(document.getElementById('root')).render( 5 import "./index.css"; 6 import App from "./App.jsx"; 7 8 createRoot(document.getElementById("root")).render( 7 9 <StrictMode> 8 <App /> 10 <BrowserRouter> 11 <App /> 12 </BrowserRouter> 9 13 </StrictMode>, 10 ) 14 ); -
frontend/src/pages/Dashboard.jsx
r140d098 r3ebe47c 1 // hello world 2 import React from "react"; 1 import React, { useMemo } from "react"; 2 import { Link, useNavigate } from "react-router-dom"; 3 4 const Dashboard = () => { 5 const navigate = useNavigate(); 6 7 const user = useMemo(() => { 8 try { 9 const raw = localStorage.getItem("authUser"); 10 return raw ? JSON.parse(raw) : null; 11 } catch { 12 return null; 13 } 14 }, []); 15 16 const onLogout = () => { 17 localStorage.removeItem("authToken"); 18 localStorage.removeItem("authUser"); 19 navigate("/", { replace: true }); 20 }; 21 22 return ( 23 <div className="min-h-screen p-6"> 24 <div className="max-w-3xl mx-auto"> 25 <div className="flex items-center justify-between gap-3"> 26 <h1 className="text-2xl font-bold">Dashboard</h1> 27 <div className="flex items-center gap-2"> 28 <Link className="btn btn-ghost" to="/"> 29 Landing 30 </Link> 31 <button className="btn btn-outline" onClick={onLogout}> 32 Logout 33 </button> 34 </div> 35 </div> 36 37 <div className="mt-6 card bg-base-200 border border-base-300"> 38 <div className="card-body"> 39 <h2 className="card-title">Account</h2> 40 <div className="text-sm leading-7"> 41 <div> 42 <span className="opacity-70">Username:</span>{" "} 43 <span className="font-medium">{user?.username ?? "—"}</span> 44 </div> 45 <div> 46 <span className="opacity-70">Email:</span>{" "} 47 <span className="font-medium">{user?.email ?? "—"}</span> 48 </div> 49 <div> 50 <span className="opacity-70">User ID:</span>{" "} 51 <span className="font-medium">{user?.userId ?? "—"}</span> 52 </div> 53 </div> 54 </div> 55 </div> 56 </div> 57 </div> 58 ); 59 }; 60 61 export default Dashboard; -
frontend/src/pages/LandingPage/components/Hero.jsx
r140d098 r3ebe47c 1 import React from "react"; 1 import React, { useEffect, useState } from "react"; 2 import { Link } from "react-router-dom"; 2 3 3 4 const Hero = () => { 5 const [isAuthed, setIsAuthed] = useState(() => 6 Boolean(localStorage.getItem("authToken")), 7 ); 8 9 useEffect(() => { 10 const onStorage = (e) => { 11 if (e.key === "authToken") { 12 setIsAuthed(Boolean(e.newValue)); 13 } 14 }; 15 window.addEventListener("storage", onStorage); 16 return () => window.removeEventListener("storage", onStorage); 17 }, []); 18 4 19 return ( 5 20 <div … … 21 36 consistently, and see your progress in one place. 22 37 </p> 23 <button className="btn btn-primary">Get started</button> 38 <Link 39 className="btn btn-primary" 40 to={isAuthed ? "/dashboard" : "/register"} 41 > 42 {isAuthed ? "Go to dashboard" : "Get started"} 43 </Link> 24 44 </div> 25 45 </div> -
frontend/src/pages/LandingPage/components/NavbarLanding.jsx
r140d098 r3ebe47c 1 import React from "react"; 1 import React, { useEffect, useState } from "react"; 2 import { Link, useNavigate } from "react-router-dom"; 2 3 import logo from "../../../assets/logo.png"; 3 4 4 5 const NavbarLanding = () => { 6 const navigate = useNavigate(); 7 const [isAuthed, setIsAuthed] = useState(() => 8 Boolean(localStorage.getItem("authToken")), 9 ); 10 11 useEffect(() => { 12 const onStorage = (e) => { 13 if (e.key === "authToken") { 14 setIsAuthed(Boolean(e.newValue)); 15 } 16 }; 17 window.addEventListener("storage", onStorage); 18 return () => window.removeEventListener("storage", onStorage); 19 }, []); 20 21 const onLogout = () => { 22 localStorage.removeItem("authToken"); 23 localStorage.removeItem("authUser"); 24 setIsAuthed(false); 25 navigate("/", { replace: true }); 26 }; 27 5 28 return ( 6 29 <div className="navbar bg-neutral shadow-sm"> 7 30 <div className="navbar-start"> 8 < a className="btn btn-ghost" aria-label="Trekr home">31 <Link className="btn btn-ghost" aria-label="Trekr home" to="/"> 9 32 <img src={logo} alt="Trekr" className="h-12 w-auto rounded-2xl" /> 10 </ a>33 </Link> 11 34 </div> 12 35 <div className="navbar-end"> 13 <button className="btn bg-black text-green-200 border-black hover:bg-black/90 hover:border-black px-8 mr-6"> 14 Start Tracking 15 </button> 36 {isAuthed ? ( 37 <div className="flex items-center gap-2 mr-6"> 38 <Link className="btn btn-primary" to="/dashboard"> 39 Dashboard 40 </Link> 41 <button className="btn btn-outline" onClick={onLogout}> 42 Logout 43 </button> 44 </div> 45 ) : ( 46 <Link 47 className="btn bg-black text-green-200 border-black hover:bg-black/90 hover:border-black px-8 mr-6" 48 to="/login" 49 > 50 Start Tracking 51 </Link> 52 )} 16 53 </div> 17 54 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
