Compare commits
13 Commits
99032bf03c
...
activity-t
| Author | SHA1 | Date | |
|---|---|---|---|
| 684d46c5b2 | |||
| b4cd05e7b2 | |||
| 286699a1a8 | |||
| c3764176ac | |||
| 47ac6f4c74 | |||
| 928f65f9c5 | |||
| 19cba1606b | |||
| 9a11b4ea7e | |||
| dc26a8c467 | |||
| c631313ded | |||
| 260d40ad67 | |||
| 4c1067c65c | |||
|
|
705b5087f4 |
32
.clinerules/frontend-color-theme.md
Normal file
32
.clinerules/frontend-color-theme.md
Normal file
@@ -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
|
||||||
38
.clinerules/git-workflow.md
Normal file
38
.clinerules/git-workflow.md
Normal file
@@ -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
|
||||||
111
.cursor/rules/git-feature-branch.mdc
Normal file
111
.cursor/rules/git-feature-branch.mdc
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Feature Branch Workflow
|
||||||
|
|
||||||
|
## Always Work in Feature Branches
|
||||||
|
|
||||||
|
**CRITICAL**: Never work directly on `main` or `master` branches. Always create and use feature branches for all development work.
|
||||||
|
|
||||||
|
## Feature Branch Naming Convention
|
||||||
|
|
||||||
|
Use descriptive, kebab-case names for feature branches:
|
||||||
|
- `feature/user-authentication`
|
||||||
|
- `feature/dashboard-redesign`
|
||||||
|
- `bugfix/login-validation`
|
||||||
|
- `hotfix/security-patch`
|
||||||
|
- `refactor/api-endpoints`
|
||||||
|
|
||||||
|
## Workflow Steps
|
||||||
|
|
||||||
|
### 1. Before Starting Work
|
||||||
|
```bash
|
||||||
|
# Ensure you're on main and it's up to date
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Create and switch to a new feature branch
|
||||||
|
git checkout -b feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. During Development
|
||||||
|
- Make frequent, small commits with descriptive messages
|
||||||
|
- Use conventional commit format: `type(scope): description`
|
||||||
|
- Examples:
|
||||||
|
- `feat(auth): add login form validation`
|
||||||
|
- `fix(ui): resolve button alignment issue`
|
||||||
|
- `refactor(api): simplify user data fetching`
|
||||||
|
|
||||||
|
### 3. Before Pushing
|
||||||
|
```bash
|
||||||
|
# Ensure your branch is up to date with main
|
||||||
|
git fetch origin
|
||||||
|
git rebase origin/main
|
||||||
|
|
||||||
|
# Push your feature branch
|
||||||
|
git push origin feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Code Review Process
|
||||||
|
- Create a Pull Request (PR) from your feature branch to main
|
||||||
|
- Request reviews from team members
|
||||||
|
- Address feedback and push updates to the same branch
|
||||||
|
- Only merge after approval
|
||||||
|
|
||||||
|
### 5. After Merge
|
||||||
|
```bash
|
||||||
|
# Switch back to main and update
|
||||||
|
git checkout main
|
||||||
|
git pull origin main
|
||||||
|
|
||||||
|
# Delete the feature branch (local and remote)
|
||||||
|
git branch -d feature/your-feature-name
|
||||||
|
git push origin --delete feature/your-feature-name
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Commit Messages
|
||||||
|
- Use present tense ("add feature" not "added feature")
|
||||||
|
- Be specific and descriptive
|
||||||
|
- Reference issue numbers when applicable: `feat(auth): add OAuth login #123`
|
||||||
|
|
||||||
|
### Branch Management
|
||||||
|
- Keep feature branches short-lived (1-3 days ideally)
|
||||||
|
- One feature per branch
|
||||||
|
- Don't mix different types of changes in one branch
|
||||||
|
|
||||||
|
### Before Creating PRs
|
||||||
|
- Ensure all tests pass
|
||||||
|
- Update documentation if needed
|
||||||
|
- Self-review your changes
|
||||||
|
- Ensure code follows project conventions
|
||||||
|
|
||||||
|
## Emergency Hotfixes
|
||||||
|
|
||||||
|
For critical production issues:
|
||||||
|
```bash
|
||||||
|
# Create hotfix branch from main
|
||||||
|
git checkout main
|
||||||
|
git checkout -b hotfix/critical-issue-name
|
||||||
|
|
||||||
|
# Make minimal necessary changes
|
||||||
|
# Test thoroughly
|
||||||
|
# Create PR for immediate review
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with CI/CD
|
||||||
|
|
||||||
|
- Feature branches should trigger CI/CD pipelines
|
||||||
|
- All tests must pass before merging
|
||||||
|
- Code coverage should not decrease
|
||||||
|
- Security scans should pass
|
||||||
|
|
||||||
|
## Remember
|
||||||
|
|
||||||
|
- **Never commit directly to main/master**
|
||||||
|
- **Always create a feature branch for new work**
|
||||||
|
- **Keep branches focused and small**
|
||||||
|
- **Use descriptive branch and commit names**
|
||||||
|
- **Review your own code before requesting reviews**
|
||||||
49
.gitea/README.md
Normal file
49
.gitea/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Gitea Actions for Vibing Project
|
||||||
|
|
||||||
|
This directory contains Gitea Actions workflows for the Vibing project (both frontend and backend).
|
||||||
|
|
||||||
|
## Workflows
|
||||||
|
|
||||||
|
### Backend CI (`backend-ci.yml`)
|
||||||
|
|
||||||
|
Runs Maven tests on the backend project.
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Push to `main` or `develop` branches
|
||||||
|
- Pull requests to `main` or `develop` branches
|
||||||
|
- Only when changes are made to files in the `backend/` directory
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Checks out the code
|
||||||
|
2. Sets up Java 21 (Temurin distribution)
|
||||||
|
3. Caches Maven dependencies for faster builds
|
||||||
|
4. Runs `mvn test` in the backend directory
|
||||||
|
5. Uploads test results as artifacts (retained for 7 days)
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Java 21
|
||||||
|
- Maven
|
||||||
|
- Ubuntu runner
|
||||||
|
|
||||||
|
### Frontend CI (`frontend-ci.yml`)
|
||||||
|
|
||||||
|
Runs linting and builds the frontend project.
|
||||||
|
|
||||||
|
**Triggers:**
|
||||||
|
- Push to `main` or `develop` branches
|
||||||
|
- Pull requests to `main` or `develop` branches
|
||||||
|
- Only when changes are made to files in the `frontend/` directory
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Checks out the code
|
||||||
|
2. Sets up Node.js 18
|
||||||
|
3. Caches npm dependencies for faster builds
|
||||||
|
4. Installs dependencies with `npm ci`
|
||||||
|
5. Runs ESLint for code quality checks
|
||||||
|
6. Builds the project with `npm run build`
|
||||||
|
7. Uploads build artifacts (retained for 7 days)
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- Node.js 18
|
||||||
|
- npm
|
||||||
|
- Ubuntu runner
|
||||||
44
.gitea/workflows/backend-ci.yml
Normal file
44
.gitea/workflows/backend-ci.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Backend CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'backend/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run Maven Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: '21'
|
||||||
|
distribution: 'temurin'
|
||||||
|
cache: 'maven'
|
||||||
|
|
||||||
|
- name: Set up Maven
|
||||||
|
uses: stCarolas/setup-maven@v5
|
||||||
|
with:
|
||||||
|
maven-version: '3.6.3'
|
||||||
|
|
||||||
|
- name: Run Maven tests
|
||||||
|
working-directory: ./backend
|
||||||
|
run: mvn test
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: backend-test-results
|
||||||
|
path: backend/target/surefire-reports/
|
||||||
|
retention-days: 7
|
||||||
46
.gitea/workflows/frontend-ci.yml
Normal file
46
.gitea/workflows/frontend-ci.yml
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Frontend CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/**'
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, develop ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-build:
|
||||||
|
name: Lint and Build Frontend
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
working-directory: frontend
|
||||||
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build project
|
||||||
|
working-directory: frontend
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: frontend-build
|
||||||
|
path: frontend/dist
|
||||||
|
retention-days: 7
|
||||||
32
README.md
Normal file
32
README.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Vibing application
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
classDiagram
|
||||||
|
class Activity {
|
||||||
|
+Long id
|
||||||
|
+String name
|
||||||
|
+Location location
|
||||||
|
+Integer priceRange
|
||||||
|
+List~String~ tags
|
||||||
|
+String description
|
||||||
|
+LocalDateTime createdAt
|
||||||
|
+LocalDateTime updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
class Location {
|
||||||
|
+Long id
|
||||||
|
+String name
|
||||||
|
+String address
|
||||||
|
+String city
|
||||||
|
+String country
|
||||||
|
+String postalCode
|
||||||
|
+Double latitude
|
||||||
|
+Double longitude
|
||||||
|
+LocalDateTime createdAt
|
||||||
|
+LocalDateTime updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity <-- Location : belongs to
|
||||||
|
```
|
||||||
99
backend/.gitignore
vendored
Normal file
99
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.nar
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
||||||
|
replay_pid*
|
||||||
|
|
||||||
|
# Maven
|
||||||
|
target/
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
pom.xml.next
|
||||||
|
release.properties
|
||||||
|
dependency-reduced-pom.xml
|
||||||
|
buildNumber.properties
|
||||||
|
.mvn/timing.properties
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
.vscode/
|
||||||
|
.settings/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
application-local.yml
|
||||||
|
application-prod.yml
|
||||||
|
*.db
|
||||||
|
*.sqlite
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
193
backend/README.md
Normal file
193
backend/README.md
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
# Vibing Backend
|
||||||
|
|
||||||
|
A REST API backend service built with Spring Boot, Java 21, and Maven for the Vibing application.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **RESTful API**: Complete CRUD operations for user management
|
||||||
|
- **Spring Boot 3.2.0**: Latest Spring Boot version with Java 21 support
|
||||||
|
- **JPA/Hibernate**: Database persistence with H2 in-memory database
|
||||||
|
- **Spring Security**: Basic security configuration (extensible for production)
|
||||||
|
- **API Documentation**: Swagger/OpenAPI 3 documentation
|
||||||
|
- **Validation**: Input validation using Bean Validation
|
||||||
|
- **Testing**: JUnit 5 test framework
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Java 21 or higher
|
||||||
|
- Maven 3.6 or higher
|
||||||
|
- IDE (IntelliJ IDEA, Eclipse, VS Code, etc.)
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd vibing/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run the Application
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn spring-boot:run
|
||||||
|
```
|
||||||
|
|
||||||
|
The application will start on `http://localhost:8080`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
- **API Base**: `http://localhost:8080/api`
|
||||||
|
- **Health Check**: `http://localhost:8080/api/health`
|
||||||
|
- **Swagger UI**: `http://localhost:8080/swagger-ui.html`
|
||||||
|
- **H2 Console**: `http://localhost:8080/h2-console`
|
||||||
|
|
||||||
|
### User Management Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/users` | Get all users |
|
||||||
|
| GET | `/api/users/{id}` | Get user by ID |
|
||||||
|
| GET | `/api/users/username/{username}` | Get user by username |
|
||||||
|
| POST | `/api/users` | Create new user |
|
||||||
|
| PUT | `/api/users/{id}` | Update user |
|
||||||
|
| DELETE | `/api/users/{id}` | Delete user |
|
||||||
|
| GET | `/api/users/check-username/{username}` | Check username availability |
|
||||||
|
| GET | `/api/users/check-email/{email}` | Check email availability |
|
||||||
|
|
||||||
|
### Health Check Endpoints
|
||||||
|
|
||||||
|
| Method | Endpoint | Description |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/health` | Application health status |
|
||||||
|
| GET | `/api/health/ping` | Simple ping endpoint |
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The application uses H2 in-memory database for development:
|
||||||
|
|
||||||
|
- **URL**: `jdbc:h2:mem:testdb`
|
||||||
|
- **Username**: `sa`
|
||||||
|
- **Password**: `password`
|
||||||
|
- **Console**: `http://localhost:8080/h2-console`
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/
|
||||||
|
│ ├── java/com/vibing/backend/
|
||||||
|
│ │ ├── VibingBackendApplication.java # Main application class
|
||||||
|
│ │ ├── controller/ # REST controllers
|
||||||
|
│ │ │ ├── UserController.java
|
||||||
|
│ │ │ └── HealthController.java
|
||||||
|
│ │ ├── service/ # Business logic
|
||||||
|
│ │ │ └── UserService.java
|
||||||
|
│ │ ├── repository/ # Data access layer
|
||||||
|
│ │ │ └── UserRepository.java
|
||||||
|
│ │ ├── model/ # Entity classes
|
||||||
|
│ │ │ └── User.java
|
||||||
|
│ │ ├── config/ # Configuration classes
|
||||||
|
│ │ │ └── SecurityConfig.java
|
||||||
|
│ │ └── exception/ # Custom exceptions
|
||||||
|
│ └── resources/
|
||||||
|
│ └── application.yml # Application configuration
|
||||||
|
└── test/
|
||||||
|
└── java/com/vibing/backend/
|
||||||
|
└── VibingBackendApplicationTests.java
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application configuration is in `src/main/resources/application.yml`:
|
||||||
|
|
||||||
|
- **Server Port**: 8080
|
||||||
|
- **Context Path**: `/api`
|
||||||
|
- **Database**: H2 in-memory
|
||||||
|
- **JPA**: Hibernate with create-drop DDL
|
||||||
|
- **Security**: Basic configuration (allows all user endpoints)
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building JAR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
The JAR file will be created in the `target/` directory.
|
||||||
|
|
||||||
|
### Running JAR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar target/vibing-backend-1.0.0.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
Once the application is running, you can access:
|
||||||
|
|
||||||
|
- **Swagger UI**: `http://localhost:8080/swagger-ui.html`
|
||||||
|
- **OpenAPI JSON**: `http://localhost:8080/api-docs`
|
||||||
|
|
||||||
|
## Example API Usage
|
||||||
|
|
||||||
|
### Create a User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/users \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"username": "john_doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"password": "password123",
|
||||||
|
"firstName": "John",
|
||||||
|
"lastName": "Doe"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get All Users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get User by ID
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8080/api/users/1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
The current security configuration allows all requests to user endpoints for development purposes. For production:
|
||||||
|
|
||||||
|
1. Implement proper authentication (JWT, OAuth2, etc.)
|
||||||
|
2. Add role-based authorization
|
||||||
|
3. Configure CORS properly
|
||||||
|
4. Use HTTPS
|
||||||
|
5. Implement rate limiting
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
1. Create a feature branch
|
||||||
|
2. Make your changes
|
||||||
|
3. Add tests
|
||||||
|
4. Submit a pull request
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
155
backend/pom.xml
Normal file
155
backend/pom.xml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||||
|
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.vibing</groupId>
|
||||||
|
<artifactId>vibing-backend</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>Vibing Backend</name>
|
||||||
|
<description>REST API backend service for Vibing application</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>21</java.version>
|
||||||
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Starters -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Database -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Development Tools -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test Dependencies -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.datafaker</groupId>
|
||||||
|
<artifactId>datafaker</artifactId>
|
||||||
|
<version>2.4.4</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hamcrest</groupId>
|
||||||
|
<artifactId>hamcrest-core</artifactId>
|
||||||
|
<version>3.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JSON Processing -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.38</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- API Documentation -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Flyway -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-postgresql</artifactId>
|
||||||
|
<version>11.10.5</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.11.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>21</source>
|
||||||
|
<target>21</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
35
backend/run.sh
Executable file
35
backend/run.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Vibing Backend Run Script
|
||||||
|
echo "Starting Vibing Backend..."
|
||||||
|
|
||||||
|
# Check if Java 21 is available
|
||||||
|
if ! command -v java &> /dev/null; then
|
||||||
|
echo "Error: Java is not installed or not in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check Java version
|
||||||
|
JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d'.' -f1)
|
||||||
|
if [ "$JAVA_VERSION" != "21" ]; then
|
||||||
|
echo "Warning: Java 21 is recommended. Current version: $JAVA_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if Maven is available
|
||||||
|
if ! command -v mvn &> /dev/null; then
|
||||||
|
echo "Error: Maven is not installed or not in PATH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building and running the application..."
|
||||||
|
echo "The application will be available at:"
|
||||||
|
echo " - API Base: http://localhost:8080/api"
|
||||||
|
echo " - Health Check: http://localhost:8080/api/health"
|
||||||
|
echo " - Swagger UI: http://localhost:8080/swagger-ui.html"
|
||||||
|
echo " - H2 Console: http://localhost:8080/h2-console"
|
||||||
|
echo ""
|
||||||
|
echo "Press Ctrl+C to stop the application"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
mvn spring-boot:run
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.vibing.backend;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Spring Boot application class for the Vibing backend service.
|
||||||
|
* This class serves as the entry point for the REST API backend.
|
||||||
|
*/
|
||||||
|
@SpringBootApplication
|
||||||
|
public class VibingBackendApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(VibingBackendApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.vibing.backend.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security configuration for the Vibing backend application.
|
||||||
|
* This is a basic configuration that can be extended for production use.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure security filter chain.
|
||||||
|
* For development purposes, this allows all requests.
|
||||||
|
* In production, you should implement proper authentication and authorization.
|
||||||
|
*
|
||||||
|
* @param http the HttpSecurity object
|
||||||
|
* @return SecurityFilterChain
|
||||||
|
* @throws Exception if configuration fails
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(AbstractHttpConfigurer::disable) // Disable CSRF for REST APIa
|
||||||
|
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<List<Activity>> getAllActivities() {
|
||||||
|
List<Activity> 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<Activity> getActivityById(@PathVariable Long id) {
|
||||||
|
Optional<Activity> 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<List<Activity>> getActivitiesByName(@RequestParam String name) {
|
||||||
|
List<Activity> 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<List<Activity>> getActivitiesByPriceRange(@RequestParam Integer priceRange) {
|
||||||
|
List<Activity> 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<List<Activity>> getActivitiesByLocation(@RequestParam Long locationId) {
|
||||||
|
List<Activity> 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<List<Activity>> getActivitiesByTag(@RequestParam String tag) {
|
||||||
|
List<Activity> 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<List<Activity>> getActivitiesByMaxPriceRange(@RequestParam Integer maxPriceRange) {
|
||||||
|
List<Activity> activities = activityService.findByPriceRangeLessThanEqual(maxPriceRange);
|
||||||
|
return ResponseEntity.ok(activities);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.vibing.backend.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Health check controller for monitoring the application status.
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/health")
|
||||||
|
@Tag(name = "Health Check", description = "APIs for checking application health")
|
||||||
|
public class HealthController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the health status of the application.
|
||||||
|
*
|
||||||
|
* @return ResponseEntity containing health information
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "Health check", description = "Check if the application is running")
|
||||||
|
public ResponseEntity<Map<String, Object>> healthCheck() {
|
||||||
|
Map<String, Object> healthInfo = new HashMap<>();
|
||||||
|
healthInfo.put("status", "UP");
|
||||||
|
healthInfo.put("timestamp", LocalDateTime.now());
|
||||||
|
healthInfo.put("service", "Vibing Backend");
|
||||||
|
healthInfo.put("version", "1.0.0");
|
||||||
|
|
||||||
|
return ResponseEntity.ok(healthInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a simple ping response.
|
||||||
|
*
|
||||||
|
* @return ResponseEntity with "pong" message
|
||||||
|
*/
|
||||||
|
@GetMapping("/ping")
|
||||||
|
@Operation(summary = "Ping", description = "Simple ping endpoint")
|
||||||
|
public ResponseEntity<String> ping() {
|
||||||
|
return ResponseEntity.ok("pong");
|
||||||
|
}
|
||||||
|
}
|
||||||
67
backend/src/main/java/com/vibing/backend/model/Activity.java
Normal file
67
backend/src/main/java/com/vibing/backend/model/Activity.java
Normal file
@@ -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<String> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
64
backend/src/main/java/com/vibing/backend/model/Location.java
Normal file
64
backend/src/main/java/com/vibing/backend/model/Location.java
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package com.vibing.backend.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.With;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location entity representing a location in the Vibing application.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(name = "locations")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@With
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Activity, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find activities by name (case-insensitive).
|
||||||
|
*
|
||||||
|
* @param name the activity name to search for
|
||||||
|
* @return List of activities matching the name
|
||||||
|
*/
|
||||||
|
List<Activity> 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<Activity> findByPriceRange(Integer priceRange);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find activities by location.
|
||||||
|
*
|
||||||
|
* @param locationId the location ID to search for
|
||||||
|
* @return List of activities at the specified location
|
||||||
|
*/
|
||||||
|
List<Activity> findByLocationId(Long locationId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find activities by tag.
|
||||||
|
*
|
||||||
|
* @param tag the tag to search for
|
||||||
|
* @return List of activities with the specified tag
|
||||||
|
*/
|
||||||
|
List<Activity> 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<Activity> findByPriceRangeLessThanEqual(Integer maxPriceRange);
|
||||||
|
}
|
||||||
@@ -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<Activity> findAll() {
|
||||||
|
return activityRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Activity> findById(Long id) {
|
||||||
|
return activityRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> findByNameContainingIgnoreCase(String name) {
|
||||||
|
return activityRepository.findByNameContainingIgnoreCase(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> findByPriceRange(Integer priceRange) {
|
||||||
|
return activityRepository.findByPriceRange(priceRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> findByTagsContaining(String tag) {
|
||||||
|
return activityRepository.findByTagsContaining(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> findByLocationId(Long locationId) {
|
||||||
|
return activityRepository.findByLocationId(locationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Activity> findByPriceRangeLessThanEqual(Integer maxPriceRange) {
|
||||||
|
return activityRepository.findByPriceRangeLessThanEqual(maxPriceRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
backend/src/main/resources/application.yml
Normal file
56
backend/src/main/resources/application.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
servlet:
|
||||||
|
context-path: /api
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: vibing-backend
|
||||||
|
|
||||||
|
# Database Configuration
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://localhost:5432/vibing
|
||||||
|
username: vibing
|
||||||
|
password: vibing
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
# JPA Configuration
|
||||||
|
jpa:
|
||||||
|
database-platform: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: update
|
||||||
|
show-sql: true
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
|
||||||
|
# Jackson Configuration
|
||||||
|
jackson:
|
||||||
|
default-property-inclusion: non_null
|
||||||
|
serialization:
|
||||||
|
write-dates-as-timestamps: false
|
||||||
|
deserialization:
|
||||||
|
fail-on-unknown-properties: false
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.vibing.backend: DEBUG
|
||||||
|
org.springframework.security: DEBUG
|
||||||
|
org.springframework.web: DEBUG
|
||||||
|
pattern:
|
||||||
|
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
|
||||||
|
|
||||||
|
# API Documentation
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
operations-sorter: method
|
||||||
|
|
||||||
|
# Security Configuration
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
secret: your-secret-key-here-change-in-production
|
||||||
|
expiration: 86400000 # 24 hours in milliseconds
|
||||||
@@ -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)
|
||||||
|
);
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package com.vibing.backend;
|
||||||
|
|
||||||
|
import com.vibing.backend.model.Activity;
|
||||||
|
import com.vibing.backend.model.Location;
|
||||||
|
import com.vibing.backend.repository.ActivityRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
|
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for Activity entity using H2 database.
|
||||||
|
* Tests repository operations and database interactions.
|
||||||
|
*/
|
||||||
|
@DataJpaTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@TestPropertySource(locations = "classpath:application-test.yml")
|
||||||
|
public class ActivityRepositoryTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestEntityManager entityManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActivityRepository activityRepository;
|
||||||
|
|
||||||
|
private Location testLocation;
|
||||||
|
private Activity testActivity;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
// Create and persist test location
|
||||||
|
testLocation = new Location();
|
||||||
|
testLocation.setName("Test Location");
|
||||||
|
testLocation.setAddress("123 Test Street");
|
||||||
|
testLocation.setCity("Test City");
|
||||||
|
testLocation.setCountry("Test Country");
|
||||||
|
testLocation.setPostalCode("12345");
|
||||||
|
testLocation.setLatitude(40.7128);
|
||||||
|
testLocation.setLongitude(-74.0060);
|
||||||
|
testLocation = entityManager.persistAndFlush(testLocation);
|
||||||
|
|
||||||
|
// Create test activity
|
||||||
|
testActivity = new Activity();
|
||||||
|
testActivity.setName("Test Activity");
|
||||||
|
testActivity.setDescription("A test activity for integration testing");
|
||||||
|
testActivity.setLocation(testLocation);
|
||||||
|
testActivity.setPriceRange(3);
|
||||||
|
testActivity.setTags(Arrays.asList("test", "integration", "fun"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSaveAndRetrieveActivity() {
|
||||||
|
// Given - activity is created in setUp()
|
||||||
|
|
||||||
|
// When
|
||||||
|
Activity savedActivity = activityRepository.save(testActivity);
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
Optional<Activity> retrievedActivity = activityRepository.findById(savedActivity.getId());
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(retrievedActivity).isPresent();
|
||||||
|
assertThat(retrievedActivity.get().getName()).isEqualTo("Test Activity");
|
||||||
|
assertThat(retrievedActivity.get().getDescription()).isEqualTo("A test activity for integration testing");
|
||||||
|
assertThat(retrievedActivity.get().getPriceRange()).isEqualTo(3);
|
||||||
|
assertThat(retrievedActivity.get().getLocation().getName()).isEqualTo("Test Location");
|
||||||
|
assertThat(retrievedActivity.get().getTags()).containsExactlyInAnyOrder("test", "integration", "fun");
|
||||||
|
assertThat(retrievedActivity.get().getCreatedAt()).isNotNull();
|
||||||
|
assertThat(retrievedActivity.get().getUpdatedAt()).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindActivitiesByNameContainingIgnoreCase() {
|
||||||
|
// Given
|
||||||
|
Activity activity1 = new Activity();
|
||||||
|
activity1.setName("Beach Volleyball");
|
||||||
|
activity1.setLocation(testLocation);
|
||||||
|
activity1.setPriceRange(2);
|
||||||
|
activity1.setTags(Arrays.asList("sport", "beach"));
|
||||||
|
|
||||||
|
Activity activity2 = new Activity();
|
||||||
|
activity2.setName("Mountain Hiking");
|
||||||
|
activity2.setLocation(testLocation);
|
||||||
|
activity2.setPriceRange(1);
|
||||||
|
activity2.setTags(Arrays.asList("outdoor", "hiking"));
|
||||||
|
|
||||||
|
activityRepository.saveAll(Arrays.asList(activity1, activity2));
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Activity> beachActivities = activityRepository.findByNameContainingIgnoreCase("beach");
|
||||||
|
List<Activity> mountainActivities = activityRepository.findByNameContainingIgnoreCase("MOUNTAIN");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(beachActivities).hasSize(1);
|
||||||
|
assertThat(beachActivities.get(0).getName()).isEqualTo("Beach Volleyball");
|
||||||
|
|
||||||
|
assertThat(mountainActivities).hasSize(1);
|
||||||
|
assertThat(mountainActivities.get(0).getName()).isEqualTo("Mountain Hiking");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindActivitiesByPriceRange() {
|
||||||
|
// Given
|
||||||
|
Activity cheapActivity = new Activity();
|
||||||
|
cheapActivity.setName("Free Walking Tour");
|
||||||
|
cheapActivity.setLocation(testLocation);
|
||||||
|
cheapActivity.setPriceRange(1);
|
||||||
|
cheapActivity.setTags(Arrays.asList("free", "walking"));
|
||||||
|
|
||||||
|
Activity expensiveActivity = new Activity();
|
||||||
|
expensiveActivity.setName("Luxury Spa Day");
|
||||||
|
expensiveActivity.setLocation(testLocation);
|
||||||
|
expensiveActivity.setPriceRange(5);
|
||||||
|
expensiveActivity.setTags(Arrays.asList("luxury", "spa"));
|
||||||
|
|
||||||
|
activityRepository.saveAll(Arrays.asList(cheapActivity, expensiveActivity));
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Activity> cheapActivities = activityRepository.findByPriceRange(1);
|
||||||
|
List<Activity> expensiveActivities = activityRepository.findByPriceRange(5);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(cheapActivities).hasSize(1);
|
||||||
|
assertThat(cheapActivities.get(0).getName()).isEqualTo("Free Walking Tour");
|
||||||
|
|
||||||
|
assertThat(expensiveActivities).hasSize(1);
|
||||||
|
assertThat(expensiveActivities.get(0).getName()).isEqualTo("Luxury Spa Day");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindActivitiesByLocationId() {
|
||||||
|
// Given
|
||||||
|
Location anotherLocation = new Location();
|
||||||
|
anotherLocation.setName("Another Location");
|
||||||
|
anotherLocation.setCity("Another City");
|
||||||
|
anotherLocation = entityManager.persistAndFlush(anotherLocation);
|
||||||
|
|
||||||
|
Activity activityAtTestLocation = new Activity();
|
||||||
|
activityAtTestLocation.setName("Activity at Test Location");
|
||||||
|
activityAtTestLocation.setLocation(testLocation);
|
||||||
|
activityAtTestLocation.setPriceRange(2);
|
||||||
|
|
||||||
|
Activity activityAtAnotherLocation = new Activity();
|
||||||
|
activityAtAnotherLocation.setName("Activity at Another Location");
|
||||||
|
activityAtAnotherLocation.setLocation(anotherLocation);
|
||||||
|
activityAtAnotherLocation.setPriceRange(3);
|
||||||
|
|
||||||
|
activityRepository.saveAll(Arrays.asList(activityAtTestLocation, activityAtAnotherLocation));
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Activity> activitiesAtTestLocation = activityRepository.findByLocationId(testLocation.getId());
|
||||||
|
List<Activity> activitiesAtAnotherLocation = activityRepository.findByLocationId(anotherLocation.getId());
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(activitiesAtTestLocation).hasSize(1);
|
||||||
|
assertThat(activitiesAtTestLocation.get(0).getName()).isEqualTo("Activity at Test Location");
|
||||||
|
|
||||||
|
assertThat(activitiesAtAnotherLocation).hasSize(1);
|
||||||
|
assertThat(activitiesAtAnotherLocation.get(0).getName()).isEqualTo("Activity at Another Location");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindActivitiesByTagsContaining() {
|
||||||
|
// Given
|
||||||
|
Activity outdoorActivity = new Activity();
|
||||||
|
outdoorActivity.setName("Rock Climbing");
|
||||||
|
outdoorActivity.setLocation(testLocation);
|
||||||
|
outdoorActivity.setPriceRange(3);
|
||||||
|
outdoorActivity.setTags(Arrays.asList("outdoor", "adventure", "climbing"));
|
||||||
|
|
||||||
|
Activity indoorActivity = new Activity();
|
||||||
|
indoorActivity.setName("Museum Visit");
|
||||||
|
indoorActivity.setLocation(testLocation);
|
||||||
|
indoorActivity.setPriceRange(2);
|
||||||
|
indoorActivity.setTags(Arrays.asList("indoor", "culture", "educational"));
|
||||||
|
|
||||||
|
activityRepository.saveAll(Arrays.asList(outdoorActivity, indoorActivity));
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Activity> outdoorActivities = activityRepository.findByTagsContaining("outdoor");
|
||||||
|
List<Activity> cultureActivities = activityRepository.findByTagsContaining("culture");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(outdoorActivities).hasSize(1);
|
||||||
|
assertThat(outdoorActivities.get(0).getName()).isEqualTo("Rock Climbing");
|
||||||
|
|
||||||
|
assertThat(cultureActivities).hasSize(1);
|
||||||
|
assertThat(cultureActivities.get(0).getName()).isEqualTo("Museum Visit");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFindActivitiesByPriceRangeLessThanEqual() {
|
||||||
|
// Given
|
||||||
|
Activity cheapActivity = new Activity();
|
||||||
|
cheapActivity.setName("Budget Activity");
|
||||||
|
cheapActivity.setLocation(testLocation);
|
||||||
|
cheapActivity.setPriceRange(1);
|
||||||
|
|
||||||
|
Activity moderateActivity = new Activity();
|
||||||
|
moderateActivity.setName("Moderate Activity");
|
||||||
|
moderateActivity.setLocation(testLocation);
|
||||||
|
moderateActivity.setPriceRange(3);
|
||||||
|
|
||||||
|
Activity expensiveActivity = new Activity();
|
||||||
|
expensiveActivity.setName("Expensive Activity");
|
||||||
|
expensiveActivity.setLocation(testLocation);
|
||||||
|
expensiveActivity.setPriceRange(5);
|
||||||
|
|
||||||
|
activityRepository.saveAll(Arrays.asList(cheapActivity, moderateActivity, expensiveActivity));
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// When
|
||||||
|
List<Activity> budgetActivities = activityRepository.findByPriceRangeLessThanEqual(2);
|
||||||
|
List<Activity> moderateBudgetActivities = activityRepository.findByPriceRangeLessThanEqual(3);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(budgetActivities).hasSize(1);
|
||||||
|
assertThat(budgetActivities.get(0).getName()).isEqualTo("Budget Activity");
|
||||||
|
|
||||||
|
assertThat(moderateBudgetActivities).hasSize(2);
|
||||||
|
assertThat(moderateBudgetActivities)
|
||||||
|
.extracting(Activity::getName)
|
||||||
|
.containsExactlyInAnyOrder("Budget Activity", "Moderate Activity");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateActivityTimestamps() {
|
||||||
|
// Given
|
||||||
|
Activity savedActivity = activityRepository.save(testActivity);
|
||||||
|
entityManager.flush();
|
||||||
|
entityManager.clear();
|
||||||
|
|
||||||
|
// When
|
||||||
|
Optional<Activity> retrievedActivity = activityRepository.findById(savedActivity.getId());
|
||||||
|
assertThat(retrievedActivity).isPresent();
|
||||||
|
|
||||||
|
Activity activityToUpdate = retrievedActivity.get();
|
||||||
|
activityToUpdate.setName("Updated Activity Name");
|
||||||
|
Activity updatedActivity = activityRepository.save(activityToUpdate);
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
assertThat(updatedActivity.getCreatedAt()).isNotNull();
|
||||||
|
assertThat(updatedActivity.getUpdatedAt()).isNotNull();
|
||||||
|
assertThat(updatedActivity.getUpdatedAt()).isAfterOrEqualTo(updatedActivity.getCreatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteActivity() {
|
||||||
|
// Given
|
||||||
|
Activity savedActivity = activityRepository.save(testActivity);
|
||||||
|
entityManager.flush();
|
||||||
|
Long activityId = savedActivity.getId();
|
||||||
|
|
||||||
|
// When
|
||||||
|
activityRepository.deleteById(activityId);
|
||||||
|
entityManager.flush();
|
||||||
|
|
||||||
|
// Then
|
||||||
|
Optional<Activity> deletedActivity = activityRepository.findById(activityId);
|
||||||
|
assertThat(deletedActivity).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
backend/src/test/java/com/vibing/backend/ActivityTestIt.java
Normal file
46
backend/src/test/java/com/vibing/backend/ActivityTestIt.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.vibing.backend;
|
||||||
|
|
||||||
|
import static org.hamcrest.MatcherAssert.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.context.TestPropertySource;
|
||||||
|
|
||||||
|
import com.vibing.backend.model.Activity;
|
||||||
|
import com.vibing.backend.model.Location;
|
||||||
|
import com.vibing.backend.repository.ActivityRepository;
|
||||||
|
import com.vibing.backend.service.ActivityService;
|
||||||
|
|
||||||
|
import net.datafaker.Faker;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@TestPropertySource(locations = "classpath:application-test.yml")
|
||||||
|
public class ActivityTestIt {
|
||||||
|
|
||||||
|
private Faker faker = new Faker(new Random(123));
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActivityService activityService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ActivityRepository activityRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldSaveActivity() {
|
||||||
|
Activity activity = new Activity();
|
||||||
|
activity.setName(faker.location().publicSpace());
|
||||||
|
activity.setPriceRange(faker.number().numberBetween(1, 5));
|
||||||
|
activity.setTags(faker.lorem().words(3));
|
||||||
|
activity.setLocation(new Location().withName(faker.location().publicSpace()));
|
||||||
|
activityRepository.save(activity);
|
||||||
|
|
||||||
|
assertThat(activityService.findAll(), containsInAnyOrder(activity));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
25
backend/src/test/resources/application-test.yml
Normal file
25
backend/src/test/resources/application-test.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL;INIT=CREATE SCHEMA IF NOT EXISTS vibing
|
||||||
|
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
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
default_schema: vibing
|
||||||
|
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
clean-disabled: false # Allow clean in tests
|
||||||
|
locations: classpath:db/migration
|
||||||
|
default-schema: vibing
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
# Vibing - React + Vite Application
|
|
||||||
|
|
||||||
A modern React application built with Vite, featuring beautiful components and a responsive design that adapts to any device.
|
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- **Fast Development**: Built with Vite for lightning-fast development and hot module replacement
|
|
||||||
- **Modern React**: Using React 19 with modern hooks and patterns
|
|
||||||
- **Responsive Design**: Fully responsive design that looks great on all devices
|
|
||||||
- **Component Library**: Reusable components with consistent styling
|
|
||||||
- **Custom Hooks**: Useful custom hooks for common functionality
|
|
||||||
- **API Utilities**: Built-in API utilities with error handling
|
|
||||||
- **Modern Styling**: Beautiful gradients and modern CSS design
|
|
||||||
|
|
||||||
## 📦 Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── components/ # Reusable UI components
|
|
||||||
│ ├── Header.jsx # Navigation header
|
|
||||||
│ ├── Footer.jsx # Site footer
|
|
||||||
│ ├── Button.jsx # Reusable button component
|
|
||||||
│ ├── Card.jsx # Card component for content
|
|
||||||
│ └── index.js # Component exports
|
|
||||||
├── hooks/ # Custom React hooks
|
|
||||||
│ └── useLocalStorage.js
|
|
||||||
├── pages/ # Page components
|
|
||||||
│ ├── Home.jsx # Home page
|
|
||||||
│ └── Home.css
|
|
||||||
├── utils/ # Utility functions
|
|
||||||
│ └── api.js # API utilities
|
|
||||||
├── styles/ # Global styles (future use)
|
|
||||||
├── assets/ # Static assets
|
|
||||||
├── App.jsx # Main app component
|
|
||||||
├── App.css # Global styles
|
|
||||||
├── main.jsx # App entry point
|
|
||||||
└── index.css # Base styles
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🛠️ Getting Started
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Node.js (version 16 or higher)
|
|
||||||
- npm or yarn
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
|
|
||||||
1. Clone the repository:
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd vibing
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Start the development server:
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Open your browser and navigate to `http://localhost:5173`
|
|
||||||
|
|
||||||
## 📜 Available Scripts
|
|
||||||
|
|
||||||
- `npm run dev` - Start development server
|
|
||||||
- `npm run build` - Build for production
|
|
||||||
- `npm run preview` - Preview production build
|
|
||||||
- `npm run lint` - Run ESLint
|
|
||||||
|
|
||||||
## 🎨 Components
|
|
||||||
|
|
||||||
### Button Component
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Button } from './components';
|
|
||||||
|
|
||||||
// Different variants
|
|
||||||
<Button variant="primary">Primary Button</Button>
|
|
||||||
<Button variant="secondary">Secondary Button</Button>
|
|
||||||
<Button variant="outline">Outline Button</Button>
|
|
||||||
<Button variant="ghost">Ghost Button</Button>
|
|
||||||
|
|
||||||
// Different sizes
|
|
||||||
<Button size="small">Small</Button>
|
|
||||||
<Button size="medium">Medium</Button>
|
|
||||||
<Button size="large">Large</Button>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Card Component
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Card } from './components';
|
|
||||||
|
|
||||||
<Card
|
|
||||||
title="Card Title"
|
|
||||||
subtitle="Card subtitle"
|
|
||||||
image="https://example.com/image.jpg"
|
|
||||||
>
|
|
||||||
<p>Card content goes here...</p>
|
|
||||||
</Card>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Header & Footer
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { Header, Footer } from './components';
|
|
||||||
|
|
||||||
// Header with navigation
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
// Footer with links and social media
|
|
||||||
<Footer />
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Custom Hooks
|
|
||||||
|
|
||||||
### useLocalStorage
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import useLocalStorage from './hooks/useLocalStorage';
|
|
||||||
|
|
||||||
const [value, setValue] = useLocalStorage('key', initialValue);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🌐 API Utilities
|
|
||||||
|
|
||||||
```jsx
|
|
||||||
import { api } from './utils/api';
|
|
||||||
|
|
||||||
// GET request
|
|
||||||
const data = await api.get('/users');
|
|
||||||
|
|
||||||
// POST request
|
|
||||||
const newUser = await api.post('/users', { name: 'John', email: 'john@example.com' });
|
|
||||||
|
|
||||||
// PUT request
|
|
||||||
const updatedUser = await api.put('/users/1', { name: 'Jane' });
|
|
||||||
|
|
||||||
// DELETE request
|
|
||||||
await api.delete('/users/1');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 Environment Variables
|
|
||||||
|
|
||||||
Create a `.env` file in the root directory:
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_API_BASE_URL=http://localhost:3000/api
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📱 Responsive Design
|
|
||||||
|
|
||||||
The application is fully responsive and includes:
|
|
||||||
|
|
||||||
- Mobile-first design approach
|
|
||||||
- Flexible grid layouts
|
|
||||||
- Responsive typography
|
|
||||||
- Touch-friendly interactions
|
|
||||||
- Optimized for all screen sizes
|
|
||||||
|
|
||||||
## 🎨 Styling
|
|
||||||
|
|
||||||
The project uses:
|
|
||||||
|
|
||||||
- CSS modules for component-specific styles
|
|
||||||
- CSS custom properties for theming
|
|
||||||
- Flexbox and Grid for layouts
|
|
||||||
- Modern CSS features like gradients and animations
|
|
||||||
- Responsive design patterns
|
|
||||||
|
|
||||||
## 🚀 Deployment
|
|
||||||
|
|
||||||
### Build for Production
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The build output will be in the `dist` directory, ready for deployment to any static hosting service.
|
|
||||||
|
|
||||||
### Deploy to Vercel
|
|
||||||
|
|
||||||
1. Install Vercel CLI:
|
|
||||||
```bash
|
|
||||||
npm i -g vercel
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Deploy:
|
|
||||||
```bash
|
|
||||||
vercel
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy to Netlify
|
|
||||||
|
|
||||||
1. Build the project:
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Upload the `dist` folder to Netlify
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
||||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
||||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
||||||
5. Open a Pull Request
|
|
||||||
|
|
||||||
## 📄 License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
||||||
|
|
||||||
## 🙏 Acknowledgments
|
|
||||||
|
|
||||||
- [Vite](https://vitejs.dev/) for the fast build tool
|
|
||||||
- [React](https://reactjs.org/) for the UI library
|
|
||||||
- [Unsplash](https://unsplash.com/) for the beautiful images
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './Footer.css';
|
import './Footer.css';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../hooks/useLanguage';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../hooks/useLanguage';
|
||||||
import LanguageSwitcher from './LanguageSwitcher';
|
import LanguageSwitcher from './LanguageSwitcher';
|
||||||
import './Header.css';
|
import './Header.css';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../hooks/useLanguage';
|
||||||
import './LanguageSwitcher.css';
|
import './LanguageSwitcher.css';
|
||||||
|
|
||||||
const LanguageSwitcher = () => {
|
const LanguageSwitcher = () => {
|
||||||
|
|||||||
@@ -1,86 +1,5 @@
|
|||||||
import React, { createContext, useContext, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
import { LanguageContext, translations } from './languageContextData';
|
||||||
const LanguageContext = createContext();
|
|
||||||
|
|
||||||
export const useLanguage = () => {
|
|
||||||
const context = useContext(LanguageContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useLanguage must be used within a LanguageProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
const translations = {
|
|
||||||
en: {
|
|
||||||
// Header
|
|
||||||
home: 'Home',
|
|
||||||
about: 'About',
|
|
||||||
contact: 'Contact',
|
|
||||||
|
|
||||||
// CTA Section
|
|
||||||
readyToStart: 'Ready to get started?',
|
|
||||||
ctaSubtitle: 'Join thousands of developers building amazing applications with our modern stack.',
|
|
||||||
startBuilding: 'Start Building',
|
|
||||||
|
|
||||||
// Language Switcher
|
|
||||||
language: 'Language',
|
|
||||||
english: 'English',
|
|
||||||
finnish: 'Finnish',
|
|
||||||
|
|
||||||
// Search Page
|
|
||||||
searchPlaceholder: 'Enter your search query...',
|
|
||||||
searchButton: 'Search',
|
|
||||||
searchResults: 'Search Results',
|
|
||||||
searchResultsSubtitle: 'Here are the results for your search query.',
|
|
||||||
searchResultsPlaceholder: 'Search results will appear here. Try entering a search term above to get started.',
|
|
||||||
|
|
||||||
// Search Filters
|
|
||||||
dogFriendly: 'Dog Friendly',
|
|
||||||
accessible: 'Accessible',
|
|
||||||
familyFriendly: 'Family Friendly',
|
|
||||||
peopleCount: 'People',
|
|
||||||
priceRange: 'Price',
|
|
||||||
any: 'Any',
|
|
||||||
|
|
||||||
// Footer
|
|
||||||
footerDescription: 'Find fun things to do!',
|
|
||||||
},
|
|
||||||
fi: {
|
|
||||||
// Header
|
|
||||||
home: 'Koti',
|
|
||||||
about: 'Tietoja',
|
|
||||||
contact: 'Yhteystiedot',
|
|
||||||
|
|
||||||
|
|
||||||
// CTA Section
|
|
||||||
readyToStart: 'Valmiina aloittamaan?',
|
|
||||||
ctaSubtitle: 'Liity tuhansien kehittäjien joukkoon, jotka rakentavat upeita sovelluksia modernilla teknologiapinoamme.',
|
|
||||||
startBuilding: 'Aloita rakentaminen',
|
|
||||||
|
|
||||||
// Language Switcher
|
|
||||||
language: 'Kieli',
|
|
||||||
english: 'Englanti',
|
|
||||||
finnish: 'Suomi',
|
|
||||||
|
|
||||||
// Search Page
|
|
||||||
searchPlaceholder: 'Syötä hakukyselysi...',
|
|
||||||
searchButton: 'Hae',
|
|
||||||
searchResults: 'Hakutulokset',
|
|
||||||
searchResultsSubtitle: 'Tässä ovat hakutulokset kyselysi perusteella.',
|
|
||||||
searchResultsPlaceholder: 'Hakutulokset näkyvät täällä. Kokeile syöttää hakusana yllä aloittaaksesi.',
|
|
||||||
|
|
||||||
// Search Filters
|
|
||||||
dogFriendly: 'Koiraystävällinen',
|
|
||||||
accessible: 'Esteetön',
|
|
||||||
familyFriendly: 'Lapsiystävällinen',
|
|
||||||
peopleCount: 'Henkilöt',
|
|
||||||
priceRange: 'Hinta',
|
|
||||||
any: 'Mikä tahansa',
|
|
||||||
|
|
||||||
// Footer
|
|
||||||
footerDescription: 'Löydä hauskaa tekemistä!',
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LanguageProvider = ({ children }) => {
|
export const LanguageProvider = ({ children }) => {
|
||||||
const [language, setLanguage] = useState('en');
|
const [language, setLanguage] = useState('en');
|
||||||
|
|||||||
75
frontend/src/contexts/languageContextData.js
Normal file
75
frontend/src/contexts/languageContextData.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export const LanguageContext = createContext();
|
||||||
|
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
// Header
|
||||||
|
home: 'Home',
|
||||||
|
about: 'About',
|
||||||
|
contact: 'Contact',
|
||||||
|
|
||||||
|
// CTA Section
|
||||||
|
readyToStart: 'Ready to get started?',
|
||||||
|
ctaSubtitle: 'Join thousands of developers building amazing applications with our modern stack.',
|
||||||
|
startBuilding: 'Start Building',
|
||||||
|
|
||||||
|
// Language Switcher
|
||||||
|
language: 'Language',
|
||||||
|
english: 'English',
|
||||||
|
finnish: 'Finnish',
|
||||||
|
|
||||||
|
// Search Page
|
||||||
|
searchPlaceholder: 'Enter your search query...',
|
||||||
|
searchButton: 'Search',
|
||||||
|
searchResults: 'Search Results',
|
||||||
|
searchResultsSubtitle: 'Here are the results for your search query.',
|
||||||
|
searchResultsPlaceholder: 'Search results will appear here. Try entering a search term above to get started.',
|
||||||
|
|
||||||
|
// Search Filters
|
||||||
|
dogFriendly: 'Dog Friendly',
|
||||||
|
accessible: 'Accessible',
|
||||||
|
familyFriendly: 'Family Friendly',
|
||||||
|
peopleCount: 'People',
|
||||||
|
priceRange: 'Price',
|
||||||
|
any: 'Any',
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footerDescription: 'Find fun things to do!',
|
||||||
|
},
|
||||||
|
fi: {
|
||||||
|
// Header
|
||||||
|
home: 'Koti',
|
||||||
|
about: 'Tietoja',
|
||||||
|
contact: 'Yhteystiedot',
|
||||||
|
|
||||||
|
|
||||||
|
// CTA Section
|
||||||
|
readyToStart: 'Valmiina aloittamaan?',
|
||||||
|
ctaSubtitle: 'Liity tuhansien kehittäjien joukkoon, jotka rakentavat upeita sovelluksia modernilla teknologiapinoamme.',
|
||||||
|
startBuilding: 'Aloita rakentaminen',
|
||||||
|
|
||||||
|
// Language Switcher
|
||||||
|
language: 'Kieli',
|
||||||
|
english: 'Englanti',
|
||||||
|
finnish: 'Suomi',
|
||||||
|
|
||||||
|
// Search Page
|
||||||
|
searchPlaceholder: 'Syötä hakukyselysi...',
|
||||||
|
searchButton: 'Hae',
|
||||||
|
searchResults: 'Hakutulokset',
|
||||||
|
searchResultsSubtitle: 'Tässä ovat hakutulokset kyselysi perusteella.',
|
||||||
|
searchResultsPlaceholder: 'Hakutulokset näkyvät täällä. Kokeile syöttää hakusana yllä aloittaaksesi.',
|
||||||
|
|
||||||
|
// Search Filters
|
||||||
|
dogFriendly: 'Koiraystävällinen',
|
||||||
|
accessible: 'Esteetön',
|
||||||
|
familyFriendly: 'Lapsiystävällinen',
|
||||||
|
peopleCount: 'Henkilöt',
|
||||||
|
priceRange: 'Hinta',
|
||||||
|
any: 'Mikä tahansa',
|
||||||
|
|
||||||
|
// Footer
|
||||||
|
footerDescription: 'Löydä hauskaa tekemistä!',
|
||||||
|
}
|
||||||
|
};
|
||||||
10
frontend/src/hooks/useLanguage.js
Normal file
10
frontend/src/hooks/useLanguage.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { LanguageContext } from '../contexts/languageContextData';
|
||||||
|
|
||||||
|
export const useLanguage = () => {
|
||||||
|
const context = useContext(LanguageContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useLanguage must be used within a LanguageProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -205,6 +205,107 @@
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Search Results Section */
|
||||||
|
.search-results {
|
||||||
|
padding: 4rem 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 3rem 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card .card-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card .card-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card .card-body p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenities {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenity-tag {
|
||||||
|
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||||
|
color: var(--color-dark);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 8px var(--color-shadow-primary);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amenity-tag:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px var(--color-shadow-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No Results Section */
|
||||||
|
.no-results {
|
||||||
|
padding: 4rem 0;
|
||||||
|
background: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content p {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.search-button.searching {
|
||||||
|
background: linear-gradient(135deg, var(--color-secondary-dark) 0%, var(--color-dark) 100%);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-button.searching:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: 0 4px 12px var(--color-shadow-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.hero {
|
.hero {
|
||||||
@@ -261,6 +362,27 @@
|
|||||||
.features-grid {
|
.features-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@@ -284,6 +406,14 @@
|
|||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark Mode Support */
|
/* Dark Mode Support */
|
||||||
@@ -334,4 +464,24 @@
|
|||||||
.features {
|
.features {
|
||||||
background: var(--color-bg-dark);
|
background: var(--color-bg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
background: var(--color-bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-title {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content h2 {
|
||||||
|
color: var(--color-text-primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results-content p {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Header, Footer, Button, Card } from '../components';
|
import { Header, Footer, Button, Card } from '../components';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../hooks/useLanguage';
|
||||||
import './Home.css';
|
import './Home.css';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
@@ -13,14 +13,62 @@ const Home = () => {
|
|||||||
peopleCount: '',
|
peopleCount: '',
|
||||||
priceRange: ''
|
priceRange: ''
|
||||||
});
|
});
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
// Mockup data for search results
|
||||||
|
const mockResults = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Cozy Mountain Cabin",
|
||||||
|
subtitle: "€€ • 4 guests • Dog friendly",
|
||||||
|
image: "https://images.unsplash.com/photo-1449824913935-59a10b8d2000?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Beautiful mountain cabin with stunning views, perfect for a peaceful getaway. Features a hot tub and fireplace.",
|
||||||
|
amenities: ["Dog friendly", "Fireplace", "Hot tub", "Mountain view"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Modern City Apartment",
|
||||||
|
subtitle: "€€€ • 2 guests • Accessible",
|
||||||
|
image: "https://images.unsplash.com/photo-1502672260266-1c1ef2d93688?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Stylish apartment in the heart of the city with modern amenities and easy access to public transportation.",
|
||||||
|
amenities: ["Accessible", "City view", "Gym access", "Parking"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Family Beach House",
|
||||||
|
subtitle: "€€€€ • 6 guests • Family friendly",
|
||||||
|
image: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Spacious beachfront property perfect for family vacations. Includes a pool, playground, and direct beach access.",
|
||||||
|
amenities: ["Family friendly", "Beach access", "Pool", "Playground"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Rustic Farmhouse",
|
||||||
|
subtitle: "€€ • 8 guests • Dog friendly",
|
||||||
|
image: "https://images.unsplash.com/photo-1571896349842-33c89424de2d?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Charming farmhouse surrounded by nature. Perfect for large groups looking for a rural escape.",
|
||||||
|
amenities: ["Dog friendly", "Large groups", "Nature", "Fireplace"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "Luxury Villa",
|
||||||
|
subtitle: "€€€€ • 4 guests • Accessible",
|
||||||
|
image: "https://images.unsplash.com/photo-1613490493576-7fde63acd811?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Exclusive villa with premium amenities including a private pool, chef's kitchen, and stunning ocean views.",
|
||||||
|
amenities: ["Accessible", "Private pool", "Ocean view", "Chef's kitchen"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: "Cozy Studio",
|
||||||
|
subtitle: "€ • 2 guests • Family friendly",
|
||||||
|
image: "https://images.unsplash.com/photo-1560448204-e02f11c3d0e2?q=80&w=1920&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||||
|
description: "Affordable studio apartment perfect for couples or small families. Located in a quiet neighborhood.",
|
||||||
|
amenities: ["Family friendly", "Affordable", "Quiet", "Central location"]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const handleGetStarted = () => {
|
|
||||||
alert('Get Started button clicked!');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLearnMore = () => {
|
|
||||||
alert('Learn More button clicked!');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFilterChange = (filterName, value) => {
|
const handleFilterChange = (filterName, value) => {
|
||||||
setFilters(prev => ({
|
setFilters(prev => ({
|
||||||
@@ -30,9 +78,30 @@ const Home = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
// Handle search logic here
|
if (!searchQuery.trim()) {
|
||||||
console.log('Search query:', searchQuery);
|
alert('Please enter a search query');
|
||||||
console.log('Filters:', filters);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSearching(true);
|
||||||
|
|
||||||
|
// Simulate API call delay
|
||||||
|
setTimeout(() => {
|
||||||
|
// Filter results based on search query and filters
|
||||||
|
let filteredResults = mockResults.filter(result => {
|
||||||
|
const matchesQuery = result.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
result.description.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
const matchesDogFriendly = !filters.dogFriendly || result.amenities.includes('Dog friendly');
|
||||||
|
const matchesAccessible = !filters.accessible || result.amenities.includes('Accessible');
|
||||||
|
const matchesFamilyFriendly = !filters.familyFriendly || result.amenities.includes('Family friendly');
|
||||||
|
|
||||||
|
return matchesQuery && matchesDogFriendly && matchesAccessible && matchesFamilyFriendly;
|
||||||
|
});
|
||||||
|
|
||||||
|
setSearchResults(filteredResults);
|
||||||
|
setIsSearching(false);
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,8 +126,12 @@ const Home = () => {
|
|||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
/>
|
/>
|
||||||
<button className="search-button" onClick={handleSearch}>
|
<button
|
||||||
{t('searchButton')}
|
className={`search-button ${isSearching ? 'searching' : ''}`}
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={isSearching}
|
||||||
|
>
|
||||||
|
{isSearching ? 'Searching...' : t('searchButton')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -140,6 +213,50 @@ const Home = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Search Results Section */}
|
||||||
|
{searchResults.length > 0 && (
|
||||||
|
<section className="search-results">
|
||||||
|
<div className="container">
|
||||||
|
<h2 className="results-title">
|
||||||
|
Found {searchResults.length} result{searchResults.length !== 1 ? 's' : ''} for "{searchQuery}"
|
||||||
|
</h2>
|
||||||
|
<div className="results-grid">
|
||||||
|
{searchResults.map(result => (
|
||||||
|
<Card
|
||||||
|
key={result.id}
|
||||||
|
title={result.title}
|
||||||
|
subtitle={result.subtitle}
|
||||||
|
image={result.image}
|
||||||
|
imageAlt={result.title}
|
||||||
|
className="result-card"
|
||||||
|
>
|
||||||
|
<p>{result.description}</p>
|
||||||
|
<div className="amenities">
|
||||||
|
{result.amenities.map((amenity, index) => (
|
||||||
|
<span key={index} className="amenity-tag">
|
||||||
|
{amenity}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* No Results Message */}
|
||||||
|
{searchResults.length === 0 && !isSearching && searchQuery && (
|
||||||
|
<section className="no-results">
|
||||||
|
<div className="container">
|
||||||
|
<div className="no-results-content">
|
||||||
|
<h2>No results found for "{searchQuery}"</h2>
|
||||||
|
<p>Try adjusting your search terms or filters to find more options.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
11
local-dev/compose.yml
Normal file
11
local-dev/compose.yml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user