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