diff --git a/.clinerules/frontend-color-theme.md b/.clinerules/frontend-color-theme.md new file mode 100644 index 0000000..8b8b86d --- /dev/null +++ b/.clinerules/frontend-color-theme.md @@ -0,0 +1,32 @@ +## Brief overview +Project-specific frontend color theme guidelines for the vibing application. Defines a cohesive peach/coral/pink color palette with specific CSS custom properties, component styling patterns, and accessibility requirements. + +## Color palette usage +- Always use the defined peach-coral color scheme: Primary Peach (#FFCDB2), Secondary Coral (#FFB4A2), Light Pink (#E5989B), Medium Mauve (#B5838D), Dark Purple (#6D6875) +- Implement CSS custom properties in src/index.css for all color definitions +- Use semantic color naming: --color-primary, --color-secondary, --color-bg-dark, --color-text-primary, etc. +- Support both dark and light modes using @media (prefers-color-scheme: light) + +## Component styling patterns +- Buttons: Use gradient backgrounds, implement translateY(-2px) hover animations, apply consistent focus states with peach outline +- Cards: White backgrounds with subtle shadows, translateY(-8px) hover animations, 16px border radius, scale(1.05) image hover effects +- Links: Primary color #FFCDB2, hover colors #E5989B (dark mode) or #B5838D (light mode), no text decoration by default +- Use semantic CSS class names: .btn-primary, .btn-secondary, .btn-outline, .btn-ghost + +## Interactive states and animations +- Apply subtle lift animations using transform: translateY(-2px) for hover effects +- Use rgba(255, 205, 178, 0.3) for focus rings and primary shadows +- Use rgba(229, 152, 155, 0.3) for secondary shadows +- Ensure hover states are distinct from focus states for accessibility + +## File organization structure +- Place global styles and CSS custom properties in src/index.css +- Create individual .css files in src/components/ for component-specific styles +- Use src/App.css for utility classes +- Consider src/styles/ directory for additional theme files when needed + +## Accessibility requirements +- Maintain minimum contrast ratios: 4.5:1 for normal text, 3:1 for large text +- Use --color-focus for focus indicators +- Test with color blindness simulators +- Ensure all interactive elements have proper focus states diff --git a/.clinerules/git-workflow.md b/.clinerules/git-workflow.md new file mode 100644 index 0000000..87f31a0 --- /dev/null +++ b/.clinerules/git-workflow.md @@ -0,0 +1,38 @@ +## Brief overview +Project-specific git workflow guidelines emphasizing feature branch development, conventional commit messages, and proper branch management practices. Never work directly on main/master branches. + +## Branch management strategy +- Always create feature branches for all development work +- Use kebab-case naming: feature/user-authentication, bugfix/login-validation, hotfix/security-patch, refactor/api-endpoints +- Keep feature branches short-lived (1-3 days ideally) +- One feature per branch, don't mix different types of changes +- Delete branches after successful merge + +## Commit message conventions +- Use conventional commit format: type(scope): description +- Use present tense ("add feature" not "added feature") +- Be specific and descriptive in commit messages +- Reference issue numbers when applicable: feat(auth): add OAuth login #123 +- Make frequent, small commits with clear purposes + +## Development workflow steps +- Always start by ensuring main branch is up to date: git checkout main && git pull origin main +- Create new feature branch: git checkout -b feature/your-feature-name +- Before pushing, rebase with main: git fetch origin && git rebase origin/main +- Push feature branch: git push origin feature/your-feature-name +- Create Pull Request from feature branch to main +- Only merge after code review approval + +## Code review requirements +- Create PR for all changes, no direct commits to main +- Request reviews from team members +- Address all feedback before merging +- Ensure all tests pass before creating PR +- Self-review changes before requesting reviews +- Update documentation when needed + +## Emergency procedures +- For critical issues, create hotfix branches from main +- Make minimal necessary changes in hotfixes +- Test thoroughly before creating emergency PR +- Follow same review process even for hotfixes diff --git a/.gitea/workflows/backend-ci.yml b/.gitea/workflows/backend-ci.yml index e1136bc..2b464dc 100644 --- a/.gitea/workflows/backend-ci.yml +++ b/.gitea/workflows/backend-ci.yml @@ -41,4 +41,4 @@ jobs: with: name: backend-test-results path: backend/target/surefire-reports/ - retention-days: 7 \ No newline at end of file + retention-days: 7 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1936ad --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# Vibing application + +## Data model + +```mermaid +classDiagram + class Activity { + +String name + +Location location + +Int priceRange + +List~String~ tags + } +``` + diff --git a/backend/pom.xml b/backend/pom.xml index edfe43d..52dc476 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -49,8 +49,8 @@ - com.h2database - h2 + org.postgresql + postgresql runtime @@ -74,6 +74,12 @@ spring-security-test test + + + com.h2database + h2 + test + @@ -81,12 +87,31 @@ jackson-databind + + + org.projectlombok + lombok + 1.18.38 + provided + + org.springdoc springdoc-openapi-starter-webmvc-ui 2.2.0 + + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + 11.10.5 + @@ -113,4 +138,4 @@ - \ No newline at end of file + diff --git a/backend/src/main/java/com/vibing/backend/config/SecurityConfig.java b/backend/src/main/java/com/vibing/backend/config/SecurityConfig.java index 5e95533..bdceb7e 100644 --- a/backend/src/main/java/com/vibing/backend/config/SecurityConfig.java +++ b/backend/src/main/java/com/vibing/backend/config/SecurityConfig.java @@ -28,8 +28,7 @@ public class SecurityConfig { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIa - .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()) - .headers(headers -> headers.frameOptions().disable()); // Allow H2 console frames + .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); return http.build(); } diff --git a/backend/src/main/java/com/vibing/backend/controller/ActivityController.java b/backend/src/main/java/com/vibing/backend/controller/ActivityController.java new file mode 100644 index 0000000..d5374d7 --- /dev/null +++ b/backend/src/main/java/com/vibing/backend/controller/ActivityController.java @@ -0,0 +1,117 @@ +package com.vibing.backend.controller; + +import com.vibing.backend.model.Activity; +import com.vibing.backend.service.ActivityService; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +/** + * REST controller for Activity-related operations. + */ +@RestController +@RequestMapping("/activities") +@Tag(name = "Activity Management", description = "APIs for managing activities") +@CrossOrigin(origins = "*") // Configure this properly for production +@AllArgsConstructor +public class ActivityController { + + private final ActivityService activityService; + + /** + * Get all activities. + * + * @return ResponseEntity containing list of all activities + */ + @GetMapping + @Operation(summary = "Get all activities", description = "Retrieve a list of all activities") + public ResponseEntity> getAllActivities() { + List activities = activityService.findAll(); + return ResponseEntity.ok(activities); + } + + /** + * Get an activity by ID. + * + * @param id the activity ID + * @return ResponseEntity containing the activity if found + */ + @GetMapping("/{id}") + @Operation(summary = "Get activity by ID", description = "Retrieve a specific activity by its ID") + public ResponseEntity getActivityById(@PathVariable Long id) { + Optional activity = activityService.findById(id); + return activity.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + /** + * Get activities by name (case-insensitive search). + * + * @param name the activity name to search for + * @return ResponseEntity containing list of matching activities + */ + @GetMapping("/search/name") + @Operation(summary = "Search activities by name", description = "Search for activities by name (case-insensitive)") + public ResponseEntity> getActivitiesByName(@RequestParam String name) { + List activities = activityService.findByNameContainingIgnoreCase(name); + return ResponseEntity.ok(activities); + } + + /** + * Get activities by price range. + * + * @param priceRange the price range to filter by + * @return ResponseEntity containing list of activities with the specified price range + */ + @GetMapping("/search/price-range") + @Operation(summary = "Get activities by price range", description = "Retrieve activities with a specific price range") + public ResponseEntity> getActivitiesByPriceRange(@RequestParam Integer priceRange) { + List activities = activityService.findByPriceRange(priceRange); + return ResponseEntity.ok(activities); + } + + /** + * Get activities by location. + * + * @param locationId the location ID to filter by + * @return ResponseEntity containing list of activities at the specified location + */ + @GetMapping("/search/location") + @Operation(summary = "Get activities by location", description = "Retrieve activities at a specific location") + public ResponseEntity> getActivitiesByLocation(@RequestParam Long locationId) { + List activities = activityService.findByLocationId(locationId); + return ResponseEntity.ok(activities); + } + + /** + * Get activities by tag. + * + * @param tag the tag to search for + * @return ResponseEntity containing list of activities with the specified tag + */ + @GetMapping("/search/tag") + @Operation(summary = "Get activities by tag", description = "Retrieve activities that have a specific tag") + public ResponseEntity> getActivitiesByTag(@RequestParam String tag) { + List activities = activityService.findByTagsContaining(tag); + return ResponseEntity.ok(activities); + } + + /** + * Get activities by maximum price range. + * + * @param maxPriceRange the maximum price range + * @return ResponseEntity containing list of activities with price range <= maxPriceRange + */ + @GetMapping("/search/max-price") + @Operation(summary = "Get activities by maximum price range", description = "Retrieve activities with price range less than or equal to the specified value") + public ResponseEntity> getActivitiesByMaxPriceRange(@RequestParam Integer maxPriceRange) { + List activities = activityService.findByPriceRangeLessThanEqual(maxPriceRange); + return ResponseEntity.ok(activities); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/controller/UserController.java b/backend/src/main/java/com/vibing/backend/controller/UserController.java deleted file mode 100644 index a34eded..0000000 --- a/backend/src/main/java/com/vibing/backend/controller/UserController.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.vibing.backend.controller; - -import com.vibing.backend.model.User; -import com.vibing.backend.service.UserService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Optional; - -/** - * REST controller for User-related operations. - */ -@RestController -@RequestMapping("/users") -@Tag(name = "User Management", description = "APIs for managing users") -@CrossOrigin(origins = "*") // Configure this properly for production -public class UserController { - - private final UserService userService; - - @Autowired - public UserController(UserService userService) { - this.userService = userService; - } - - /** - * Get all users. - * - * @return ResponseEntity containing list of users - */ - @GetMapping - @Operation(summary = "Get all users", description = "Retrieve a list of all users") - public ResponseEntity> getAllUsers() { - List users = userService.getAllUsers(); - return ResponseEntity.ok(users); - } - - /** - * Get a user by ID. - * - * @param id the user ID - * @return ResponseEntity containing the user if found - */ - @GetMapping("/{id}") - @Operation(summary = "Get user by ID", description = "Retrieve a specific user by their ID") - public ResponseEntity getUserById(@PathVariable Long id) { - Optional user = userService.getUserById(id); - return user.map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - /** - * Get a user by username. - * - * @param username the username - * @return ResponseEntity containing the user if found - */ - @GetMapping("/username/{username}") - @Operation(summary = "Get user by username", description = "Retrieve a specific user by their username") - public ResponseEntity getUserByUsername(@PathVariable String username) { - Optional user = userService.getUserByUsername(username); - return user.map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - /** - * Create a new user. - * - * @param user the user to create - * @return ResponseEntity containing the created user - */ - @PostMapping - @Operation(summary = "Create a new user", description = "Create a new user account") - public ResponseEntity createUser(@Valid @RequestBody User user) { - try { - User createdUser = userService.createUser(user); - return ResponseEntity.status(HttpStatus.CREATED).body(createdUser); - } catch (RuntimeException e) { - return ResponseEntity.badRequest().build(); - } - } - - /** - * Update an existing user. - * - * @param id the user ID - * @param user the updated user data - * @return ResponseEntity containing the updated user - */ - @PutMapping("/{id}") - @Operation(summary = "Update user", description = "Update an existing user's information") - public ResponseEntity updateUser(@PathVariable Long id, @Valid @RequestBody User user) { - Optional updatedUser = userService.updateUser(id, user); - return updatedUser.map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - /** - * Delete a user by ID. - * - * @param id the user ID - * @return ResponseEntity indicating success or failure - */ - @DeleteMapping("/{id}") - @Operation(summary = "Delete user", description = "Delete a user by their ID") - public ResponseEntity deleteUser(@PathVariable Long id) { - boolean deleted = userService.deleteUser(id); - return deleted ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build(); - } - - /** - * Check if a username exists. - * - * @param username the username to check - * @return ResponseEntity with boolean indicating existence - */ - @GetMapping("/check-username/{username}") - @Operation(summary = "Check username availability", description = "Check if a username is already taken") - public ResponseEntity checkUsernameExists(@PathVariable String username) { - boolean exists = userService.userExistsByUsername(username); - return ResponseEntity.ok(exists); - } - - /** - * Check if an email exists. - * - * @param email the email to check - * @return ResponseEntity with boolean indicating existence - */ - @GetMapping("/check-email/{email}") - @Operation(summary = "Check email availability", description = "Check if an email is already registered") - public ResponseEntity checkEmailExists(@PathVariable String email) { - boolean exists = userService.userExistsByEmail(email); - return ResponseEntity.ok(exists); - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/model/Activity.java b/backend/src/main/java/com/vibing/backend/model/Activity.java new file mode 100644 index 0000000..3c969bf --- /dev/null +++ b/backend/src/main/java/com/vibing/backend/model/Activity.java @@ -0,0 +1,67 @@ +package com.vibing.backend.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Max; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import java.time.LocalDateTime; +import java.util.List; + +/** + * Activity entity representing an activity in the Vibing application. + */ +@Entity +@Table(name = "activities") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Activity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Activity name is required") + @Column(nullable = false) + private String name; + + @NotNull(message = "Location is required") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "location_id", nullable = false) + private Location location; + + @NotNull(message = "Price range is required") + @Min(value = 1, message = "Price range must be at least 1") + @Max(value = 5, message = "Price range must be at most 5") + @Column(name = "price_range", nullable = false) + private Integer priceRange; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "activity_tags", joinColumns = @JoinColumn(name = "activity_id")) + @Column(name = "tag") + private List tags; + + @Column(name = "description") + private String description; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/model/Location.java b/backend/src/main/java/com/vibing/backend/model/Location.java new file mode 100644 index 0000000..c7b2b0c --- /dev/null +++ b/backend/src/main/java/com/vibing/backend/model/Location.java @@ -0,0 +1,62 @@ +package com.vibing.backend.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import java.time.LocalDateTime; + +/** + * Location entity representing a location in the Vibing application. + */ +@Entity +@Table(name = "locations") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Location { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "Location name is required") + @Column(nullable = false) + private String name; + + @Column + private String address; + + @Column + private String city; + + @Column + private String country; + + @Column(name = "postal_code") + private String postalCode; + + @Column(name = "latitude") + private Double latitude; + + @Column(name = "longitude") + private Double longitude; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/model/User.java b/backend/src/main/java/com/vibing/backend/model/User.java deleted file mode 100644 index 1eb2329..0000000 --- a/backend/src/main/java/com/vibing/backend/model/User.java +++ /dev/null @@ -1,144 +0,0 @@ -package com.vibing.backend.model; - -import jakarta.persistence.*; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import java.time.LocalDateTime; - -/** - * User entity representing a user in the Vibing application. - */ -@Entity -@Table(name = "users") -public class User { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @NotBlank(message = "Username is required") - @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") - @Column(unique = true, nullable = false) - private String username; - - @NotBlank(message = "Email is required") - @Email(message = "Email should be valid") - @Column(unique = true, nullable = false) - private String email; - - @NotBlank(message = "Password is required") - @Size(min = 6, message = "Password must be at least 6 characters") - @Column(nullable = false) - private String password; - - @Column(name = "first_name") - private String firstName; - - @Column(name = "last_name") - private String lastName; - - @Column(name = "created_at") - private LocalDateTime createdAt; - - @Column(name = "updated_at") - private LocalDateTime updatedAt; - - @PrePersist - protected void onCreate() { - createdAt = LocalDateTime.now(); - updatedAt = LocalDateTime.now(); - } - - @PreUpdate - protected void onUpdate() { - updatedAt = LocalDateTime.now(); - } - - // Constructors - public User() {} - - public User(String username, String email, String password) { - this.username = username; - this.email = email; - this.password = password; - } - - // Getters and Setters - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public LocalDateTime getUpdatedAt() { - return updatedAt; - } - - public void setUpdatedAt(LocalDateTime updatedAt) { - this.updatedAt = updatedAt; - } - - @Override - public String toString() { - return "User{" + - "id=" + id + - ", username='" + username + '\'' + - ", email='" + email + '\'' + - ", firstName='" + firstName + '\'' + - ", lastName='" + lastName + '\'' + - ", createdAt=" + createdAt + - ", updatedAt=" + updatedAt + - '}'; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/repository/ActivityRepository.java b/backend/src/main/java/com/vibing/backend/repository/ActivityRepository.java new file mode 100644 index 0000000..94c6b46 --- /dev/null +++ b/backend/src/main/java/com/vibing/backend/repository/ActivityRepository.java @@ -0,0 +1,54 @@ +package com.vibing.backend.repository; + +import com.vibing.backend.model.Activity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** + * Repository interface for Activity entity operations. + */ +@Repository +public interface ActivityRepository extends JpaRepository { + + /** + * Find activities by name (case-insensitive). + * + * @param name the activity name to search for + * @return List of activities matching the name + */ + List findByNameContainingIgnoreCase(String name); + + /** + * Find activities by price range. + * + * @param priceRange the price range to search for + * @return List of activities with the specified price range + */ + List findByPriceRange(Integer priceRange); + + /** + * Find activities by location. + * + * @param locationId the location ID to search for + * @return List of activities at the specified location + */ + List findByLocationId(Long locationId); + + /** + * Find activities by tag. + * + * @param tag the tag to search for + * @return List of activities with the specified tag + */ + List findByTagsContaining(String tag); + + /** + * Find activities by price range less than or equal to. + * + * @param maxPriceRange the maximum price range + * @return List of activities with price range <= maxPriceRange + */ + List findByPriceRangeLessThanEqual(Integer maxPriceRange); +} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/repository/UserRepository.java b/backend/src/main/java/com/vibing/backend/repository/UserRepository.java deleted file mode 100644 index 34c1634..0000000 --- a/backend/src/main/java/com/vibing/backend/repository/UserRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.vibing.backend.repository; - -import com.vibing.backend.model.User; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.Optional; - -/** - * Repository interface for User entity operations. - */ -@Repository -public interface UserRepository extends JpaRepository { - - /** - * Find a user by username. - * - * @param username the username to search for - * @return Optional containing the user if found - */ - Optional findByUsername(String username); - - /** - * Find a user by email. - * - * @param email the email to search for - * @return Optional containing the user if found - */ - Optional findByEmail(String email); - - /** - * Check if a user exists by username. - * - * @param username the username to check - * @return true if user exists, false otherwise - */ - boolean existsByUsername(String username); - - /** - * Check if a user exists by email. - * - * @param email the email to check - * @return true if user exists, false otherwise - */ - boolean existsByEmail(String email); -} \ No newline at end of file diff --git a/backend/src/main/java/com/vibing/backend/service/ActivityService.java b/backend/src/main/java/com/vibing/backend/service/ActivityService.java new file mode 100644 index 0000000..959d16c --- /dev/null +++ b/backend/src/main/java/com/vibing/backend/service/ActivityService.java @@ -0,0 +1,49 @@ +package com.vibing.backend.service; + +import java.util.List; +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import com.vibing.backend.model.Activity; +import com.vibing.backend.repository.ActivityRepository; + +import lombok.AllArgsConstructor; + +/** + * Service class for managing activities. + */ +@Service +@AllArgsConstructor +public class ActivityService { + + private final ActivityRepository activityRepository; + + public List findAll() { + return activityRepository.findAll(); + } + + public Optional findById(Long id) { + return activityRepository.findById(id); + } + + public List findByNameContainingIgnoreCase(String name) { + return activityRepository.findByNameContainingIgnoreCase(name); + } + + public List findByPriceRange(Integer priceRange) { + return activityRepository.findByPriceRange(priceRange); + } + + public List findByTagsContaining(String tag) { + return activityRepository.findByTagsContaining(tag); + } + + public List findByLocationId(Long locationId) { + return activityRepository.findByLocationId(locationId); + } + + public List findByPriceRangeLessThanEqual(Integer maxPriceRange) { + return activityRepository.findByPriceRangeLessThanEqual(maxPriceRange); + } +} diff --git a/backend/src/main/java/com/vibing/backend/service/UserService.java b/backend/src/main/java/com/vibing/backend/service/UserService.java deleted file mode 100644 index 803cf12..0000000 --- a/backend/src/main/java/com/vibing/backend/service/UserService.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.vibing.backend.service; - -import com.vibing.backend.model.User; -import com.vibing.backend.repository.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.List; -import java.util.Optional; - -/** - * Service class for User-related business operations. - */ -@Service -public class UserService { - - private final UserRepository userRepository; - - @Autowired - public UserService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - /** - * Get all users. - * - * @return List of all users - */ - public List getAllUsers() { - return userRepository.findAll(); - } - - /** - * Get a user by ID. - * - * @param id the user ID - * @return Optional containing the user if found - */ - public Optional getUserById(Long id) { - return userRepository.findById(id); - } - - /** - * Get a user by username. - * - * @param username the username - * @return Optional containing the user if found - */ - public Optional getUserByUsername(String username) { - return userRepository.findByUsername(username); - } - - /** - * Get a user by email. - * - * @param email the email - * @return Optional containing the user if found - */ - public Optional getUserByEmail(String email) { - return userRepository.findByEmail(email); - } - - /** - * Create a new user. - * - * @param user the user to create - * @return the created user - */ - public User createUser(User user) { - // Check if username already exists - if (userRepository.existsByUsername(user.getUsername())) { - throw new RuntimeException("Username already exists: " + user.getUsername()); - } - - // Check if email already exists - if (userRepository.existsByEmail(user.getEmail())) { - throw new RuntimeException("Email already exists: " + user.getEmail()); - } - - return userRepository.save(user); - } - - /** - * Update an existing user. - * - * @param id the user ID - * @param user the updated user data - * @return Optional containing the updated user if found - */ - public Optional updateUser(Long id, User user) { - return userRepository.findById(id) - .map(existingUser -> { - existingUser.setUsername(user.getUsername()); - existingUser.setEmail(user.getEmail()); - existingUser.setFirstName(user.getFirstName()); - existingUser.setLastName(user.getLastName()); - return userRepository.save(existingUser); - }); - } - - /** - * Delete a user by ID. - * - * @param id the user ID - * @return true if user was deleted, false if not found - */ - public boolean deleteUser(Long id) { - if (userRepository.existsById(id)) { - userRepository.deleteById(id); - return true; - } - return false; - } - - /** - * Check if a user exists by username. - * - * @param username the username to check - * @return true if user exists, false otherwise - */ - public boolean userExistsByUsername(String username) { - return userRepository.existsByUsername(username); - } - - /** - * Check if a user exists by email. - * - * @param email the email to check - * @return true if user exists, false otherwise - */ - public boolean userExistsByEmail(String email) { - return userRepository.existsByEmail(email); - } -} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index d1e21e9..f0421dd 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -9,27 +9,21 @@ spring: # Database Configuration datasource: - url: jdbc:h2:mem:testdb - driver-class-name: org.h2.Driver - username: sa - password: password + url: jdbc:postgresql://localhost:5432/vibing + username: vibing + password: vibing + driver-class-name: org.postgresql.Driver # JPA Configuration jpa: - database-platform: org.hibernate.dialect.H2Dialect + database-platform: org.hibernate.dialect.PostgreSQLDialect hibernate: - ddl-auto: create-drop + ddl-auto: update show-sql: true properties: hibernate: format_sql: true - # H2 Console (for development) - h2: - console: - enabled: true - path: /h2-console - # Jackson Configuration jackson: default-property-inclusion: non_null diff --git a/backend/src/main/resources/db/migration/V1__initial_schema.sql b/backend/src/main/resources/db/migration/V1__initial_schema.sql new file mode 100644 index 0000000..02b0a92 --- /dev/null +++ b/backend/src/main/resources/db/migration/V1__initial_schema.sql @@ -0,0 +1,35 @@ +CREATE SCHEMA IF NOT EXISTS vibing; + +CREATE TABLE IF NOT EXISTS vibing.locations ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + address TEXT, + city TEXT, + country TEXT, + postal_code TEXT, + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS vibing.activities ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + price_range INT NOT NULL, + location_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_location + FOREIGN KEY(location_id) + REFERENCES vibing.locations(id) + ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS vibing.activity_tags ( + activity_id INT NOT NULL, + tag TEXT NOT NULL, + FOREIGN KEY (activity_id) REFERENCES vibing.activities(id) ON DELETE CASCADE, + PRIMARY KEY (activity_id, tag) +); diff --git a/backend/src/test/java/com/vibing/backend/VibingBackendApplicationTests.java b/backend/src/test/java/com/vibing/backend/VibingBackendApplicationTests.java index 02d8a52..4469286 100644 --- a/backend/src/test/java/com/vibing/backend/VibingBackendApplicationTests.java +++ b/backend/src/test/java/com/vibing/backend/VibingBackendApplicationTests.java @@ -2,15 +2,17 @@ package com.vibing.backend; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; /** * Basic test class for the Vibing Backend application. */ @SpringBootTest +@ActiveProfiles("test") class VibingBackendApplicationTests { @Test void contextLoads() { // This test verifies that the Spring application context loads successfully } -} \ No newline at end of file +} diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml new file mode 100644 index 0000000..b056451 --- /dev/null +++ b/backend/src/test/resources/application-test.yml @@ -0,0 +1,21 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL + username: sa + password: + driver-class-name: org.h2.Driver + + h2: + console: + enabled: true + + jpa: + hibernate: + ddl-auto: none # Let Flyway handle schema creation + show-sql: true + database-platform: org.hibernate.dialect.H2Dialect + + flyway: + enabled: true + clean-disabled: false # Allow clean in tests + locations: classpath:db/migration diff --git a/local-dev/compose.yml b/local-dev/compose.yml new file mode 100644 index 0000000..a1fc9b5 --- /dev/null +++ b/local-dev/compose.yml @@ -0,0 +1,11 @@ +services: + postgres: + image: postgres:15 + ports: + - 5432:5432 + environment: + POSTGRES_USER: vibing + POSTGRES_PASSWORD: vibing + POSTGRES_DB: vibing + volumes: + - postgres_data:/var/lib/postgresql/data \ No newline at end of file