├── .gitignore
├── backend
├── lombok.config
├── src
│ ├── test
│ │ ├── resources
│ │ │ └── application.properties
│ │ └── java
│ │ │ └── de
│ │ │ └── neuefische
│ │ │ └── capstone
│ │ │ └── backend
│ │ │ ├── imageupload
│ │ │ ├── CloudinaryServiceTest.java
│ │ │ ├── ImageUploadServiceTest.java
│ │ │ └── ImageUploadIntegrationTest.java
│ │ │ ├── security
│ │ │ ├── MongoUserDetailsServiceTest.java
│ │ │ ├── MongoUserServiceTest.java
│ │ │ └── MongoUserIntegrationTest.java
│ │ │ ├── ProjectCalculationsTest.java
│ │ │ └── ProjectServiceTest.java
│ └── main
│ │ ├── resources
│ │ └── application.properties
│ │ └── java
│ │ └── de
│ │ └── neuefische
│ │ └── capstone
│ │ └── backend
│ │ ├── models
│ │ ├── Category.java
│ │ ├── ImageCreation.java
│ │ ├── Demand.java
│ │ ├── ParticipationCreation.java
│ │ ├── DonationCreation.java
│ │ ├── Participation.java
│ │ ├── Image.java
│ │ ├── Donation.java
│ │ ├── ProjectCreation.java
│ │ ├── ProjectNoId.java
│ │ └── Project.java
│ │ ├── services
│ │ └── IdService.java
│ │ ├── ProjectRepo.java
│ │ ├── imageupload
│ │ ├── ImageUploadRepo.java
│ │ ├── BeanConfigCloudinary.java
│ │ ├── ImageUploadController.java
│ │ ├── CloudinaryService.java
│ │ └── ImageUploadService.java
│ │ ├── BackendApplication.java
│ │ ├── security
│ │ ├── MongoUserRepository.java
│ │ ├── MongoUserWithoutPassword.java
│ │ ├── MongoUserCreation.java
│ │ ├── MongoUser.java
│ │ ├── MongoUserDetailsService.java
│ │ ├── MongoUserController.java
│ │ ├── SecurityConfig.java
│ │ └── MongoUserService.java
│ │ ├── ProjectController.java
│ │ ├── ProjectCalculations.java
│ │ └── ProjectService.java
├── .mvn
│ └── wrapper
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── .gitignore
├── pom.xml
├── mvnw.cmd
└── mvnw
├── frontend
├── src
│ ├── vite-env.d.ts
│ ├── pages
│ │ ├── Home.tsx
│ │ ├── Gallery.tsx
│ │ ├── ShowProject.tsx
│ │ ├── FilteredGallery.tsx
│ │ ├── LoginPage.tsx
│ │ ├── UserProfile.tsx
│ │ ├── RegisterPage.tsx
│ │ ├── SearchGallary.tsx
│ │ ├── AddDonationOrParticipation.tsx
│ │ └── AddEditProject.tsx
│ ├── main.tsx
│ ├── components
│ │ ├── ProtectedRoutes.tsx
│ │ ├── LogoutButton.tsx
│ │ ├── HomeButton.tsx
│ │ ├── SearchButton.tsx
│ │ ├── AddButton.tsx
│ │ ├── DonationButton.tsx
│ │ ├── ParticipationButton.tsx
│ │ ├── DeleteButton.tsx
│ │ ├── ProgressBarGalleryView.tsx
│ │ ├── UserButton.tsx
│ │ ├── ProgressBar.tsx
│ │ ├── EditButton.tsx
│ │ ├── FilterButton.tsx
│ │ ├── TopDonators.tsx
│ │ ├── NavigationBar.tsx
│ │ ├── FilterUserData.tsx
│ │ └── ProjectCard.tsx
│ ├── models
│ │ └── models.tsx
│ ├── GlobalStyles.tsx
│ ├── App.tsx
│ └── hooks
│ │ └── useFetch.tsx
├── tsconfig.node.json
├── .gitignore
├── vite.config.ts
├── index.html
├── .eslintrc.cjs
├── tsconfig.json
├── package.json
└── public
│ ├── vite.svg
│ └── favicon.svg
├── Dockerfile
├── sonar-project.properties
├── .github
├── workflows
│ ├── sonar-frontend.yml
│ ├── maven.yml
│ ├── show-logs.yml
│ ├── sonar-backend.yml
│ └── deploy.yml
└── ISSUE_TEMPLATE
│ └── user-story-template.md
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea/
2 | *.iml
3 |
--------------------------------------------------------------------------------
/backend/lombok.config:
--------------------------------------------------------------------------------
1 | lombok.addLombokGeneratedAnnotation = true
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/backend/src/test/resources/application.properties:
--------------------------------------------------------------------------------
1 | de.flapdoodle.mongodb.embedded.version=6.0.1
2 |
--------------------------------------------------------------------------------
/backend/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ben-21/capstone-neuefische/HEAD/backend/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/backend/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.data.mongodb.uri=${MONGO_DB_URI}
2 | server.error.include-message=always
3 | server.error.include-binding-errors=always
4 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Category.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | public enum Category {
4 | DONATION,
5 | PARTICIPATION
6 | }
7 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/ImageCreation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | public record ImageCreation(
4 | String name
5 | ) {
6 | }
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:20
2 |
3 | ENV ENVIRONMENT=prod
4 |
5 | LABEL maintainer="bs"
6 |
7 | EXPOSE 8080
8 |
9 | ADD backend/target/capstone.jar app.jar
10 |
11 | CMD [ "sh", "-c", "java -jar /app.jar" ]
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Demand.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | public enum Demand {
4 | MONEYDONATION,
5 | DONATIONINKIND,
6 | FOODDONATION,
7 | DRUGDONATION,
8 | }
9 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/ParticipationCreation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | public record ParticipationCreation(
4 | String projectId,
5 | String projectName
6 | ) {
7 | }
8 |
--------------------------------------------------------------------------------
/backend/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
3 |
--------------------------------------------------------------------------------
/frontend/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/DonationCreation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public record DonationCreation(
6 | String projectId,
7 | String projectName,
8 | BigDecimal amount
9 | ) {
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Participation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | public record Participation(
4 | String id,
5 | String projectId,
6 | String projectName,
7 | String participationName,
8 | String userId
9 | ) {
10 | }
11 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Image.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import org.springframework.data.mongodb.core.mapping.Document;
4 |
5 | @Document("images")
6 | public record Image(
7 | String id,
8 | String name,
9 | String url
10 | ) {
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import Gallery from "./Gallery.tsx";
2 | import {StyledBody, StyledMain} from "../GlobalStyles.tsx";
3 |
4 | export default function Home() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/services/IdService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.services;
2 |
3 |
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class IdService {
8 | public String createRandomId() {
9 | return java.util.UUID.randomUUID().toString();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Donation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import java.math.BigDecimal;
4 |
5 | public record Donation(
6 | String id,
7 | String projectId,
8 | String projectName,
9 | String donorName,
10 | BigDecimal amount,
11 | String userId
12 | ) {
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | host: 'localhost',
9 | port: 3000,
10 | proxy: {
11 | '/api/': {
12 | target: 'http://localhost:8080',
13 | }
14 | }
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/ProjectRepo.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import de.neuefische.capstone.backend.models.Project;
4 | import org.springframework.data.mongodb.repository.MongoRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | @Repository
8 | public interface ProjectRepo extends MongoRepository {
9 | }
10 |
--------------------------------------------------------------------------------
/frontend/src/main.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom/client'
3 | import App from './App.tsx'
4 | import {BrowserRouter} from "react-router-dom";
5 |
6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
7 |
8 |
9 |
10 |
11 | ,
12 | )
13 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/ProjectCreation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import java.util.List;
4 |
5 | public record ProjectCreation(
6 | String name,
7 | String description,
8 | Category category,
9 | List demands,
10 | String location,
11 | int goal,
12 | Image image
13 | ) {
14 | }
15 |
--------------------------------------------------------------------------------
/frontend/src/components/ProtectedRoutes.tsx:
--------------------------------------------------------------------------------
1 | import {Outlet, Navigate} from "react-router-dom";
2 |
3 | type Props = {
4 | user?: string
5 | }
6 |
7 | export default function ProtectedRoutes({user}: Props) {
8 |
9 | if (user === undefined) return "loading ...";
10 |
11 | const isLoggedIn = user !== "anonymousUser";
12 |
13 | return <>{isLoggedIn ? : }>;
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/imageupload/ImageUploadRepo.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import de.neuefische.capstone.backend.models.Image;
4 | import org.springframework.data.mongodb.repository.MongoRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | @Repository
8 | public interface ImageUploadRepo extends MongoRepository {
9 | }
10 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/BackendApplication.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class BackendApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(BackendApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Be Human - A Humanitarian Project Platform
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserRepository.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import org.springframework.data.mongodb.repository.MongoRepository;
4 | import org.springframework.stereotype.Repository;
5 |
6 | import java.util.Optional;
7 | @Repository
8 | public interface MongoUserRepository extends MongoRepository {
9 | Optional findByUsername(String username);
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: { browser: true, es2020: true },
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:react-hooks/recommended',
7 | ],
8 | parser: '@typescript-eslint/parser',
9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
10 | plugins: ['react-refresh'],
11 | rules: {
12 | 'react-refresh/only-export-components': 'warn',
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserWithoutPassword.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import de.neuefische.capstone.backend.models.Donation;
4 | import de.neuefische.capstone.backend.models.Participation;
5 |
6 | import java.util.List;
7 |
8 | public record MongoUserWithoutPassword(
9 | String id,
10 | String username,
11 | List donations,
12 | List participations
13 | ) {
14 | }
15 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | sonar.projectKey=ben-21_capstone-neuefische-frontend
2 | sonar.organization=ben-21
3 |
4 | # This is the name and version displayed in the SonarCloud UI.
5 | #sonar.projectName=ben-21_capstone-neuefische-frontend
6 | #sonar.projectVersion=1.0
7 |
8 |
9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
10 | sonar.sources=./frontend/src
11 |
12 | # Encoding of the source code. Default is default system encoding
13 | #sonar.sourceEncoding=UTF-8
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/ProjectNoId.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import java.util.List;
4 |
5 | public record ProjectNoId(
6 | String name,
7 | String description,
8 | Category category,
9 | List demands,
10 | int progress,
11 | int goal,
12 | String location,
13 | List donations,
14 | List participations,
15 | String userId,
16 | Image image
17 | ) {
18 | }
19 |
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/frontend/src/pages/Gallery.tsx:
--------------------------------------------------------------------------------
1 | import ProjectCard from "../components/ProjectCard.tsx";
2 | import {useFetch} from "../hooks/useFetch.tsx";
3 | import {StyledGallery} from "../GlobalStyles.tsx";
4 |
5 | export default function Gallery() {
6 |
7 | const projects = useFetch((state) => state.projects);
8 |
9 | return (
10 | <>
11 |
12 | {projects.map((project) => (
13 |
14 | ))}
15 |
16 | >
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserCreation.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 | import jakarta.validation.constraints.NotEmpty;
3 | import jakarta.validation.constraints.Pattern;
4 | import jakarta.validation.constraints.Size;
5 |
6 | public record MongoUserCreation(
7 | @NotEmpty
8 | @Size(min=4, max=30, message = "Username must be between 4 and 30 characters")
9 | String username,
10 | @Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*[0-9]).{6,20}$", message = "Invalid password")
11 | String password
12 | ) {
13 | }
14 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUser.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import de.neuefische.capstone.backend.models.Donation;
4 | import de.neuefische.capstone.backend.models.Participation;
5 | import org.springframework.data.annotation.Id;
6 | import org.springframework.data.mongodb.core.mapping.Document;
7 |
8 | import java.util.List;
9 |
10 | @Document("users")
11 | public record MongoUser(
12 | @Id
13 | String id,
14 | String username,
15 | String password,
16 | List donations,
17 | List participations
18 | ) {
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-frontend.yml:
--------------------------------------------------------------------------------
1 | name: SonarCloud-Frontend
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | sonarcloud:
10 | name: SonarCloud
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16 | - name: SonarCloud Scan
17 | uses: SonarSource/sonarcloud-github-action@master
18 | env:
19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
20 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/models/Project.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.models;
2 |
3 | import org.springframework.data.annotation.Id;
4 | import org.springframework.data.mongodb.core.mapping.Document;
5 |
6 | import java.util.List;
7 |
8 | @Document("projects")
9 | public record Project(
10 | @Id
11 | String id,
12 | String name,
13 | String description,
14 | Category category,
15 | List demands,
16 | int progress,
17 | int goal,
18 | String location,
19 | List donations,
20 | List participations,
21 | String userId,
22 | Image image
23 | ) {
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "noEmit": true,
15 | "jsx": "react-jsx",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": ["src"],
24 | "references": [{ "path": "./tsconfig.node.json" }]
25 | }
26 |
--------------------------------------------------------------------------------
/frontend/src/components/LogoutButton.tsx:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import {useNavigate} from "react-router-dom";
3 | import LoginIcon from "@mui/icons-material/Login";
4 | import {useFetch} from "../hooks/useFetch.tsx";
5 | import {StyledButton} from "../GlobalStyles.tsx";
6 |
7 | export default function LogoutButton() {
8 |
9 | const navigate = useNavigate();
10 | const me = useFetch((state) => state.me);
11 |
12 | function handleLogout() {
13 | axios.post("/api/users/logout")
14 | .catch(console.error)
15 | .then(() => me())
16 | .then(() => navigate("/"));
17 | }
18 |
19 | return (
20 | }>LOGOUT
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/user-story-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: User Story Template
3 | about: Describe this issue template's purpose here.
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ---
11 | name: User story template
12 | about: This template provides a basic structure for user story issues.
13 |
14 | ---
15 |
16 | # User story
17 | As a ..., I want to ..., so I can ...
18 |
19 | *Ideally, this is in the issue title, but if not, you can put it here. If so, delete this section.*
20 |
21 | # Acceptance criteria
22 |
23 | - [ ] This is something that can be verified to show that this user story is satisfied.
24 |
25 | # Tasks
26 | ## Frontend
27 | - [ ]
28 | ## Backend
29 | - [ ]
30 |
31 | # Sprint Ready Checklist
32 | - [ ] Acceptance criteria defined
33 | - [ ] Team understands acceptance criteria
34 | - [ ] Team has defined solution / steps to satisfy acceptance criteria
35 | - [ ] Acceptance criteria is verifiable / testable
36 |
--------------------------------------------------------------------------------
/frontend/src/components/HomeButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import HomeIcon from '@mui/icons-material/Home';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 |
6 | export default function HomeButton() {
7 |
8 | const navigate = useNavigate();
9 |
10 | function handleClick() {
11 | navigate("/")
12 | window.scrollTo(0, 0);
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | const StyledIconButton = styled(IconButton)`
23 | width: 46px;
24 | height: 46px;
25 |
26 | svg {
27 | font-size: 32px;
28 | }
29 |
30 | border-radius: 4px;
31 | border: none;
32 | background-color: var(--colorBlack);
33 | margin: 0;
34 | padding: 0;
35 | display: grid;
36 | place-items: center;
37 | cursor: pointer;
38 | `;
39 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
3 |
4 | # This workflow uses actions that are not certified by GitHub.
5 | # They are provided by a third-party and are governed by
6 | # separate terms of service, privacy policy, and support
7 | # documentation.
8 |
9 | name: Java CI with Maven
10 |
11 | on:
12 | push
13 |
14 |
15 | jobs:
16 | build:
17 |
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v3
22 | - name: Set up JDK 20
23 | uses: actions/setup-java@v3
24 | with:
25 | java-version: '20'
26 | distribution: 'temurin'
27 | cache: maven
28 | - name: Build with Maven
29 | run: mvn -B package --file backend/pom.xml
30 |
31 |
32 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/imageupload/BeanConfigCloudinary.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import com.cloudinary.Cloudinary;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | import java.util.HashMap;
8 | import java.util.Map;
9 |
10 | @Configuration
11 | public class BeanConfigCloudinary {
12 | private static final String CLOUD_NAME = "dfzzbhu3x";
13 | private static final String API_KEY = "399958539413299";
14 | private static final String API_SECRET = System.getenv("CLOUDINARY_SECRET");
15 | @Bean
16 | public Cloudinary createCloudinary() {
17 | Map config = new HashMap<>();
18 | config.put("cloud_name",CLOUD_NAME);
19 | config.put("api_key",API_KEY);
20 | config.put("api_secret",API_SECRET);
21 | return new Cloudinary(config);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/show-logs.yml:
--------------------------------------------------------------------------------
1 | name: "Get Logs"
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | get-logs:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Get logs from docker
11 | uses: appleboy/ssh-action@master
12 | with:
13 | host: capstone-project.de
14 | #Set App Name (replace "example" with "alpha"-"tango")
15 | username: cgn-java-23-2-benjamin
16 | password: ${{ secrets.SSH_PASSWORD }}
17 | #Set App Name (replace "example" with "alpha"-"tango")
18 | script: |
19 | sudo docker logs cgn-java-23-2-benjamin
20 | - name: Check the deployed service URL
21 | uses: jtalk/url-health-check-action@v3
22 | with:
23 | #Set App Name (replace "example" with "alpha"-"tango")
24 | url: http://cgn-java-23-2-benjamin.capstone-project.de
25 | max-attempts: 3
26 | retry-delay: 5s
27 | retry-all: true
--------------------------------------------------------------------------------
/frontend/src/components/SearchButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import styled from "@emotion/styled";
3 | import SearchIcon from '@mui/icons-material/Search';
4 | import {IconButton} from "@mui/material";
5 |
6 | export default function SearchButton() {
7 | const navigate = useNavigate();
8 |
9 | function handleClick() {
10 | navigate(`/search`)
11 | window.scrollTo(0, 0)
12 | }
13 |
14 | return (
15 |
16 |
17 |
18 | )
19 | }
20 |
21 | const StyledIconButton = styled(IconButton)`
22 | width: 46px;
23 | height: 46px;
24 |
25 | svg {
26 | font-size: 32px;
27 | }
28 |
29 | border-radius: 4px;
30 | border: none;
31 | background-color: var(--colorBlack);
32 | margin: 0;
33 | padding: 0;
34 | display: grid;
35 | place-items: center;
36 | cursor: pointer;
37 | `;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/AddButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import AddCircleIcon from '@mui/icons-material/AddCircle';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 |
6 | export default function AddButton() {
7 |
8 | const navigate = useNavigate();
9 |
10 | function handleClick() {
11 | navigate("/add")
12 | window.scrollTo(0, 0);
13 | }
14 |
15 | return (
16 |
17 |
18 |
19 | )
20 | }
21 |
22 | const StyledIconButton = styled(IconButton)`
23 | width: 46px;
24 | height: 46px;
25 |
26 | svg {
27 | font-size: 32px;
28 | }
29 |
30 | border-radius: 4px;
31 | border: none;
32 | background-color: var(--colorBlack);
33 | margin: 0;
34 | padding: 0;
35 | display: grid;
36 | place-items: center;
37 | cursor: pointer;
38 | `;
39 |
--------------------------------------------------------------------------------
/frontend/src/components/DonationButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import AttachMoneyIcon from '@mui/icons-material/AttachMoney';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 |
6 |
7 | type Props = {
8 | projectId: string;
9 | }
10 | export default function DonationButton(props: Props) {
11 |
12 | const navigate = useNavigate();
13 |
14 | function handleClick() {
15 | navigate(`/donate/${props.projectId}`)
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | const StyledIconButton = styled(IconButton)`
26 | width: 46px;
27 | height: 46px;
28 |
29 | svg {
30 | font-size: 32px;
31 | }
32 |
33 | border-radius: 4px;
34 | border: none;
35 | background-color: var(--colorBlack);
36 | margin: 0;
37 | padding: 0;
38 | display: grid;
39 | place-items: center;
40 | cursor: pointer;
41 | `;
42 |
--------------------------------------------------------------------------------
/frontend/src/components/ParticipationButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import VolunteerActivismIcon from '@mui/icons-material/VolunteerActivism';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 |
6 | type Props = {
7 | projectId: string;
8 | }
9 |
10 | export default function ParticipationButton(props: Props) {
11 |
12 | const navigate = useNavigate();
13 |
14 | function handleClick() {
15 | navigate(`/participate/${props.projectId}`)
16 | }
17 |
18 | return (
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | const StyledIconButton = styled(IconButton)`
26 | width: 46px;
27 | height: 46px;
28 |
29 | svg {
30 | font-size: 32px;
31 | }
32 |
33 | border-radius: 4px;
34 | border: none;
35 | background-color: var(--colorBlack);
36 | margin: 0;
37 | padding: 0;
38 | display: grid;
39 | place-items: center;
40 | cursor: pointer;
41 | `;
42 |
--------------------------------------------------------------------------------
/frontend/src/components/DeleteButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import DeleteIcon from '@mui/icons-material/Delete';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 | import {useFetch} from "../hooks/useFetch.tsx";
6 |
7 |
8 | type Props = {
9 | projectId: string;
10 | }
11 | export default function DeleteButton(props: Props) {
12 |
13 | const navigate = useNavigate();
14 | const deleteProject = useFetch(state => state.deleteProject);
15 |
16 | function handleClick() {
17 | deleteProject(props.projectId);
18 | navigate("/")
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | )
26 | }
27 |
28 | const StyledIconButton = styled(IconButton)`
29 | width: 46px;
30 | height: 46px;
31 |
32 | svg {
33 | font-size: 32px;
34 | }
35 |
36 | border-radius: 4px;
37 | border: none;
38 | background-color: var(--colorBlack);
39 | margin: 0;
40 | padding: 0;
41 | display: grid;
42 | place-items: center;
43 | cursor: pointer;
44 | `;
45 |
--------------------------------------------------------------------------------
/frontend/src/components/ProgressBarGalleryView.tsx:
--------------------------------------------------------------------------------
1 | import LinearProgress, {LinearProgressProps} from '@mui/material/LinearProgress';
2 | import Box from '@mui/material/Box';
3 | import {Project} from "../models/models.tsx";
4 | import styled from "@emotion/styled";
5 |
6 | type Props = {
7 | project: Project;
8 | }
9 |
10 | export default function ProgressBarGalleryView(props: Props) {
11 | function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) {
12 | return (
13 |
14 |
15 |
16 |
17 |
18 | );
19 | }
20 |
21 | return (
22 |
23 | {props.project.progress > 0 &&
24 | }
26 |
27 | );
28 | }
29 |
30 | const StyledBox = styled.div`
31 | margin: 10px 0 0 0;
32 | padding-left: 0;
33 | width: 100%;
34 | `;
35 |
--------------------------------------------------------------------------------
/frontend/src/pages/ShowProject.tsx:
--------------------------------------------------------------------------------
1 | import ProjectCard from "../components/ProjectCard.tsx";
2 | import {useFetch} from "../hooks/useFetch.tsx";
3 | import {useParams} from "react-router-dom";
4 | import {useEffect, useState} from "react";
5 | import {Project} from "../models/models.tsx";
6 | import {StyledBody} from "../GlobalStyles.tsx";
7 |
8 | export default function ShowProject() {
9 |
10 | const {id} = useParams();
11 | const getProjectById = useFetch(state => state.getProjectById);
12 | const [project, setProject] = useState(undefined);
13 |
14 | useEffect(() => {
15 | if (!id) {
16 | throw new Error("Id is undefined")
17 | }
18 | getProjectById(id)
19 | .then((project) => {
20 | setProject(project);
21 | })
22 | .catch(error => {
23 | console.error(error);
24 | });
25 | }, [id, getProjectById]);
26 |
27 | if (!project) {
28 | return Loading...
29 | }
30 |
31 | return (
32 |
33 |
34 |
35 | )
36 | }
37 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/imageupload/ImageUploadController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import de.neuefische.capstone.backend.models.Image;
4 | import de.neuefische.capstone.backend.models.ImageCreation;
5 | import lombok.RequiredArgsConstructor;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.web.bind.annotation.*;
8 | import org.springframework.web.multipart.MultipartFile;
9 |
10 | import java.io.IOException;
11 | import java.util.List;
12 |
13 | @RestController
14 | @RequiredArgsConstructor
15 | @RequestMapping("/api/upload")
16 | public class ImageUploadController {
17 | private final ImageUploadService imageUploadService;
18 |
19 | @GetMapping
20 | public List getAllImages() {
21 | return imageUploadService.getAllImages();
22 | }
23 |
24 | @PostMapping
25 | @ResponseStatus(HttpStatus.CREATED)
26 | public Image addImageProfile(@RequestPart("data") ImageCreation imageProfileWithoutId, @RequestPart(name="file",required = false) MultipartFile image) throws IOException {
27 | return imageUploadService.addImage(imageProfileWithoutId, image);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/imageupload/CloudinaryService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import com.cloudinary.Cloudinary;
4 | import com.cloudinary.utils.ObjectUtils;
5 | import lombok.RequiredArgsConstructor;
6 | import org.springframework.stereotype.Service;
7 | import org.springframework.web.multipart.MultipartFile;
8 |
9 | import java.io.File;
10 | import java.io.IOException;
11 | import java.util.Collections;
12 | import java.util.Map;
13 |
14 | @Service
15 | @RequiredArgsConstructor
16 | public class CloudinaryService {
17 | private final Cloudinary cloudinary;
18 | private static final String TEMP_DIRECTORY = System.getProperty("java.io.tmpdir");
19 |
20 | public String uploadImage(MultipartFile image) throws IOException {
21 | File fileToUpload = File.createTempFile("image", null, new File(TEMP_DIRECTORY));
22 | image.transferTo(fileToUpload);
23 |
24 | Map response = cloudinary.uploader().upload(fileToUpload, Collections.emptyMap());
25 |
26 | String httpUrl = response.get("url").toString();
27 | String httpsUrl = httpUrl.replace("http://", "https://");
28 |
29 | return httpsUrl;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.11.1",
14 | "@emotion/styled": "^11.11.0",
15 | "@mui/icons-material": "^5.14.1",
16 | "@mui/material": "^5.14.2",
17 | "axios": "^1.4.0",
18 | "mui-file-input": "^3.0.0",
19 | "react": "^18.2.0",
20 | "react-dom": "^18.2.0",
21 | "react-router-dom": "^6.14.2",
22 | "react-toastify": "^9.1.3",
23 | "styled-components": "^6.0.5",
24 | "zustand": "^4.3.9"
25 | },
26 | "devDependencies": {
27 | "@types/react": "^18.0.37",
28 | "@types/react-dom": "^18.0.11",
29 | "@typescript-eslint/eslint-plugin": "^5.59.0",
30 | "@typescript-eslint/parser": "^5.59.0",
31 | "@vitejs/plugin-react": "^4.0.0",
32 | "eslint": "^8.38.0",
33 | "eslint-plugin-react-hooks": "^4.6.0",
34 | "eslint-plugin-react-refresh": "^0.3.4",
35 | "typescript": "^5.0.2",
36 | "vite": "^4.3.9"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserDetailsService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import org.springframework.security.core.userdetails.User;
4 | import org.springframework.security.core.userdetails.UserDetails;
5 | import org.springframework.security.core.userdetails.UserDetailsService;
6 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
7 | import org.springframework.stereotype.Service;
8 |
9 | import java.util.Collections;
10 |
11 | @Service
12 | public class MongoUserDetailsService implements UserDetailsService {
13 |
14 | private final MongoUserRepository mongoUserRepository;
15 |
16 | public MongoUserDetailsService(MongoUserRepository mongoUserRepository) {
17 | this.mongoUserRepository = mongoUserRepository;
18 | }
19 |
20 | @Override
21 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
22 | MongoUser mongoUser = mongoUserRepository.findByUsername(username)
23 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found"));
24 |
25 | return new User(mongoUser.username(), mongoUser.password(), Collections.emptyList());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/imageupload/CloudinaryServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import com.cloudinary.Cloudinary;
4 | import com.cloudinary.Uploader;
5 | import org.junit.jupiter.api.Test;
6 | import org.springframework.mock.web.MockMultipartFile;
7 |
8 | import java.io.IOException;
9 | import java.util.Map;
10 |
11 | import static org.junit.jupiter.api.Assertions.assertEquals;
12 | import static org.mockito.Mockito.*;
13 |
14 | class CloudinaryServiceTest {
15 | Cloudinary cloudinary = mock(Cloudinary.class);
16 | Uploader uploader = mock(Uploader.class);
17 | CloudinaryService cloudinaryService = new CloudinaryService(cloudinary);
18 |
19 | @Test
20 | void uploadImage() throws IOException {
21 | //GIVEN
22 | MockMultipartFile mockMultipartFile = new MockMultipartFile("file", "test".getBytes());
23 |
24 | when(cloudinary.uploader()).thenReturn(uploader);
25 | when(uploader.upload(any(), any())).thenReturn(Map.of("url", "test-url"));
26 |
27 | //WHEN
28 | String actual = cloudinaryService.uploadImage(mockMultipartFile);
29 |
30 | //THEN
31 | verify(uploader).upload(any(), any());
32 | assertEquals("test-url", actual);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/imageupload/ImageUploadService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import de.neuefische.capstone.backend.models.Image;
4 | import de.neuefische.capstone.backend.models.ImageCreation;
5 | import de.neuefische.capstone.backend.services.IdService;
6 | import lombok.RequiredArgsConstructor;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.web.multipart.MultipartFile;
9 |
10 | import java.io.IOException;
11 | import java.util.List;
12 |
13 | @Service
14 | @RequiredArgsConstructor
15 | public class ImageUploadService {
16 |
17 | private final ImageUploadRepo imageUploadRepo;
18 | private final IdService idService;
19 | private final CloudinaryService cloudinaryService;
20 |
21 | public List getAllImages() {
22 | return imageUploadRepo.findAll();
23 | }
24 |
25 | public Image addImage(ImageCreation imageWithoutId, MultipartFile image) throws IOException {
26 | String id = idService.createRandomId();
27 | String url = null;
28 |
29 | if (image!= null) {
30 | url = cloudinaryService.uploadImage(image);
31 | }
32 | Image imageToSave = new Image(id, imageWithoutId.name(),url);
33 | return imageUploadRepo.save(imageToSave);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/frontend/src/components/UserButton.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {useNavigate} from "react-router-dom";
3 | import {IconButton} from "@mui/material";
4 | import AccountCircleIcon from '@mui/icons-material/AccountCircle';
5 | import {useFetch} from "../hooks/useFetch.tsx";
6 | import {useEffect} from "react";
7 |
8 | export default function UserButton() {
9 |
10 | const navigate = useNavigate();
11 | const me = useFetch(state => state.me);
12 | const userName = useFetch(state => state.userName);
13 |
14 | useEffect(() => {
15 | me();
16 | }, [me, userName]);
17 |
18 | function handleClick() {
19 | if (userName === "anonymousUser") {
20 | navigate("/login")
21 | } else {
22 | navigate("/profile")
23 | }
24 | }
25 |
26 | return (
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | const StyledIconButton = styled(IconButton)`
34 | width: 46px;
35 | height: 46px;
36 |
37 | svg {
38 | font-size: 32px;
39 | }
40 |
41 | border-radius: 4px;
42 | border: none;
43 | background-color: var(--colorBlack);
44 | margin: 0;
45 | padding: 0;
46 | display: grid;
47 | place-items: center;
48 | cursor: pointer;
49 | `;
50 |
--------------------------------------------------------------------------------
/.github/workflows/sonar-backend.yml:
--------------------------------------------------------------------------------
1 | name: SonarCloud-Backend
2 | on:
3 | push:
4 | branches:
5 | - main
6 | pull_request:
7 | types: [opened, synchronize, reopened]
8 | jobs:
9 | build:
10 | name: Build and analyze
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | with:
15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
16 | - name: Set up JDK 20
17 | uses: actions/setup-java@v3
18 | with:
19 | java-version: 20
20 | distribution: 'zulu' # Alternative distribution options are available.
21 | - name: Cache SonarCloud packages
22 | uses: actions/cache@v3
23 | with:
24 | path: ~/.sonar/cache
25 | key: ${{ runner.os }}-sonar
26 | restore-keys: ${{ runner.os }}-sonar
27 | - name: Cache Maven packages
28 | uses: actions/cache@v3
29 | with:
30 | path: ~/.m2
31 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
32 | restore-keys: ${{ runner.os }}-m2
33 | - name: Build and analyze
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
36 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
37 | run: mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=ben-21_capstone-neuefische-backend --file backend/pom.xml
--------------------------------------------------------------------------------
/frontend/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/pages/FilteredGallery.tsx:
--------------------------------------------------------------------------------
1 | import ProjectCard from "../components/ProjectCard.tsx";
2 | import {useFetch} from "../hooks/useFetch.tsx";
3 | import {Project} from "../models/models.tsx";
4 | import {useLocation} from "react-router-dom";
5 | import {useEffect, useState} from "react";
6 | import {StyledBody, StyledGallery, StyledMain} from "../GlobalStyles.tsx";
7 |
8 | export default function FilteredGallery() {
9 |
10 | const location = useLocation();
11 | const searchParams = new URLSearchParams(location.search);
12 | const filter = searchParams.get("filter");
13 | const projects = useFetch((state) => state.projects);
14 | const fetchProjects = useFetch((state) => state.fetchProjects);
15 | const [filteredProjects, setFilteredProjects] = useState([]);
16 |
17 | useEffect(() => {
18 | fetchProjects();
19 | }, [fetchProjects]);
20 |
21 | useEffect(() => {
22 | if (filter === null || filter === "all") {
23 | setFilteredProjects(projects);
24 | } else {
25 | setFilteredProjects(projects.filter((project) => project.category === filter));
26 | }
27 | }, [filter, projects]);
28 |
29 | return (
30 |
31 |
32 |
33 | {filteredProjects.map((project) => (
34 |
35 | ))}
36 |
37 |
38 |
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import jakarta.servlet.http.HttpSession;
4 | import jakarta.validation.Valid;
5 | import lombok.RequiredArgsConstructor;
6 | import org.springframework.security.core.context.SecurityContextHolder;
7 | import org.springframework.web.bind.annotation.*;
8 |
9 | @RestController
10 | @RequiredArgsConstructor
11 | @RequestMapping("/api/users")
12 | public class MongoUserController {
13 | private final MongoUserService mongoUserService;
14 |
15 | @GetMapping("/me")
16 | public String getUserInfo() {
17 | return SecurityContextHolder.getContext().getAuthentication().getName();
18 | }
19 |
20 | @GetMapping("/me-object")
21 | public MongoUserWithoutPassword getUserObject() {
22 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
23 |
24 | return mongoUserService.findByUsername(username);
25 | }
26 |
27 | @PostMapping("/login")
28 | public String login() {
29 | return SecurityContextHolder.getContext().getAuthentication().getName();
30 | }
31 |
32 | @PostMapping("/logout")
33 | public void logout(HttpSession httpSession) {
34 | httpSession.invalidate();
35 | SecurityContextHolder.clearContext();
36 | }
37 |
38 | @PostMapping("/register")
39 | public String register(@Valid @RequestBody MongoUserCreation mongoUserWithoutId) {
40 | mongoUserService.registerUser(mongoUserWithoutId);
41 | return "registered";
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/frontend/src/models/models.tsx:
--------------------------------------------------------------------------------
1 |
2 | export type Project = {
3 | id: string;
4 | name: string;
5 | description: string;
6 | category: "DONATION" | "PARTICIPATION";
7 | demands: Demand[];
8 | progress: number;
9 | goal: string;
10 | location: string;
11 | donations: Donation[];
12 | participations: Participation[];
13 | userId: string;
14 | image: Image;
15 | }
16 |
17 | export type ProjectCreation = {
18 | name: string;
19 | description: string;
20 | category: "DONATION" | "PARTICIPATION";
21 | demands: Demand[];
22 | location: string;
23 | goal: string;
24 | image: Image;
25 | }
26 |
27 | export type ProjectNoId = {
28 | name: string;
29 | description: string;
30 | category: "DONATION" | "PARTICIPATION";
31 | demands: Demand[];
32 | progress: number;
33 | goal: string;
34 | location: string;
35 | donations: Donation[];
36 | participations: Participation[];
37 | userId: string;
38 | image: Image;
39 | }
40 |
41 | export type User = {
42 | id: string;
43 | username: string;
44 | donations: Donation[];
45 | participations: Participation[];
46 | }
47 |
48 | export type Image = {
49 | id: string;
50 | name: string;
51 | url: string;
52 | }
53 |
54 | export type Demand = "MONEYDONATION" | "DONATIONINKIND" | "FOODDONATION" | "DRUGDONATION";
55 | export type Donation = {id: string, projectId: string, projectName: string, donorName: string, amount: string, userId: string};
56 | export type DonationCreation = {projectId: string, projectName: string, amount: string};
57 | export type Participation = {id: string, projectId: string, projectName: string, participationName: string, userId: string};
58 | export type ParticipationCreation = {projectId: string, projectName: string};
59 | export type ImageCreation = {name: string};
60 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/security/MongoUserDetailsServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.security.core.userdetails.User;
5 | import org.springframework.security.core.userdetails.UserDetails;
6 |
7 | import java.util.ArrayList;
8 | import java.util.Collections;
9 | import java.util.Optional;
10 |
11 | import static org.junit.jupiter.api.Assertions.assertEquals;
12 | import static org.mockito.Mockito.*;
13 |
14 |
15 | class MongoUserDetailsServiceTest {
16 |
17 | private final MongoUserRepository mongoUserRepository = mock(MongoUserRepository.class);
18 | private final MongoUserDetailsService mongoUserDetailsService = new MongoUserDetailsService(mongoUserRepository);
19 |
20 | @Test
21 | void returnUser_WhenUsernameExists() {
22 | // Given
23 | MongoUser mongoUser = new MongoUser("1", "test", "test", new ArrayList<>(), new ArrayList<>());
24 | when(mongoUserRepository.findByUsername("test"))
25 | .thenReturn(Optional.of(mongoUser));
26 |
27 | // When
28 | UserDetails actual = mongoUserDetailsService.loadUserByUsername("test");
29 |
30 | // Then
31 | UserDetails expected = new User("test", "test", Collections.emptyList());
32 | assertEquals(expected, actual);
33 | }
34 |
35 | @Test
36 | void throwException_WhenUsernameDoesNotExist() {
37 | // Given
38 | when(mongoUserRepository.findByUsername("test"))
39 | .thenReturn(Optional.empty());
40 |
41 | // When / Then
42 | try {
43 | mongoUserDetailsService.loadUserByUsername("test");
44 | } catch (Exception e) {
45 | assertEquals("Username test not found", e.getMessage());
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/frontend/src/components/ProgressBar.tsx:
--------------------------------------------------------------------------------
1 | import LinearProgress, {LinearProgressProps} from '@mui/material/LinearProgress';
2 | import Box from '@mui/material/Box';
3 | import {Project} from "../models/models.tsx";
4 | import styled from "@emotion/styled";
5 | import {useEffect, useState} from "react";
6 |
7 | type Props = {
8 | project: Project;
9 | }
10 |
11 | export default function ProgressBar(props: Props) {
12 | function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) {
13 | return (
14 | <>
15 |
16 |
17 |
18 |
19 |
20 |
21 | Target achieved by {props.value}%
22 |
23 | >
24 | );
25 | }
26 |
27 | const [progress, setProgress] = useState(0);
28 |
29 | useEffect(() => {
30 | const timer = setInterval(() => {
31 | setProgress((prevProgress) => {
32 | if (prevProgress >= props.project.progress) {
33 | clearInterval(timer);
34 | return props.project.progress;
35 | }
36 | return prevProgress + 1;
37 | });
38 | }, 100);
39 |
40 | return () => {
41 | clearInterval(timer);
42 | };
43 | }, [props.project.progress]);
44 |
45 | return (
46 |
47 | {props.project.progress > 0 &&
48 | }
49 |
50 | );
51 | }
52 |
53 | const StyledBox = styled.div`
54 | margin: 10px 10% 0 12%;
55 | `;
56 |
57 | const StyledPercentageDiv = styled.div`
58 | font-family: "Roboto", sans-serif;
59 | font-weight: 400;
60 | margin-top: 10px;
61 | `;
62 |
--------------------------------------------------------------------------------
/frontend/src/pages/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import {ChangeEvent, FormEvent, useState} from "react";
2 | import {useNavigate} from "react-router-dom";
3 | import {useFetch} from "../hooks/useFetch.tsx";
4 | import LoginIcon from '@mui/icons-material/Login';
5 | import {StyledBody, StyledButton, StyledForm, StyledTextField} from "../GlobalStyles.tsx";
6 |
7 | export default function LoginPage() {
8 |
9 | const [username, setUsername] = useState("");
10 | const [password, setPassword] = useState("");
11 | const navigate = useNavigate();
12 | const login = useFetch((state) => state.login);
13 |
14 | function handleUsernameInput(event: ChangeEvent) {
15 | setUsername(event.currentTarget.value);
16 | }
17 |
18 | function handlePasswordInput(event: ChangeEvent) {
19 | setPassword(event.currentTarget.value);
20 | }
21 |
22 | function handleSubmit(event: FormEvent) {
23 | event.preventDefault();
24 | login(username, password, navigate);
25 | }
26 |
27 | return (
28 |
29 |
30 |
34 |
39 | }>LOGIN
41 | {/*OR*/}
42 | {/* navigate("/register")} variant="outlined"*/}
43 | {/* endIcon={}>REGISTER*/}
44 |
45 |
46 | );
47 | }
48 |
--------------------------------------------------------------------------------
/frontend/src/components/EditButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import EditIcon from '@mui/icons-material/Edit';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 | import {useFetch} from "../hooks/useFetch.tsx";
6 | import {useEffect, useState} from "react";
7 | import {Project} from "../models/models.tsx";
8 |
9 |
10 | type Props = {
11 | projectId: string;
12 | }
13 |
14 | export default function EditButton(props: Props) {
15 |
16 | const navigate = useNavigate();
17 | const user = useFetch(state => state.user);
18 | const meObject = useFetch(state => state.meObject);
19 | const getProjectById = useFetch(state => state.getProjectById);
20 | const [project, setProject] = useState(undefined);
21 | const [checkUser, setCheckUser] = useState(false);
22 |
23 | useEffect(() => {
24 | meObject();
25 | }, [meObject]);
26 |
27 | useEffect(() => {
28 | if (props.projectId) {
29 | getProjectById(props.projectId)
30 | .then((project) => {
31 | setProject(project);
32 | })
33 | .catch(error => {
34 | console.error(error);
35 | });
36 | }
37 | }, [props.projectId, getProjectById]);
38 |
39 | useEffect(() => {
40 | if (project?.userId === user?.id) {
41 | setCheckUser(true);
42 | } else {
43 | setCheckUser(false);
44 | }
45 | }, [project, user]);
46 |
47 | function handleClick() {
48 | navigate(`/edit/${props.projectId}`)
49 | }
50 |
51 | return (
52 | <>
53 | {checkUser &&
54 |
55 |
56 |
57 | }
58 | >
59 | )
60 | }
61 |
62 | const StyledIconButton = styled(IconButton)`
63 | width: 46px;
64 | height: 46px;
65 |
66 | svg {
67 | font-size: 32px;
68 | }
69 |
70 | border-radius: 4px;
71 | border: none;
72 | background-color: var(--colorBlack);
73 | margin: 0;
74 | padding: 0;
75 | display: grid;
76 | place-items: center;
77 | cursor: pointer;
78 | `;
79 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/ProjectController.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import de.neuefische.capstone.backend.models.*;
4 | import lombok.RequiredArgsConstructor;
5 | import org.springframework.http.ResponseEntity;
6 | import org.springframework.web.bind.annotation.*;
7 |
8 | import java.util.List;
9 | import java.util.NoSuchElementException;
10 |
11 | @RestController
12 | @RequiredArgsConstructor
13 | @RequestMapping("/api/projects")
14 | public class ProjectController {
15 |
16 | private final ProjectService projectService;
17 |
18 | @PostMapping
19 | public Project addProject(@RequestBody ProjectCreation projectCreation) {
20 | return projectService.addProject(projectCreation);
21 | }
22 |
23 | @GetMapping("{id}")
24 | public Project getProjectById(@PathVariable String id) {
25 | return projectService.getProjectById(id);
26 | }
27 |
28 | @GetMapping
29 | public List getAllProjects() {
30 | return projectService.getAllProjects();
31 | }
32 |
33 | @PutMapping("{id}")
34 | public ResponseEntity updateProject(@PathVariable String id, @RequestBody ProjectNoId projectNoId) {
35 | try {
36 | Project updatedProject = projectService.updateProject(id, projectNoId);
37 | return ResponseEntity.ok(updatedProject);
38 | } catch (NoSuchElementException e) {
39 | return ResponseEntity.notFound().build();
40 | }
41 | }
42 |
43 | @DeleteMapping("{id}")
44 | public ResponseEntity deleteProject(@PathVariable String id) {
45 |
46 | try {
47 | projectService.deleteProject(id);
48 | return ResponseEntity.ok("Project deleted successfully");
49 |
50 | } catch (NoSuchElementException e) {
51 | return ResponseEntity.notFound().build();
52 | }
53 | }
54 |
55 | @PostMapping("/donate/{id}")
56 | public Project addDonation(@PathVariable String id, @RequestBody DonationCreation donationCreation) {
57 | return projectService.addDonation(id, donationCreation);
58 | }
59 |
60 | @PostMapping("/participate/{id}")
61 | public Project addParticipation(@PathVariable String id, @RequestBody ParticipationCreation participationCreation) {
62 | return projectService.addParticipation(id, participationCreation);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/ProjectCalculations.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 |
4 | import de.neuefische.capstone.backend.models.Category;
5 | import de.neuefische.capstone.backend.models.Project;
6 | import org.springframework.stereotype.Component;
7 |
8 | import java.math.BigDecimal;
9 | import java.math.RoundingMode;
10 |
11 | @Component
12 | public class ProjectCalculations {
13 |
14 | public Project calculateProgressForDonations(Project project) {
15 | if (project.category() == Category.DONATION) {
16 | BigDecimal sumOfDonations = project.donations().stream().reduce(BigDecimal.ZERO, (sum, donation) -> sum.add(donation.amount()), BigDecimal::add);
17 | int newProgress = sumOfDonations.divide(BigDecimal.valueOf(project.goal()), 2, RoundingMode.DOWN).multiply(BigDecimal.valueOf(100)).intValue();
18 | return new Project(
19 | project.id(),
20 | project.name(),
21 | project.description(),
22 | project.category(),
23 | project.demands(),
24 | newProgress,
25 | project.goal(),
26 | project.location(),
27 | project.donations(),
28 | project.participations(),
29 | project.userId(),
30 | project.image()
31 | );
32 | }
33 | return project;
34 | }
35 |
36 | public Project calculateProgressForParticipations(Project project) {
37 | if (project.category() == Category.PARTICIPATION) {
38 | int newParticipations = project.participations().size();
39 | int newProgress = (int) ((double) newParticipations / project.goal() * 100);
40 |
41 | return new Project(
42 | project.id(),
43 | project.name(),
44 | project.description(),
45 | project.category(),
46 | project.demands(),
47 | newProgress,
48 | project.goal(),
49 | project.location(),
50 | project.donations(),
51 | project.participations(),
52 | project.userId(),
53 | project.image()
54 | );
55 | }
56 | return project;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/imageupload/ImageUploadServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import de.neuefische.capstone.backend.models.Image;
4 | import de.neuefische.capstone.backend.models.ImageCreation;
5 | import de.neuefische.capstone.backend.services.IdService;
6 | import org.junit.jupiter.api.Test;
7 | import org.springframework.mock.web.MockMultipartFile;
8 |
9 | import java.io.IOException;
10 | import java.util.Collections;
11 | import java.util.List;
12 |
13 |
14 |
15 | import static org.assertj.core.api.Assertions.assertThat;
16 | import static org.junit.jupiter.api.Assertions.assertEquals;
17 | import static org.mockito.Mockito.*;
18 |
19 | class ImageUploadServiceTest {
20 | ImageUploadRepo imageUploadRepo = mock(ImageUploadRepo.class);
21 | CloudinaryService cloudinaryService = mock(CloudinaryService.class);
22 | IdService idService = mock(IdService.class);
23 | ImageUploadService imageUploadService = new ImageUploadService(imageUploadRepo, idService, cloudinaryService);
24 |
25 | @Test
26 | void getAllProfiles() {
27 | //GIVEN
28 | Image image = new Image("1", "docker-image", "test-url.de");
29 | when(imageUploadRepo.findAll())
30 | .thenReturn(Collections.singletonList(image));
31 | //WHEN
32 | List actual = imageUploadService.getAllImages();
33 |
34 | //THEN
35 | assertThat(actual)
36 | .containsOnly(image);
37 | }
38 |
39 | @Test
40 | void postImage() throws IOException {
41 | //GIVEN
42 | ImageCreation imageCreation = new ImageCreation("docker-image");
43 | Image expected = new Image("1", "docker-image", "test-url.de");
44 | MockMultipartFile file = new MockMultipartFile("docker-image", "irgendwas".getBytes());
45 |
46 | when(cloudinaryService.uploadImage(file)).thenReturn("test-url.de");
47 | when(imageUploadRepo.save(expected)).thenReturn(expected);
48 | when(idService.createRandomId()).thenReturn("1");
49 |
50 |
51 | //WHEN
52 | Image actual = imageUploadService.addImage(imageCreation, file);
53 |
54 | //THEN
55 | verify(cloudinaryService).uploadImage(file);
56 | verify(imageUploadRepo).save(expected);
57 | verify(idService).createRandomId();
58 | assertEquals(expected, actual);
59 | }
60 | }
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/frontend/src/components/FilterButton.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate} from "react-router-dom";
2 | import FilterAltIcon from '@mui/icons-material/FilterAlt';
3 | import {IconButton} from "@mui/material";
4 | import styled from "@emotion/styled";
5 | import * as React from 'react';
6 | import Menu from '@mui/material/Menu';
7 | import MenuItem from '@mui/material/MenuItem';
8 |
9 |
10 | export default function FilterButton() {
11 |
12 | const navigate = useNavigate();
13 | const [anchorEl, setAnchorEl] = React.useState(null);
14 | const open = Boolean(anchorEl);
15 |
16 | const handleClick = (event: React.MouseEvent) => {
17 | setAnchorEl(event.currentTarget);
18 | };
19 |
20 | const handleClose = (filter: string) => {
21 | if (filter === "stay") {
22 | setAnchorEl(null)
23 | } else {
24 | navigate(`/filter?filter=${filter}`);
25 | window.scrollTo(0, 0);
26 | setAnchorEl(null);
27 | }
28 | };
29 |
30 | return (
31 | <>
32 |
39 |
40 |
41 |
54 | >
55 | )
56 | }
57 |
58 | const StyledIconButton = styled(IconButton)`
59 | width: 46px;
60 | height: 46px;
61 |
62 | svg {
63 | font-size: 32px;
64 | }
65 |
66 | border-radius: 4px;
67 | border: none;
68 | background-color: var(--colorBlack);
69 | margin: 0;
70 | padding: 0;
71 | display: grid;
72 | place-items: center;
73 | cursor: pointer;
74 | `;
75 |
--------------------------------------------------------------------------------
/frontend/src/GlobalStyles.tsx:
--------------------------------------------------------------------------------
1 | import {createGlobalStyle} from "styled-components";
2 | import {Button, createTheme, FormControl, TextField, ToggleButton, ToggleButtonGroup} from "@mui/material";
3 | import styled from "@emotion/styled";
4 |
5 | const GlobalStyles = createGlobalStyle`
6 | body {
7 | background-color: #FF644A;
8 | }
9 | `;
10 | export default GlobalStyles;
11 |
12 |
13 | export const StyledTheme = createTheme({
14 | components: {
15 | MuiPaper: {
16 | styleOverrides: {
17 | root: {
18 | backgroundColor: "#EBE7D8"
19 | }
20 | }
21 | }
22 | }
23 | });
24 |
25 | export const StyledBody = styled.div`
26 | display: flex;
27 | flex-direction: column;
28 | justify-content: flex-start;
29 | gap: 1.1em;
30 | margin-bottom: 100px;
31 | margin-top: 101px;
32 | `;
33 |
34 | export const StyledForm = styled.form`
35 | display: flex;
36 | flex-direction: column;
37 | align-items: center;
38 | justify-content: center;
39 | gap: 1.1em;
40 | background-color: #EBE7D8;
41 | border-radius: 4px;
42 | padding: 10px;
43 | `;
44 |
45 | export const StyledTextField = styled(TextField)`
46 | width: 100%;
47 | font-family: "Roboto", sans-serif;
48 | border-radius: 4px;
49 | `;
50 |
51 | export const StyledButton = styled(Button)`
52 | width: 100%;
53 | height: 56px;
54 | color: #163E56;
55 | border-color: #163E56;
56 | `;
57 |
58 | export const StyledSpan = styled.span`
59 | font-family: "Roboto Light", sans-serif;
60 | font-size: 0.8em;
61 | color: #163E56;
62 | `;
63 |
64 | export const StyledGallery = styled.div`
65 | display: flex;
66 | flex-direction: column;
67 | align-items: center;
68 | justify-content: center;
69 | gap: 1.1em;
70 | `;
71 |
72 | export const StyledSearchBar = styled.div`
73 | position: fixed;
74 | transform: translateX(-50%);
75 | left: 50%;
76 | top: 100px;
77 | width: 90%;
78 | border-radius: 4px;
79 | `;
80 |
81 | export const StyledToggleGroup = styled(ToggleButtonGroup)`
82 | font-family: "Roboto Light", sans-serif;
83 | display: flex;
84 | justify-content: center;
85 | width: 100%;
86 | `;
87 |
88 | export const StyledToggleButton = styled(ToggleButton)`
89 | font-family: "Roboto", sans-serif;
90 | display: flex;
91 | justify-content: center;
92 | width: 100%;
93 | height: 56px;
94 |
95 | &.Mui-selected {
96 | color: #163E56;
97 | }
98 | `;
99 |
100 | export const StyledChipFormControl = styled(FormControl)`
101 | width: 100%;
102 | border-radius: 4px;
103 | `;
104 |
105 | export const StyledMain = styled.main`
106 | display: flex;
107 | flex-direction: column;
108 | justify-content: center;
109 | gap: 20px;
110 | `;
111 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/SecurityConfig.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import org.springframework.context.annotation.Bean;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.http.HttpMethod;
6 | import org.springframework.security.config.Customizer;
7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9 | import org.springframework.security.config.http.SessionCreationPolicy;
10 | import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
11 | import org.springframework.security.crypto.password.PasswordEncoder;
12 | import org.springframework.security.web.SecurityFilterChain;
13 | import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
14 | import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
15 |
16 |
17 | @EnableWebSecurity
18 | @Configuration
19 | public class SecurityConfig {
20 |
21 | @Bean
22 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
23 | CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler();
24 | requestHandler.setCsrfRequestAttributeName(null);
25 |
26 | return http.csrf(csrf -> csrf
27 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
28 | .csrfTokenRequestHandler(requestHandler))
29 | .httpBasic(Customizer.withDefaults())
30 | .sessionManagement(httpSecuritySessionManagementConfigurer ->
31 | httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.ALWAYS))
32 | .authorizeHttpRequests(httpRequests ->
33 | httpRequests
34 | .requestMatchers(HttpMethod.GET, "/api/projects").permitAll()
35 | .requestMatchers("/api/projects").authenticated()
36 | .requestMatchers(HttpMethod.GET, "/api/projects/**").permitAll()
37 | .requestMatchers("/api/projects/**").authenticated()
38 | .requestMatchers("/api/users/me").permitAll()
39 | .requestMatchers("/api/users/me-object").permitAll()
40 | .requestMatchers("/api/users/register").permitAll()
41 | .requestMatchers("/api/users/profile").authenticated()
42 | .anyRequest().permitAll()
43 | )
44 | .build();
45 | }
46 |
47 | @Bean
48 | public PasswordEncoder passwordEncoder() {
49 | return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/security/MongoUserService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import de.neuefische.capstone.backend.services.IdService;
4 | import lombok.RequiredArgsConstructor;
5 | import org.springframework.security.core.userdetails.UsernameNotFoundException;
6 | import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
7 | import org.springframework.security.crypto.password.PasswordEncoder;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.util.ArrayList;
11 |
12 | @Service
13 | @RequiredArgsConstructor
14 | public class MongoUserService {
15 |
16 | private final MongoUserRepository mongoUserRepository;
17 | private final IdService idService;
18 |
19 | public MongoUserWithoutPassword findByUsername(String username) {
20 | if (mongoUserRepository.findByUsername(username).isEmpty()) {
21 | return new MongoUserWithoutPassword("unknown", "anonymousUser", new ArrayList<>(), new ArrayList<>());
22 | }
23 | MongoUser mongoUser = mongoUserRepository.findByUsername(username)
24 | .orElseThrow(() -> new UsernameNotFoundException("Username " + username + " not found"));
25 |
26 | return new MongoUserWithoutPassword(mongoUser.id(), mongoUser.username(), mongoUser.donations(), mongoUser.participations());
27 | }
28 |
29 | public void registerUser(MongoUserCreation mongoUserWithoutId) {
30 | if (mongoUserRepository.findByUsername(mongoUserWithoutId.username()).isPresent()) {
31 | throw new IllegalArgumentException("User already exists");
32 | }
33 |
34 | PasswordEncoder encoder = new Argon2PasswordEncoder(16, 32, 8, 1 << 16, 4);
35 | String encodedPassword = encoder.encode(mongoUserWithoutId.password());
36 |
37 | MongoUser newUser = new MongoUser(idService.createRandomId(), mongoUserWithoutId.username(), encodedPassword, new ArrayList<>(), new ArrayList<>());
38 | mongoUserRepository.insert(newUser);
39 | }
40 |
41 | public MongoUserWithoutPassword updateUser(MongoUserWithoutPassword mongoUserWithoutPassword) {
42 | MongoUser mongoUser = mongoUserRepository.findById(mongoUserWithoutPassword.id()).orElseThrow(() -> new UsernameNotFoundException("Username " + mongoUserWithoutPassword.username() + " not found"));
43 | MongoUser updatedUser = new MongoUser(
44 | mongoUserWithoutPassword.id(),
45 | mongoUserWithoutPassword.username(),
46 | mongoUser.password(),
47 | mongoUserWithoutPassword.donations(),
48 | mongoUserWithoutPassword.participations());
49 |
50 | MongoUser returnUser = mongoUserRepository.save(updatedUser);
51 | return new MongoUserWithoutPassword(returnUser.id(), returnUser.username(), returnUser.donations(), returnUser.participations());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/frontend/src/components/TopDonators.tsx:
--------------------------------------------------------------------------------
1 | import {Project} from "../models/models.tsx";
2 | import styled from "@emotion/styled";
3 | import Avatar from '@mui/material/Avatar';
4 |
5 | type Props = {
6 | project: Project;
7 | }
8 |
9 | export default function TopDonators(props: Props) {
10 |
11 | const maxDonation = Math.max(...props.project.donations.map(donation => parseFloat(donation.amount)));
12 | const sortedDonations = props.project.donations.slice().sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount));
13 | const topDonations = sortedDonations.slice(0, 5);
14 |
15 | function stringAvatar(name: string) {
16 | const nameParts = name.split(' ');
17 | return {
18 | sx: {
19 | bgcolor: stringToColor(name),
20 | },
21 | children: nameParts.length > 1
22 | ? `${nameParts[0][0]}${nameParts[1][0]}`
23 | : `${nameParts[0][0]}`,
24 | };
25 | }
26 |
27 | function stringToColor(string: string) {
28 | let hash = 0;
29 | for (let i = 0; i < string.length; i += 1) {
30 | hash = string.charCodeAt(i) + ((hash << 5) - hash);
31 | }
32 | let color = '#';
33 | for (let i = 0; i < 3; i += 1) {
34 | const value = (hash >> (i * 8)) & 0xff;
35 | color += `00${value.toString(16)}`.slice(-2);
36 | }
37 | return color;
38 | }
39 |
40 | return (
41 |
42 | Top Donations
43 |
44 | {topDonations.map(donation =>
45 |
46 |
47 |
48 | {donation.amount} EUR
49 |
50 |
51 | )}
52 |
53 |
54 | )
55 | }
56 |
57 | const StyledTopDonators = styled.div`
58 | margin: 30px 0 10px 0;
59 | `;
60 |
61 | const StyledDonationChart = styled.div`
62 | display: flex;
63 | flex-direction: column;
64 | gap: 10px;
65 | margin: 0 10px;
66 | `;
67 |
68 | const StyledDonationItem = styled.div`
69 | display: flex;
70 | align-items: center;
71 | `;
72 |
73 | const StyledDonationBar = styled.div`
74 | min-width: 50px;
75 | background-color: #e74c3c;
76 | color: white;
77 | padding: 5px;
78 | margin-left: 15px;
79 | border-radius: 5px;
80 | display: flex;
81 | justify-content: flex-end;
82 | `;
83 |
84 | const StyledHeadLine = styled.h2`
85 | padding: 0 0 10px 10px;
86 | margin: 0;
87 | font-family: "Robot", sans-serif;
88 | font-weight: 400;
89 | `;
90 |
--------------------------------------------------------------------------------
/frontend/src/App.tsx:
--------------------------------------------------------------------------------
1 | import {Route, Routes} from "react-router-dom";
2 | import {ToastContainer} from "react-toastify";
3 | import 'react-toastify/dist/ReactToastify.css';
4 | import {useFetch} from "./hooks/useFetch.tsx";
5 | import {useEffect, useState} from "react";
6 | import ShowProject from "./pages/ShowProject.tsx";
7 | import AddEditProject from "./pages/AddEditProject.tsx";
8 | import NavigationBar from "./components/NavigationBar.tsx";
9 | import Header from "./components/Header.tsx";
10 | import AddDonationOrParticipation from "./pages/AddDonationOrParticipation.tsx";
11 | import LoginPage from "./pages/LoginPage.tsx";
12 | import RegisterPage from "./pages/RegisterPage.tsx";
13 | import UserProfile from "./pages/UserProfile.tsx";
14 | import ProtectedRoutes from "./components/ProtectedRoutes.tsx";
15 | import Home from "./pages/Home.tsx";
16 | import FilteredGallery from "./pages/FilteredGallery.tsx";
17 | import SearchGallery from "./pages/SearchGallary.tsx";
18 | import {ThemeProvider} from "@mui/material";
19 | import GlobalStyles, {StyledTheme} from "./GlobalStyles.tsx";
20 |
21 | export default function App() {
22 |
23 | const fetchProjects = useFetch((state) => state.fetchProjects);
24 | const [initialLoad, setInitialLoad] = useState(true);
25 | const me = useFetch(state => state.me);
26 | const meObject = useFetch(state => state.meObject);
27 | const userName = useFetch(state => state.userName);
28 |
29 | useEffect(() => {
30 | try {
31 | fetchProjects();
32 | me();
33 | meObject();
34 | } catch (error) {
35 | console.error(error);
36 | } finally {
37 | setInitialLoad(false);
38 | }
39 | }, [fetchProjects, me, meObject]);
40 |
41 | if (initialLoad) return null;
42 |
43 | return (
44 | <>
45 |
46 |
47 |
48 |
49 |
50 | }>
51 | }/>
52 | }/>
53 | }/>
54 | }/>
55 | }/>
56 |
57 | }/>
58 | }/>
59 | }/>
60 | }/>
61 | }/>
62 | }/>
63 |
64 |
65 |
66 | >
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/frontend/src/pages/UserProfile.tsx:
--------------------------------------------------------------------------------
1 | import styled from "@emotion/styled";
2 | import {SelectChangeEvent} from "@mui/material";
3 | import {useFetch} from "../hooks/useFetch.tsx";
4 | import EditIcon from "@mui/icons-material/Edit";
5 | import {useEffect, useState} from "react";
6 | import LogoutButton from "../components/LogoutButton.tsx";
7 | import FilterUserData from "../components/FilterUserData.tsx";
8 | import Box from '@mui/material/Box';
9 | import InputLabel from '@mui/material/InputLabel';
10 | import MenuItem from '@mui/material/MenuItem';
11 | import FormControl from '@mui/material/FormControl';
12 | import Select from '@mui/material/Select';
13 | import {StyledBody, StyledButton, StyledTextField} from "../GlobalStyles.tsx";
14 |
15 | export default function UserProfile() {
16 |
17 | const user = useFetch(state => state.user);
18 | const meObject = useFetch(state => state.meObject);
19 | const [filter, setFilter] = useState("My Projects");
20 |
21 | useEffect(() => {
22 | meObject();
23 | }, [meObject]);
24 |
25 | const handleFilterChange = (event: SelectChangeEvent) => {
26 | setFilter(event.target.value);
27 | };
28 |
29 | return (
30 |
31 |
32 | Personal Data
33 |
37 | }>EDIT USERDATA
39 |
40 | Project Data
41 |
42 |
43 | Filter
44 |
54 |
55 |
56 |
57 |
58 |
59 | )
60 | }
61 |
62 | const StyledProfile = styled.div`
63 | font-family: "Roboto", sans-serif;
64 | background-color: #EBE7D8;
65 | border-radius: 4px;
66 | padding: 0 10px 10px 10px;
67 | `;
68 |
69 | const StyledHeadLine = styled.h2`
70 | padding-left: 0;
71 | margin-top: 10px;
72 | margin-bottom: 6px;
73 | `;
74 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/security/MongoUserServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import de.neuefische.capstone.backend.services.IdService;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.ArrayList;
7 | import java.util.Optional;
8 |
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 | import static org.mockito.Mockito.*;
11 |
12 | class MongoUserServiceTest {
13 | private final MongoUserRepository mongoUserRepository = mock(MongoUserRepository.class);
14 | private final IdService idService = mock(IdService.class);
15 | private final MongoUserService mongoUserService = new MongoUserService(mongoUserRepository, idService);
16 |
17 | @Test
18 | void verifyRepoCalls_WhenUserIsRegistered() {
19 | //Given
20 | MongoUserCreation mongoUserWithNoId = new MongoUserCreation("test", "test");
21 | when(mongoUserRepository.findByUsername("test"))
22 | .thenReturn(Optional.empty());
23 | when(idService.createRandomId())
24 | .thenReturn("1");
25 |
26 | //When
27 | mongoUserService.registerUser(mongoUserWithNoId);
28 |
29 | //Then
30 | verify(mongoUserRepository).findByUsername("test");
31 | verify(idService).createRandomId();
32 | }
33 |
34 | @Test
35 | void throwException_WhenNoUseralreadyExists() {
36 | //Given
37 | when(mongoUserRepository.findByUsername("test"))
38 | .thenReturn(Optional.of(new MongoUser("1", "test", "test", new ArrayList<>(), new ArrayList<>())));
39 |
40 | //When / Then
41 | try {
42 | mongoUserService.registerUser(new MongoUserCreation("test", "test"));
43 | } catch (Exception e) {
44 | assertEquals("User already exists", e.getMessage());
45 | }
46 | }
47 |
48 | @Test
49 | void returnObjectOfUser_WhenUsernameExists() {
50 | // Given
51 | MongoUser mongoUser = new MongoUser("1", "test", "test", new ArrayList<>(), new ArrayList<>());
52 | when(mongoUserRepository.findByUsername("test"))
53 | .thenReturn(Optional.of(mongoUser));
54 | MongoUserWithoutPassword expected = new MongoUserWithoutPassword("1", "test", new ArrayList<>(), new ArrayList<>());
55 |
56 | // When
57 | MongoUserWithoutPassword actual = mongoUserService.findByUsername("test");
58 |
59 | // Then
60 | verify(mongoUserRepository, times(2)).findByUsername("test");
61 | assertEquals(expected, actual);
62 | }
63 |
64 | @Test
65 | void returnObjectOfAnonymousUser_WhenUsernameExists() {
66 | // Given
67 | when(mongoUserRepository.findByUsername("test"))
68 | .thenReturn(Optional.empty());
69 | MongoUserWithoutPassword expected = new MongoUserWithoutPassword("unknown", "anonymousUser", new ArrayList<>(), new ArrayList<>());
70 |
71 | // When
72 | MongoUserWithoutPassword actual = mongoUserService.findByUsername("test");
73 |
74 | // Then
75 | verify(mongoUserRepository).findByUsername("test");
76 | assertEquals(expected, actual);
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/frontend/src/components/NavigationBar.tsx:
--------------------------------------------------------------------------------
1 | import AddButton from "./AddButton.tsx";
2 | import EditButton from "./EditButton.tsx";
3 | import styled from "@emotion/styled";
4 | import {useEffect, useState} from "react";
5 | import {useLocation} from "react-router-dom";
6 | import HomeButton from "./HomeButton.tsx";
7 | import DeleteButton from "./DeleteButton.tsx";
8 | import {useFetch} from "../hooks/useFetch.tsx";
9 | import DonationButton from "./DonationButton.tsx";
10 | import ParticipationButton from "./ParticipationButton.tsx";
11 | import {Project} from "../models/models.tsx";
12 | import UserButton from "./UserButton.tsx";
13 | import FilterButton from "./FilterButton.tsx";
14 | import SearchButton from "./SearchButton.tsx";
15 |
16 | export default function NavigationBar() {
17 | const location = useLocation();
18 | const id = location.pathname.split("/")[2]
19 | const [page, setPage] = useState("");
20 | const checkPage = useFetch(state => state.checkPage);
21 | const getProjectById = useFetch(state => state.getProjectById);
22 | const [project, setProject] = useState(undefined);
23 | const [participationVisible, setParticipationVisible] = useState(false);
24 |
25 | useEffect(() => {
26 | setPage(checkPage(location.pathname))
27 | }, [location, checkPage]);
28 |
29 | useEffect(() => {
30 | if (id) {
31 | getProjectById(id)
32 | .then((project) => {
33 | setProject(project);
34 | })
35 | .catch(error => {
36 | console.error(error);
37 | });
38 | }
39 | }, [id, getProjectById, location]);
40 |
41 | useEffect(() => {
42 | if (project && project.category === "PARTICIPATION") {
43 | setParticipationVisible(true);
44 | } else {
45 | setParticipationVisible(false);
46 | }
47 | }, [project, project?.category]);
48 |
49 | return (
50 |
51 |
52 |
53 | {page === "/" && }
54 | {page === "details" && }
55 | {page === "edit" && }
56 | {page === "details" && }
57 | {page === "details" && participationVisible && }
58 | {page === "/" && }
59 | {(page === "/" || page === "filter" || page === "filter-all" || page === "filter-donation" || page === "filter-participation") &&
60 | }
61 |
62 |
63 |
64 | )
65 | }
66 |
67 | const StyledNavigationBar = styled.div`
68 | display: inline-flex;
69 | border-radius: 5px 5px 0 0;
70 | height: 45px;
71 | `;
72 |
73 | const StyledNavigationWrapper = styled.div`
74 | z-index: 1;
75 | border-radius: 10px 10px 0 0;
76 | background: var(--blue, #163E56);
77 | box-shadow: 0 -4px 4px 0px rgba(0, 0, 0, 0.25);
78 | width: 100%;
79 | bottom: 0;
80 | left: 0;
81 | position: fixed;
82 | height: 60px;
83 | display: flex;
84 | justify-content: center;
85 | align-items: center;
86 | `;
87 |
--------------------------------------------------------------------------------
/frontend/src/pages/RegisterPage.tsx:
--------------------------------------------------------------------------------
1 | import {ChangeEvent, FormEvent, useState} from "react";
2 | import {useNavigate} from "react-router-dom";
3 | import {useFetch} from "../hooks/useFetch.tsx";
4 | import ArrowBackIcon from '@mui/icons-material/ArrowBack';
5 | import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
6 | import {StyledBody, StyledButton, StyledForm, StyledSpan, StyledTextField} from "../GlobalStyles.tsx";
7 |
8 | export default function RegisterPage() {
9 |
10 | const [username, setUsername] = useState("");
11 | const [password, setPassword] = useState("");
12 | const [repeatedPassword, setRepeatedPassword] = useState("");
13 | const navigate = useNavigate();
14 | const register = useFetch((state) => state.register);
15 | const regex = /^(?=.*[a-zA-Z])(?=.*\d).{6,20}$/;
16 |
17 | function handleUsernameInput(event: ChangeEvent) {
18 | setUsername(event.currentTarget.value);
19 | }
20 |
21 | function handlePasswordInput(event: ChangeEvent) {
22 | setPassword(event.currentTarget.value);
23 | }
24 |
25 | function handleRepeatedPasswordInput(event: ChangeEvent) {
26 | setRepeatedPassword(event.currentTarget.value);
27 | }
28 |
29 | function handleRegistration(event: FormEvent) {
30 | event.preventDefault();
31 | register(username, password, repeatedPassword, setPassword, setRepeatedPassword, navigate);
32 | }
33 |
34 | return (
35 |
36 |
37 |
41 | At least 4 characters
42 |
48 |
49 | At least 6 characters, must contain numbers and letters
50 |
51 |
57 | navigate("/login")} variant="outlined"
58 | endIcon={}>BACK
59 | }>REGISTER
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | name: "Deploy App"
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-frontend:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 |
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: '18'
17 |
18 | - name: Build Frontend
19 | working-directory: frontend
20 | run: |
21 | npm install
22 | npm run build
23 |
24 | - uses: actions/upload-artifact@v3
25 | with:
26 | name: frontend-build
27 | path: frontend/dist/
28 | build-backend:
29 | runs-on: ubuntu-latest
30 | needs: build-frontend
31 | steps:
32 | - uses: actions/checkout@v3
33 |
34 | - uses: actions/download-artifact@v3
35 | with:
36 | name: frontend-build
37 | path: backend/src/main/resources/static
38 |
39 | - name: Set up JDK
40 | uses: actions/setup-java@v3
41 | with:
42 | #Set Java Version
43 | java-version: '20'
44 | distribution: 'adopt'
45 | cache: 'maven'
46 |
47 | - name: Build with maven
48 | run: mvn -B package --file backend/pom.xml
49 |
50 | #Set backend/target/"MyApp".jar
51 | - uses: actions/upload-artifact@v3
52 | with:
53 | name: app.jar
54 | path: backend/target/capstone.jar
55 |
56 | push-to-docker-hub:
57 | runs-on: ubuntu-latest
58 | needs: build-backend
59 | steps:
60 | - uses: actions/checkout@v3
61 | - uses: actions/download-artifact@v3
62 | with:
63 | name: app.jar
64 | path: backend/target
65 |
66 | - name: Login to DockerHub
67 | uses: docker/login-action@v3
68 | with:
69 | #Set dockerhub username
70 | username: ben21
71 | password: ${{ secrets.DOCKERHUB_PASSWORD }}
72 |
73 | - name: Build and push
74 | uses: docker/build-push-action@v3
75 | with:
76 | push: true
77 | #Set dockerhub project (replace "bartfastiel/deploy-to-aws-with-github-actions")
78 | tags: ben21/capstone:latest
79 | context: .
80 |
81 | deploy:
82 | runs-on: ubuntu-latest
83 | needs: push-to-docker-hub
84 | steps:
85 | - name: Restart Docker Compose Services
86 | uses: appleboy/ssh-action@master
87 | with:
88 | #Set hostname of your AWS EC2 instance
89 | host: 164.90.173.123
90 | #Set App Name ("myapp" - "tango")
91 | username: ben
92 | password: ${{ secrets.SSH_PASSWORD }}
93 | #Set App Name ("myapp" - "tango")
94 | #Set dockerhub project (replace "bartfastiel/deploy-to-aws-with-github-actions")
95 | script: |
96 | cd /home/ben/services/
97 | docker compose pull app-be-human
98 | docker stop be-human
99 | docker rm be-human
100 | docker compose up -d --no-deps app-be-human
101 | - name: Check the deployed service URL
102 | uses: jtalk/url-health-check-action@v3
103 | with:
104 | #Set URL of your AWS EC2 instance
105 | url: http://be-human.schaefer-inet.de
106 | max-attempts: 3
107 | retry-delay: 5s
108 | retry-all: true
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/imageupload/ImageUploadIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.imageupload;
2 |
3 | import com.cloudinary.Cloudinary;
4 | import com.cloudinary.Uploader;
5 | import org.junit.jupiter.api.Test;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8 | import org.springframework.boot.test.context.SpringBootTest;
9 | import org.springframework.boot.test.mock.mockito.MockBean;
10 | import org.springframework.http.MediaType;
11 | import org.springframework.mock.web.MockMultipartFile;
12 | import org.springframework.security.test.context.support.WithMockUser;
13 | import org.springframework.test.annotation.DirtiesContext;
14 | import org.springframework.test.web.servlet.MockMvc;
15 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
16 |
17 | import java.io.File;
18 | import java.util.Map;
19 |
20 | import static org.mockito.ArgumentMatchers.any;
21 | import static org.mockito.Mockito.mock;
22 | import static org.mockito.Mockito.when;
23 |
24 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart;
26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
27 |
28 | @SpringBootTest
29 | @AutoConfigureMockMvc
30 | class ImageUploadIntegrationTest {
31 | @Autowired
32 | MockMvc mockMvc;
33 |
34 | @MockBean
35 | Cloudinary cloudinary;
36 |
37 | Uploader uploader = mock(Uploader.class);
38 |
39 | @WithMockUser
40 | @Test
41 | void getAllImageProfilesAndExpectEmptyList() throws Exception {
42 | mockMvc.perform(MockMvcRequestBuilders.get("/api/upload"))
43 | .andExpect(status().isOk())
44 | .andExpect(content().json("""
45 | []
46 | """));
47 | }
48 |
49 | @WithMockUser
50 | @Test
51 | @DirtiesContext
52 | void expectSuccessfulPost() throws Exception {
53 | MockMultipartFile data = new MockMultipartFile("data",
54 | null,
55 | MediaType.APPLICATION_JSON_VALUE,
56 | """
57 | {"name":"docker-image"}
58 | """
59 | .getBytes()
60 | );
61 |
62 | MockMultipartFile file = new MockMultipartFile("file",
63 | "testImage.png",
64 | MediaType.IMAGE_PNG_VALUE,
65 | "testImage".getBytes()
66 | );
67 |
68 | File fileToUpload = File.createTempFile("image", null);
69 | file.transferTo(fileToUpload);
70 |
71 |
72 | when(cloudinary.uploader()).thenReturn(uploader);
73 | when(uploader.upload(any(), any())).thenReturn(Map.of("url", "test-url"));
74 |
75 | mockMvc.perform(multipart("/api/upload")
76 | .file(data)
77 | .file(file)
78 | .with(csrf()))
79 |
80 |
81 | .andExpect(status().isCreated())
82 | .andExpect(content().json("""
83 | {"name": "docker-image",
84 | "url": "test-url"}
85 | """))
86 | .andExpect(jsonPath("$.id").isNotEmpty());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/frontend/src/pages/SearchGallary.tsx:
--------------------------------------------------------------------------------
1 | import {useFetch} from "../hooks/useFetch.tsx";
2 | import styled from "@emotion/styled";
3 | import {Project} from "../models/models.tsx";
4 | import React, {useEffect, useState} from "react";
5 | import {Autocomplete, Stack} from "@mui/material";
6 | import ProjectCard from "../components/ProjectCard.tsx";
7 | import {StyledBody, StyledGallery, StyledSearchBar, StyledTextField} from "../GlobalStyles.tsx";
8 |
9 | export default function SearchGallery() {
10 |
11 | const projects = useFetch((state) => state.projects);
12 | const fetchProjects = useFetch((state) => state.fetchProjects);
13 | const [searchTerm, setSearchTerm] = useState("");
14 |
15 | useEffect(() => {
16 | fetchProjects();
17 | }, [fetchProjects]);
18 |
19 | function searchProjects(projects: Project[], searchTerm: string) {
20 | if (!searchTerm) {
21 | return projects; // Return the original list if no search term provided
22 | }
23 |
24 | const lowerCaseSearchTerm = searchTerm.toLowerCase();
25 |
26 | return projects.filter(project => {
27 | const projectValues = Object.values(project).map(value =>
28 | typeof value === 'string' ? value.toLowerCase() : ''
29 | );
30 |
31 | return projectValues.some(value => value.includes(lowerCaseSearchTerm));
32 | });
33 | }
34 |
35 | function handleChange(event: React.ChangeEvent) {
36 | setSearchTerm(event.target.value);
37 | }
38 |
39 | function handleAutoCompleteChange(
40 | _: React.SyntheticEvent,
41 | value: string | null
42 | ) {
43 | if (value !== null) {
44 | setSearchTerm(value);
45 | } else {
46 | setSearchTerm("");
47 | }
48 | }
49 |
50 | const filteredProjects = searchProjects(projects, searchTerm);
51 |
52 | return (
53 |
54 |
55 |
56 |
57 | option.name)}
64 | renderInput={(params) => }/>
66 |
67 |
68 |
69 |
70 |
71 | {filteredProjects.map((project) => (
72 |
73 | ))}
74 |
75 |
76 |
77 | )
78 | }
79 |
80 | const StyledSearchResultsDiv = styled.div`
81 | margin-top: 80px;
82 | `;
83 |
84 | const StyledFixedSearchBarDiv = styled.div`
85 | position: fixed;
86 | z-index: 1;
87 | top: 50px;
88 | width: 100%;
89 | height: 125px;
90 | background-color: #FF644A;
91 | `;
92 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/security/MongoUserIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend.security;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.security.test.context.support.WithMockUser;
9 | import org.springframework.test.web.servlet.MockMvc;
10 | import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
11 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
12 |
13 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
14 |
15 | @SpringBootTest
16 | @AutoConfigureMockMvc
17 | class MongoUserIntegrationTest {
18 |
19 | @Autowired
20 | private MockMvc mockMvc;
21 |
22 | @Test
23 | void getAnonymousUser_whenGetUserName() throws Exception {
24 | // GIVEN that user is not logged in
25 | // WHEN
26 | mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me"))
27 | // THEN
28 | .andExpect(MockMvcResultMatchers.status().isOk())
29 | .andExpect(MockMvcResultMatchers.content().string("anonymousUser"));
30 | }
31 |
32 | @Test
33 | void getUserObject_whenGetUserName() throws Exception {
34 | // GIVEN that user is not logged in
35 | // WHEN
36 | mockMvc.perform(MockMvcRequestBuilders.get("/api/users/me-object"))
37 | // THEN
38 | .andExpect(MockMvcResultMatchers.status().isOk())
39 | .andExpect(MockMvcResultMatchers.jsonPath("username").value("anonymousUser"))
40 | .andExpect(MockMvcResultMatchers.jsonPath("id").value("unknown"))
41 | .andExpect(MockMvcResultMatchers.jsonPath("donations").isEmpty())
42 | .andExpect(MockMvcResultMatchers.jsonPath("participations").isEmpty());
43 | }
44 |
45 | @Test
46 | @WithMockUser(username = "testUser", password = "testPassword")
47 | void getUsername_whenLoggedInGetUserName() throws Exception {
48 | // Given that user is logged in
49 | // When
50 | mockMvc.perform(MockMvcRequestBuilders.post("/api/users/login")
51 | .with(csrf()))
52 | //Then
53 | .andExpect(MockMvcResultMatchers.status().isOk())
54 | .andExpect(MockMvcResultMatchers.content().string("testUser"));
55 | }
56 |
57 | @Test
58 | @WithMockUser(username = "testUser", password = "testPassword")
59 | void expectStatusOk_whenLogoutUser() throws Exception {
60 | //Given
61 | mockMvc.perform(MockMvcRequestBuilders.post("/api/users/login")
62 | .with(csrf()));
63 |
64 | //When
65 | mockMvc.perform(MockMvcRequestBuilders.post("/api/users/logout")
66 | .with(csrf()))
67 |
68 | //Then
69 | .andExpect(MockMvcResultMatchers.status().isOk());
70 | }
71 |
72 | @Test
73 | void expectRegistration_whenRegisterUser() throws Exception {
74 | //GIVEN
75 | String testUserWithoutId = """
76 | {
77 | "username": "themeTest",
78 | "password": "secretPass3"
79 | }
80 | """;
81 |
82 | //WHEN
83 | mockMvc.perform(MockMvcRequestBuilders.post("/api/users/register")
84 | .contentType(MediaType.APPLICATION_JSON)
85 | .content(testUserWithoutId)
86 | .with(csrf()))
87 | //THEN
88 | .andExpect(MockMvcResultMatchers.status().isOk())
89 | .andExpect(MockMvcResultMatchers.content().string("registered"));
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/frontend/src/components/FilterUserData.tsx:
--------------------------------------------------------------------------------
1 | import {User} from "../models/models.tsx";
2 | import styled from "@emotion/styled";
3 | import {useNavigate} from "react-router-dom";
4 | import {useFetch} from "../hooks/useFetch.tsx";
5 | import {useEffect} from "react";
6 |
7 | type Props = {
8 | user: User,
9 | filterArgument: string,
10 | }
11 | export default function FilterUserData(props: Props) {
12 |
13 | const navigate = useNavigate();
14 | const projects = useFetch(state => state.projects);
15 | const fetchProjects = useFetch(state => state.fetchProjects);
16 |
17 | useEffect(() => {
18 | fetchProjects();
19 | }, [fetchProjects]);
20 |
21 | const formatAmountToCurrency = (amount: string) => {
22 | return parseFloat(amount).toLocaleString("de-DE", {
23 | style: "currency",
24 | currency: "EUR",
25 | minimumFractionDigits: 2,
26 | maximumFractionDigits: 2,
27 | })
28 | }
29 |
30 | const totalDonations = (props.user.donations.reduce((sum, donation) => sum + parseFloat(donation.amount), 0)).toString();
31 |
32 | function filter(filter: string) {
33 | if (filter === "My Donations") {
34 | return (
35 | <>
36 | {props.user.donations.map((donation) =>
37 | navigate(`/details/${donation.projectId}`)}
38 | key={donation.id}>
39 | {donation.projectName}
40 | {formatAmountToCurrency(donation.amount)}
41 | )}
42 |
43 | Sum: {formatAmountToCurrency(totalDonations)}
44 |
45 | >
46 | )
47 | } else if (filter === "My Participations") {
48 | return (
49 | <>
50 | {props.user.participations.map((participation) =>
51 | navigate(`/details/${participation.projectId}`)}
52 | key={participation.id}>{participation.projectName})}
53 |
54 | Sum: {props.user.participations.length}
55 |
56 | >
57 | )
58 | } else if (filter === "My Projects") {
59 | const userProjects = projects.filter(project => project.userId === props.user.id);
60 |
61 | return (
62 | <>
63 | {userProjects.map((project) =>
64 | navigate(`/details/${project.id}`)}
65 | key={project.id}>{project.name})}
66 |
67 | Sum: {userProjects.length}
68 |
69 | >
70 | )
71 | }
72 | }
73 |
74 |
75 | return (
76 | <>
77 | {filter(props.filterArgument)}
78 | >
79 | )
80 | }
81 |
82 | const StyledListDiv = styled.div`
83 | display: flex;
84 | justify-content: space-between;
85 | align-items: center;
86 | border: none;
87 | background-color: #FFB34F;
88 | border-radius: 4px;
89 | padding: 5px;
90 | margin: 0;
91 | cursor: pointer;
92 | `;
93 |
94 | const StyledP = styled.p`
95 | margin: 0;
96 | font-family: "Roboto", sans-serif;
97 | font-weight: 400;
98 | `;
99 |
100 | const StyledSumP = styled.p`
101 | margin: 0;
102 | font-family: "Roboto", sans-serif;
103 | font-weight: 600;
104 | `;
105 |
106 | const StyledTotalPWrapper = styled.div`
107 | display: flex;
108 | justify-content: right;
109 | `;
110 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/ProjectCalculationsTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import de.neuefische.capstone.backend.models.*;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.math.BigDecimal;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | import static org.junit.jupiter.api.Assertions.assertEquals;
11 |
12 | class ProjectCalculationsTest {
13 |
14 | ProjectCalculations projectCalculations = new ProjectCalculations();
15 |
16 | @Test
17 | void returnProjectWithProgress_whenCalculateProgressForParticipation() {
18 | //Given
19 | Participation newParticipation = new Participation(
20 | "01A",
21 | "01B",
22 | "ProjectName",
23 | "ParticipationName",
24 | "userId123"
25 | );
26 |
27 | Project project = new Project(
28 | "01A",
29 | "test",
30 | "test",
31 | Category.PARTICIPATION,
32 | List.of(Demand.DONATIONINKIND),
33 | 0,
34 | 100,
35 | "test",
36 | new ArrayList<>(),
37 | List.of(newParticipation),
38 | "userId123",
39 | new Image("", "", "")
40 | );
41 |
42 | Project expectedProject = new Project(
43 | "01A",
44 | "test",
45 | "test",
46 | Category.PARTICIPATION,
47 | List.of(Demand.DONATIONINKIND),
48 | 1,
49 | 100,
50 | "test",
51 | new ArrayList<>(),
52 | List.of(newParticipation),
53 | "userId123",
54 | new Image("", "", "")
55 | );
56 |
57 | //When
58 | Project actualProject = projectCalculations.calculateProgressForParticipations(project);
59 |
60 | //Then
61 | assertEquals(expectedProject, actualProject);
62 | }
63 |
64 | @Test
65 | void returnProject_whenCategoryWrong() {
66 | //Given
67 | Participation newParticipation = new Participation(
68 | "01A",
69 | "01B",
70 | "ProjectName",
71 | "ParticipationName",
72 | "userId123"
73 | );
74 |
75 | Project project = new Project(
76 | "01A",
77 | "test",
78 | "test",
79 | Category.DONATION,
80 | List.of(Demand.DONATIONINKIND),
81 | 0,
82 | 100,
83 | "test",
84 | new ArrayList<>(),
85 | List.of(newParticipation),
86 | "userId123",
87 | new Image("", "", "")
88 | );
89 |
90 | //When
91 | Project actualProject = projectCalculations.calculateProgressForParticipations(project);
92 |
93 | //Then
94 | assertEquals(project, actualProject);
95 | }
96 |
97 | @Test
98 | void returnProjectWithProgress_whenCalculateProgressForDonation() {
99 | //Given
100 | Donation newDonation = new Donation(
101 | "01A",
102 | "01B",
103 | "ProjectName",
104 | "DonationName",
105 | new BigDecimal(50),
106 | "userId123"
107 | );
108 |
109 | Project project = new Project(
110 | "01A",
111 | "test",
112 | "test",
113 | Category.DONATION,
114 | List.of(Demand.DONATIONINKIND),
115 | 0,
116 | 100,
117 | "test",
118 | List.of(newDonation),
119 | new ArrayList<>(),
120 | "userId123",
121 | new Image("", "", "")
122 | );
123 |
124 | Project expectedProject = new Project(
125 | "01A",
126 | "test",
127 | "test",
128 | Category.DONATION,
129 | List.of(Demand.DONATIONINKIND),
130 | 50,
131 | 100,
132 | "test",
133 | List.of(newDonation),
134 | new ArrayList<>(),
135 | "userId123",
136 | new Image("", "", "")
137 | );
138 |
139 | //When
140 | Project actualProject = projectCalculations.calculateProgressForDonations(project);
141 |
142 | //Then
143 | assertEquals(expectedProject, actualProject);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/backend/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 3.1.2
9 |
10 |
11 | com.example
12 | backend
13 | 0.0.1-SNAPSHOT
14 | backend
15 | backend
16 |
17 | 20
18 | ben-21
19 | https://sonarcloud.io
20 | ben-21_capstone-neuefische-backend
21 |
22 |
23 |
24 | org.springframework.boot
25 | spring-boot-starter-data-mongodb
26 |
27 |
28 | org.springframework.boot
29 | spring-boot-starter-web
30 |
31 |
32 | org.projectlombok
33 | lombok
34 | true
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-test
39 | test
40 |
41 |
42 | de.flapdoodle.embed
43 | de.flapdoodle.embed.mongo.spring30x
44 | test
45 | 4.6.2
46 |
47 |
48 | org.springframework.boot
49 | spring-boot-starter-security
50 |
51 |
52 | org.bouncycastle
53 | bcprov-jdk15on
54 | 1.70
55 |
56 |
57 | org.springframework.security
58 | spring-security-test
59 | test
60 |
61 |
62 | org.springframework.boot
63 | spring-boot-starter-validation
64 |
65 |
66 | jakarta.validation
67 | jakarta.validation-api
68 | 3.0.2
69 |
70 |
71 | com.cloudinary
72 | cloudinary-http44
73 | 1.34.0
74 |
75 |
76 |
77 | capstone
78 |
79 |
80 | org.springframework.boot
81 | spring-boot-maven-plugin
82 |
83 |
84 |
85 | org.projectlombok
86 | lombok
87 |
88 |
89 |
90 |
91 |
92 | org.jacoco
93 | jacoco-maven-plugin
94 | 0.8.10
95 |
96 |
97 | jacoco-initialize
98 |
99 | prepare-agent
100 |
101 |
102 |
103 | jacoco-site
104 | package
105 |
106 | report
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/frontend/src/pages/AddDonationOrParticipation.tsx:
--------------------------------------------------------------------------------
1 | import {useFetch} from "../hooks/useFetch.tsx";
2 | import React, {useEffect, useState} from "react";
3 | import styled from "@emotion/styled";
4 | import {useLocation, useNavigate, useParams} from "react-router-dom";
5 | import CancelIcon from '@mui/icons-material/Cancel';
6 | import {toast} from "react-toastify";
7 | import {DonationCreation, Project, ParticipationCreation} from "../models/models.tsx";
8 | import ProjectCard from "../components/ProjectCard.tsx";
9 | import AttachMoneyIcon from "@mui/icons-material/AttachMoney";
10 | import VolunteerActivismIcon from "@mui/icons-material/VolunteerActivism";
11 | import {StyledBody, StyledButton, StyledTextField} from "../GlobalStyles.tsx";
12 |
13 | export default function AddDonationOrParticipation() {
14 |
15 | const getProjectById = useFetch(state => state.getProjectById);
16 | const [project, setProject] = useState(undefined);
17 | const {id} = useParams();
18 | const [amount, setAmount] = useState("");
19 | const navigate = useNavigate();
20 | const location = useLocation();
21 | const checkPage = useFetch(state => state.checkPage);
22 | const [page, setPAge] = useState("");
23 | const postDonation = useFetch(state => state.postDonation);
24 | const postParticipation = useFetch(state => state.postParticipation);
25 |
26 | useEffect(() => {
27 | if (id) {
28 | getProjectById(id)
29 | .then((project) => {
30 | setProject(project);
31 | })
32 | .catch(error => {
33 | console.error(error);
34 | });
35 | } else {
36 | toast.error("Something went wrong");
37 | navigate("/");
38 | }
39 | }, [id, navigate, getProjectById]);
40 |
41 | useEffect(() => {
42 | setPAge(checkPage(location.pathname));
43 | }, [location, checkPage]);
44 |
45 | function handleChange(event: React.ChangeEvent) {
46 | setAmount(event.target.value);
47 | }
48 |
49 | function handleSubmit(event: React.FormEvent) {
50 | event.preventDefault();
51 | if (project && page === "donate") {
52 | const donation: DonationCreation = {
53 | projectId: project.id,
54 | projectName: project.name,
55 | amount: amount
56 | }
57 | postDonation(project.id, donation)
58 | .then(() => {
59 | navigate(`/details/${project.id}`);
60 | });
61 | }
62 |
63 | if (project && page === "participate") {
64 | const participation: ParticipationCreation = {
65 | projectId: project.id,
66 | projectName: project.name,
67 | }
68 | postParticipation(project.id, participation)
69 | .then(() => {
70 | navigate(`/details/${project.id}`);
71 | });
72 | }
73 | }
74 |
75 | function handleCancelButton() {
76 | if (project) {
77 | navigate(`/details/${project.id}`)
78 | window.scrollTo(0, 0);
79 | } else {
80 | toast.error("Something went wrong");
81 | navigate("/");
82 | }
83 | }
84 |
85 | return (
86 |
87 | {project && }
88 |
89 |
90 | {page === "donate" &&
91 | }
95 | {page === "donate" &&
96 | }>DONATE}
97 | {page === "participate" &&
98 | }>PARTICIPATE}
100 | }>CANCEL
102 |
103 |
104 | )
105 | }
106 |
107 | const StyledDonationParticipationForm = styled.form`
108 | display: flex;
109 | flex-direction: column;
110 | align-items: center;
111 | justify-content: center;
112 | gap: 1.1em;
113 | background-color: #EBE7D8;
114 | border-radius: 4px;
115 | padding: 20px 10px 10px 10px;
116 | margin-top: -30px;
117 | `;
118 |
--------------------------------------------------------------------------------
/frontend/src/components/ProjectCard.tsx:
--------------------------------------------------------------------------------
1 | import Card from '@mui/material/Card';
2 | import CardMedia from '@mui/material/CardMedia';
3 | import {CardActionArea} from '@mui/material';
4 | import styled from "@emotion/styled";
5 | import {Project} from "../models/models.tsx";
6 | import {useLocation, useNavigate} from "react-router-dom";
7 | import {useEffect, useState} from "react";
8 | import {useFetch} from "../hooks/useFetch.tsx";
9 | import ProgressBar from "./ProgressBar.tsx";
10 | import ProgressBarGalleryView from "./ProgressBarGalleryView.tsx";
11 | import TopDonators from "./TopDonators.tsx";
12 |
13 |
14 | type Props = {
15 | project: Project;
16 | }
17 |
18 | export default function ProjectCard(props: Props) {
19 | const navigate = useNavigate();
20 | const [demandsUserFriendly, setDemandsUserFriendly] = useState([]);
21 | const checkPage = useFetch(state => state.checkPage);
22 | const location = useLocation();
23 | const [page, setPage] = useState("");
24 | const mapDemandsToUserFriendly = useFetch(state => state.mapDemandsToUserFriendly);
25 |
26 | useEffect(() => {
27 | setDemandsUserFriendly(mapDemandsToUserFriendly(props.project.demands));
28 | }, [props.project, mapDemandsToUserFriendly]);
29 |
30 | useEffect(() => {
31 | setPage(checkPage(location.pathname));
32 | }, [location, checkPage]);
33 |
34 | return (
35 | navigate(`/details/${props.project.id}`)}>
36 |
37 | {props.project.image.url &&
38 | }
44 |
45 | {props.project.name}
46 |
47 | {page === "details" &&
48 | <>
49 |
50 | {props.project.description}
51 |
52 |
53 |
54 | {props.project.donations.map(donation => parseFloat(donation.amount)).reduce((a, b) => a + b, 0)} EUR
55 |
56 | {props.project.category === "DONATION" &&
57 |
58 | of {props.project.goal} EUR collected
59 |
60 | }
61 | {props.project.category === "DONATION" && }
62 |
63 | {props.project.category === "PARTICIPATION" &&
64 | <>
65 |
66 | {props.project.participations.length} Participations
67 |
68 |
69 | of {props.project.goal} needed
70 |
71 | {props.project.category === "PARTICIPATION" && }
72 | >}
73 |
74 | Demands:
75 |
76 |
77 | {demandsUserFriendly.map((demand) => {demand})}
79 |
80 | >}
81 | {page === "details" && }
82 |
83 | Location:
84 |
85 |
86 | {props.project.location}
87 |
88 | {(page === "/" || page === "filter") && }
89 |
90 |
91 | );
92 | }
93 |
94 | const StyledCard = styled(Card)`
95 | width: 100%;
96 | background-color: #EBE7D8;
97 | margin: 0;
98 | padding: 0;
99 | border-radius: 4px;
100 | `;
101 |
102 | const StyledHeadLine = styled.h1`
103 | padding-left: 10px;
104 | margin-top: 10px;
105 | margin-bottom: 6px;
106 | `;
107 |
108 | const StyledSubHeadLine = styled.h2`
109 | padding-top: 20px;
110 | padding-left: 10px;
111 | margin: 0;
112 | font-family: "Robot", sans-serif;
113 | font-weight: 400;
114 | `;
115 |
116 | const StyledDonationParticipation = styled.h2`
117 | padding-top: 10px;
118 | margin: 0;
119 | display: flex;
120 | justify-content: center;
121 | font-family: "Robot", sans-serif;
122 | font-weight: 600;
123 | `;
124 |
125 | const StyledSubDonationParticipation = styled.div`
126 | padding: 0;
127 | margin: 0;
128 | display: flex;
129 | justify-content: center;
130 | font-family: "Robot", sans-serif;
131 | font-weight: 400;
132 | `;
133 |
134 | const StyledDescription = styled.div`
135 | padding: 0 10px 10px 10px;
136 | margin: 0;
137 | font-family: "Robot", sans-serif;
138 | font-weight: 300;
139 | font-size: 1em;
140 | `;
141 |
142 | const StyledDemandsWrapper = styled.div`
143 | display: flex;
144 | justify-content: space-between;
145 | `;
146 |
147 | const StyledDemands = styled.div`
148 | flex: 1;
149 | display: flex;
150 | align-items: center;
151 | justify-content: center;
152 | text-align: center;
153 | border: none;
154 | width: 96px;
155 | height: 44px;
156 | border-radius: 4px;
157 | background: var(--orange, #FFB34F);
158 | font-family: "Robot", sans-serif;
159 | font-weight: 500;
160 | font-size: 1em;
161 | margin: 10px;
162 | `;
163 |
--------------------------------------------------------------------------------
/backend/src/main/java/de/neuefische/capstone/backend/ProjectService.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import de.neuefische.capstone.backend.models.*;
4 | import de.neuefische.capstone.backend.security.MongoUserService;
5 | import de.neuefische.capstone.backend.security.MongoUserWithoutPassword;
6 | import de.neuefische.capstone.backend.services.IdService;
7 | import org.springframework.security.core.context.SecurityContextHolder;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.web.server.MethodNotAllowedException;
10 |
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.NoSuchElementException;
14 |
15 | @Service
16 | public class ProjectService {
17 |
18 | private final ProjectRepo projectRepo;
19 | private final IdService idService;
20 | private final ProjectCalculations projectCalculations;
21 | private final MongoUserService mongoUserService;
22 |
23 | public ProjectService(ProjectRepo projectRepo, IdService idService, ProjectCalculations projectCalculations, MongoUserService mongoUserService) {
24 | this.projectRepo = projectRepo;
25 | this.idService = idService;
26 | this.projectCalculations = projectCalculations;
27 | this.mongoUserService = mongoUserService;
28 | }
29 |
30 | private static final String NOT_FOUND_MESSAGE1 = "No project with ";
31 | private static final String NOT_FOUND_MESSAGE2 = " found";
32 |
33 | public List getAllProjects() {
34 | return projectRepo.findAll();
35 | }
36 |
37 | public Project addProject(ProjectCreation projectCreation) {
38 |
39 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
40 | MongoUserWithoutPassword user = mongoUserService.findByUsername(username);
41 |
42 | Project newProject = new Project(
43 | idService.createRandomId(),
44 | projectCreation.name(),
45 | projectCreation.description(),
46 | projectCreation.category(),
47 | projectCreation.demands(),
48 | 0,
49 | projectCreation.goal(),
50 | projectCreation.location(),
51 | new ArrayList<>(),
52 | new ArrayList<>(),
53 | user.id(),
54 | projectCreation.image()
55 | );
56 | return projectRepo.insert(newProject);
57 | }
58 |
59 | public Project getProjectById(String projectId) {
60 | return projectRepo.findById(projectId).orElseThrow(() -> new NoSuchElementException(NOT_FOUND_MESSAGE1 + projectId + NOT_FOUND_MESSAGE2));
61 | }
62 |
63 | public Project updateProject(String id, ProjectNoId projectNoId) {
64 | if (!projectRepo.existsById(id)) throw new NoSuchElementException(NOT_FOUND_MESSAGE1 + id + NOT_FOUND_MESSAGE2);
65 |
66 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
67 | MongoUserWithoutPassword user = mongoUserService.findByUsername(username);
68 |
69 | if (!user.id().equals(projectNoId.userId()))
70 | throw new MethodNotAllowedException("You are not allowed to edit this project", null);
71 |
72 | Project updatedProject = new Project(
73 | id,
74 | projectNoId.name(),
75 | projectNoId.description(),
76 | projectNoId.category(),
77 | projectNoId.demands(),
78 | projectNoId.progress(),
79 | projectNoId.goal(),
80 | projectNoId.location(),
81 | projectNoId.donations(),
82 | projectNoId.participations(),
83 | projectNoId.userId(),
84 | projectNoId.image());
85 |
86 | return projectRepo.save(updatedProject);
87 | }
88 |
89 | public void deleteProject(String projectId) {
90 | if (!projectRepo.existsById(projectId)) throw new NoSuchElementException(NOT_FOUND_MESSAGE1 + projectId + NOT_FOUND_MESSAGE2);
91 | projectRepo.deleteById(projectId);
92 | }
93 |
94 | public Project addDonation(String projectId, DonationCreation donationCreation) {
95 |
96 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
97 | MongoUserWithoutPassword user = mongoUserService.findByUsername(username);
98 |
99 | Donation newDonation = new Donation(
100 | idService.createRandomId(),
101 | donationCreation.projectId(),
102 | donationCreation.projectName(),
103 | user.username(),
104 | donationCreation.amount(),
105 | user.id()
106 | );
107 |
108 | user.donations().add(newDonation);
109 | mongoUserService.updateUser(user);
110 |
111 | Project project = projectRepo.findById(projectId).orElseThrow(() -> new NoSuchElementException(NOT_FOUND_MESSAGE1 + projectId + NOT_FOUND_MESSAGE2));
112 | project.donations().add(newDonation);
113 |
114 | return projectRepo.save(projectCalculations.calculateProgressForDonations(project));
115 | }
116 |
117 | public Project addParticipation(String projectId, ParticipationCreation participationCreation) {
118 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
119 | MongoUserWithoutPassword user = mongoUserService.findByUsername(username);
120 |
121 | Participation newParticipation = new Participation(
122 | idService.createRandomId(),
123 | participationCreation.projectId(),
124 | participationCreation.projectName(),
125 | user.username(),
126 | user.id()
127 | );
128 |
129 | user.participations().add(newParticipation);
130 | mongoUserService.updateUser(user);
131 |
132 | Project project = projectRepo.findById(projectId).orElseThrow(() -> new NoSuchElementException(NOT_FOUND_MESSAGE1 + projectId + NOT_FOUND_MESSAGE2));
133 | project.participations().add(newParticipation);
134 |
135 | return projectRepo.save(projectCalculations.calculateProgressForParticipations(project));
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Be Human - Capstone Project
2 |
3 |
4 |
5 |
6 | > "Be Human" is my Fullstack capstone project, developed during the neuefische Java Full Stack Bootcamp, showcasing skills in Java Backend Development and React / TypeScript Development by implementing crud operations.
7 |
8 |
9 | ## SonarCloud
10 | ### backend
11 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-backend)
12 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-backend)
13 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-backend)
14 |
15 | ### frontend
16 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-frontend)
17 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-frontend)
18 | [](https://sonarcloud.io/summary/new_code?id=ben-21_capstone-neuefische-frontend)
19 |
20 |
21 | ## Table of Contents
22 |
23 | - [CI / CD](#ci--cd)
24 | - [Overview](#overview)
25 | - [Wireframes](#wireframes)
26 | - [Models / Database-Structure](#models-and-database-structure)
27 | - [Tech Stack](#tech-stack)
28 | - [Features](#features)
29 | - [Usage](#usage)
30 | - [License](#license)
31 |
32 | ## CI / CD
33 | The project runs on my own cloud server and is developed using continous integration and contionous deployment. For this I set up a NGINX-Webserver and a Docker Environment. My App, the NGINX-Webserver and Certbot (for SSL Certificates) run as a container. All is configured and started using one Docker-Compose file.
34 | CI and CD is triggered by merging a branch into the Main-Branch. For both I used Github Actions (build with Maven and SonarCloud for CI - own yml. Files for CD to my DigitalOcean Cloudserver).
35 |
36 | ## Overview
37 |
38 | The deployed project can be found at [Be Human](https://be-human.schaefer-inet.de)
39 | The app is **only for mobile view** designed - please use your phone or switch your browser, via inspect/ dev-tools, to mobile view.
40 |
41 | Title: Be Human - A Humanitarian Project Platform
42 |
43 | Summary:
44 | "Be Human" is an innovative application developed using a comprehensive tech stack that includes Java, Spring Boot, REST, Maven, JUnit, React, TypeScript, MongoDB, and Docker. The platform serves as a hub for fostering participation in humanitarian initiatives.
45 |
46 | Key Features - CRUD-Operations:
47 | The app allows users to engage in existing humanitarian projects and create new ones. Projects are categorized into two main types: Donation Projects and Aid Projects. Users can contribute financially to both types and actively participate as volunteers in Aid Projects. Each project can define specific goals, such as donation targets or the number of volunteers needed.
48 |
49 | Motivation through Progress:
50 | The application calculates and visually presents project progress, both numerically and as a percentage, based on registered donations or volunteers and the predefined goal. This progress tracking serves as motivation for users to actively contribute.
51 |
52 | Project Filtering and Search:
53 | Users can easily navigate through projects using the filter function, which categorizes projects. Additionally, a comprehensive search function enables users to explore projects based on various attributes.
54 |
55 | User Management and Security:
56 | The backend incorporates Spring Security for robust user management. While projects are publicly viewable, contributing or participating requires user registration and login. Registered users can create and modify projects, with each user having only access to their own projects exclusively regarding changing content of a project.
57 |
58 | Insightful Analytics:
59 | The platform offers insightful analytics for both projects and users. For projects, users can view total donation amounts and the number of volunteers involved. The platform also features a unique privacy-focused approach for recognizing top donors, displaying avatars composed of initials instead of full usernames.
60 |
61 | Personalized User Insights:
62 | Users gain access to personalized insights, including lists of their own projects and projects they've participated in, segmented into Donation Projects and Aid Projects. Users can view cumulative donation amounts and their total contributions to projects, promoting a sense of accomplishment.
63 |
64 | In Conclusion:
65 | "Be Human" is a compelling application that leverages an array of cutting-edge technologies to facilitate engagement in humanitarian projects. The platform's user-friendly interface, robust security features, motivational progress tracking, and insightful analytics converge to encourage active participation in noble causes. Everyone shall be reminded of the importance of performing acts of kindness.
66 |
67 | ## Wireframes
68 | 
69 |
70 | ## Models and Database Structure
71 | 
72 |
73 |
74 |
75 |
76 |
77 | ## Tech Stack
78 |
79 | - Java
80 | - Spring Boot
81 | - Maven
82 | - JUnit
83 | - TypeScript
84 | - React
85 | - MongoDB
86 | - REST
87 | - Docker
88 |
89 | ## Features
90 |
91 | - CRUD Operations (create, read, update, delete)
92 | - Objects stored in MongoDB
93 | - Project
94 | - User
95 | - Donation
96 | - Volunteer
97 | - Progress Calculation
98 | - Top Tier Calculation
99 | - Image Upload
100 |
101 | ## Usage
102 |
103 | 1. ATTENTION: App is only build for mobile use! Please turn on mobile view in your webbrowser.
104 | 2. Clone the repository: `git clone https://github.com/your-username/Be-Human.git`
105 | 3. Start backend and frontend inside your IDE (e.g. IntelliJ)
106 | 4. Define accesss to MongoDB via environment variable "MONGO_DB_URI" (e.g. mongodb://localhost:27017/capstoneDB)
107 |
108 | ## License
109 |
110 | Copyright [2023] [Ben-21]
111 |
112 | ---
113 |
114 |
--------------------------------------------------------------------------------
/backend/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Apache Maven Wrapper startup batch script, version 3.2.0
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
30 | @REM e.g. to debug Maven itself, use
31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
33 | @REM ----------------------------------------------------------------------------
34 |
35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
36 | @echo off
37 | @REM set title of command window
38 | title %0
39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
41 |
42 | @REM set %HOME% to equivalent of $HOME
43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
44 |
45 | @REM Execute a user defined script before this one
46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
50 | :skipRcPre
51 |
52 | @setlocal
53 |
54 | set ERROR_CODE=0
55 |
56 | @REM To isolate internal variables from possible post scripts, we use another setlocal
57 | @setlocal
58 |
59 | @REM ==== START VALIDATION ====
60 | if not "%JAVA_HOME%" == "" goto OkJHome
61 |
62 | echo.
63 | echo Error: JAVA_HOME not found in your environment. >&2
64 | echo Please set the JAVA_HOME variable in your environment to match the >&2
65 | echo location of your Java installation. >&2
66 | echo.
67 | goto error
68 |
69 | :OkJHome
70 | if exist "%JAVA_HOME%\bin\java.exe" goto init
71 |
72 | echo.
73 | echo Error: JAVA_HOME is set to an invalid directory. >&2
74 | echo JAVA_HOME = "%JAVA_HOME%" >&2
75 | echo Please set the JAVA_HOME variable in your environment to match the >&2
76 | echo location of your Java installation. >&2
77 | echo.
78 | goto error
79 |
80 | @REM ==== END VALIDATION ====
81 |
82 | :init
83 |
84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
85 | @REM Fallback to current working directory if not found.
86 |
87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
89 |
90 | set EXEC_DIR=%CD%
91 | set WDIR=%EXEC_DIR%
92 | :findBaseDir
93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
94 | cd ..
95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
96 | set WDIR=%CD%
97 | goto findBaseDir
98 |
99 | :baseDirFound
100 | set MAVEN_PROJECTBASEDIR=%WDIR%
101 | cd "%EXEC_DIR%"
102 | goto endDetectBaseDir
103 |
104 | :baseDirNotFound
105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
106 | cd "%EXEC_DIR%"
107 |
108 | :endDetectBaseDir
109 |
110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
111 |
112 | @setlocal EnableExtensions EnableDelayedExpansion
113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
115 |
116 | :endReadAdditionalConfig
117 |
118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
123 |
124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
126 | )
127 |
128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
130 | if exist %WRAPPER_JAR% (
131 | if "%MVNW_VERBOSE%" == "true" (
132 | echo Found %WRAPPER_JAR%
133 | )
134 | ) else (
135 | if not "%MVNW_REPOURL%" == "" (
136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
137 | )
138 | if "%MVNW_VERBOSE%" == "true" (
139 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
140 | echo Downloading from: %WRAPPER_URL%
141 | )
142 |
143 | powershell -Command "&{"^
144 | "$webclient = new-object System.Net.WebClient;"^
145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
147 | "}"^
148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
149 | "}"
150 | if "%MVNW_VERBOSE%" == "true" (
151 | echo Finished downloading %WRAPPER_JAR%
152 | )
153 | )
154 | @REM End of extension
155 |
156 | @REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
157 | SET WRAPPER_SHA_256_SUM=""
158 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
159 | IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
160 | )
161 | IF NOT %WRAPPER_SHA_256_SUM%=="" (
162 | powershell -Command "&{"^
163 | "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
164 | "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
165 | " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
166 | " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
167 | " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
168 | " exit 1;"^
169 | "}"^
170 | "}"
171 | if ERRORLEVEL 1 goto error
172 | )
173 |
174 | @REM Provide a "standardized" way to retrieve the CLI args that will
175 | @REM work with both Windows and non-Windows executions.
176 | set MAVEN_CMD_LINE_ARGS=%*
177 |
178 | %MAVEN_JAVA_EXE% ^
179 | %JVM_CONFIG_MAVEN_PROPS% ^
180 | %MAVEN_OPTS% ^
181 | %MAVEN_DEBUG_OPTS% ^
182 | -classpath %WRAPPER_JAR% ^
183 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
184 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
185 | if ERRORLEVEL 1 goto error
186 | goto end
187 |
188 | :error
189 | set ERROR_CODE=1
190 |
191 | :end
192 | @endlocal & set ERROR_CODE=%ERROR_CODE%
193 |
194 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
195 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
196 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
197 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
198 | :skipRcPost
199 |
200 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
201 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
202 |
203 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
204 |
205 | cmd /C exit /B %ERROR_CODE%
206 |
--------------------------------------------------------------------------------
/frontend/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useFetch.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Demand,
3 |
4 | DonationCreation,
5 | Image,
6 | Project,
7 | ProjectCreation,
8 | User,
9 | ParticipationCreation
10 | } from "../models/models.tsx";
11 | import {create} from "zustand";
12 | import axios from "axios";
13 | import {toast} from 'react-toastify';
14 | import {NavigateFunction} from "react-router-dom";
15 |
16 | type State = {
17 | projects: Project[],
18 | fetchProjects: () => void,
19 | postProject: (requestBody: ProjectCreation) => Promise,
20 | getProjectById: (id: string) => Promise,
21 | putProject: (requestBody: Project) => Promise,
22 | deleteProject: (id: string) => void,
23 | isLoading: boolean,
24 | checkPage: (page: string) => string,
25 | page: string,
26 | mapDemandsToUserFriendly: (demands: Demand[]) => string[],
27 | mapDemandsToEnum: (string: string[]) => Demand[],
28 | postDonation: (projectId: string, donationCreation: DonationCreation) => Promise,
29 | postParticipation: (projectId: string, participationCreation: ParticipationCreation) => Promise,
30 | userName: string,
31 | login: (username: string, password: string, navigate: NavigateFunction) => void,
32 | me: () => void,
33 | register: (username: string,
34 | password: string,
35 | repeatedPassword: string,
36 | setPassword: (password: string) => void,
37 | setRepeatedPassword: (repeatedPassword: string) => void,
38 | navigate: NavigateFunction)
39 | => void,
40 | user: User,
41 | meObject: () => void,
42 | addImage: (data: FormData) => Promise,
43 | addedImage: Image,
44 | setAddedImage: (image: Image) => void,
45 | resetAddedImage: () => void,
46 | };
47 |
48 | export const useFetch = create((set, get) => ({
49 | projects: [],
50 | isLoading: true,
51 | page: "",
52 | userName: "",
53 | user:
54 | {
55 | id: "",
56 | username: "",
57 | donations: [],
58 | participations: []
59 | },
60 | addedImage: {
61 | id: "",
62 | name: "",
63 | url: ""
64 | },
65 |
66 | fetchProjects: () => {
67 | set({isLoading: true})
68 | axios
69 | .get("/api/projects")
70 | .then((response) => response.data)
71 | .then((data) => {
72 | set({projects: data})
73 | })
74 | .catch(console.error)
75 | .then(() => set({isLoading: false}))
76 | },
77 |
78 | postProject: (requestBody: ProjectCreation) => {
79 | return axios
80 | .post("/api/projects", requestBody)
81 | .then(response => {
82 | set({projects: [...get().projects, response.data]})
83 | })
84 | .then(() => toast.success("Project successfully added"))
85 | .catch((error) => {
86 | toast.error("Something went wrong");
87 | console.error(error);
88 | })
89 | },
90 |
91 | getProjectById: (id: string) => {
92 | if (!id) {
93 | throw new Error("Id is undefined")
94 | }
95 | return axios
96 | .get(`/api/projects/${id}`)
97 | .then(response => response.data)
98 | .catch(error => {
99 | toast.error("Something went wrong");
100 | console.error(error);
101 | })
102 | },
103 |
104 | putProject: (requestBody: Project) => {
105 | const {id, ...projectNoId} = requestBody;
106 | return axios
107 | .put(`/api/projects/${id}`, projectNoId)
108 | .then(response => {
109 | set({projects: get().projects.map(project => project.id === id ? response.data : project)})
110 | })
111 | .then(() => toast.success("Project successfully updated"))
112 | .catch((error) => {
113 | toast.error("Something went wrong");
114 | console.error(error);
115 | })
116 | },
117 |
118 | deleteProject: (id: string) => {
119 | const {fetchProjects} = get();
120 | axios
121 | .delete(`/api/projects/${id}`)
122 | .then(fetchProjects)
123 | .then(() => toast.success("Project successfully deleted"))
124 | .catch((error) => {
125 | toast.error("Something went wrong");
126 | console.error(error);
127 | })
128 | },
129 |
130 | checkPage: (path) => {
131 | const queryParams = new URLSearchParams(window.location.search);
132 | const filterQueryParam = queryParams.get('filter');
133 | const pathSegments = path.split("/");
134 |
135 | if (pathSegments[1] === "filter") {
136 | if (filterQueryParam === "all") {
137 | set({page: "filter-all"});
138 | } else if (filterQueryParam === "DONATION") {
139 | set({page: "filter-donation"});
140 | } else if (filterQueryParam === "PARTICIPATION") {
141 | set({page: "filter-participation"});
142 | } else {
143 | set({page: "filter"});
144 | }
145 | } else {
146 | const pages: { [key: string]: string } = {
147 | "": "/",
148 | details: "details",
149 | edit: "edit",
150 | add: "add",
151 | donate: "donate",
152 | participate: "participate",
153 | login: "login",
154 | register: "register",
155 | profile: "profile",
156 | search: "search"
157 | };
158 | const page = pathSegments[1];
159 | set({page: pages[page] || page});
160 | }
161 | return get().page;
162 | },
163 |
164 | mapDemandsToUserFriendly: (demands: Demand[]) => {
165 | const finalDemands: string[] = [];
166 | demands.forEach(demand => {
167 | switch (demand) {
168 | case "MONEYDONATION":
169 | finalDemands.push("Money Donation")
170 | break;
171 | case "DONATIONINKIND":
172 | finalDemands.push("Donation in Kind")
173 | break;
174 | case "FOODDONATION":
175 | finalDemands.push("Food Donation")
176 | break;
177 | case "DRUGDONATION":
178 | finalDemands.push("Drug Donation")
179 | break;
180 | }
181 | });
182 | return finalDemands;
183 | },
184 |
185 | mapDemandsToEnum: (string: string[]) => {
186 | const finalDemands: Demand[] = [];
187 | string.forEach(demand => {
188 | switch (demand) {
189 | case "Money Donation":
190 | finalDemands.push("MONEYDONATION")
191 | break;
192 | case "Donation in Kind":
193 | finalDemands.push("DONATIONINKIND")
194 | break;
195 | case "Food Donation":
196 | finalDemands.push("FOODDONATION")
197 | break;
198 | case "Drug Donation":
199 | finalDemands.push("DRUGDONATION")
200 | break;
201 | }
202 | });
203 | return finalDemands;
204 | },
205 |
206 | postDonation: (projectId: string, requestBody: DonationCreation) => {
207 | const {fetchProjects} = get();
208 | return axios.post(`/api/projects/donate/${projectId}`, requestBody)
209 | .then(fetchProjects)
210 | .then(() => toast.success("Donation successfully added"))
211 | .catch((error) => {
212 | toast.error("Something went wrong");
213 | console.error(error);
214 | })
215 | },
216 |
217 | postParticipation: (projectId: string, requestBody: ParticipationCreation) => {
218 | const {fetchProjects} = get();
219 | return axios.post(`/api/projects/participate/${projectId}`, requestBody)
220 | .then(fetchProjects)
221 | .then(() => toast.success("Participation successfully added"))
222 | .catch((error) => {
223 | toast.error("Something went wrong");
224 | console.error(error);
225 | })
226 | },
227 |
228 | login: (username: string, password: string, navigate: NavigateFunction) => {
229 | axios.post("/api/users/login", null, {
230 | auth: {
231 | username: username,
232 | password: password
233 | }
234 | })
235 | .then(response => {
236 | set({userName: response.data.username})
237 | navigate("/")
238 | })
239 | .then(() => toast.success("Login successful"))
240 | .catch((error) => {
241 | toast.error("You are not registered. Please register first");
242 | console.error(error);
243 | });
244 | },
245 |
246 | me: () => {
247 | axios.get("/api/users/me")
248 | .then(response => set({userName: response.data}))
249 | },
250 |
251 | meObject: () => {
252 | axios.get("/api/users/me-object")
253 | .then(response => set({user: response.data}))
254 |
255 | },
256 |
257 | register: (userName: string, password: string, repeatedPassword: string, setPassword: (password: string) => void, setRepeatedPassword: (repeatedPassword: string) => void, navigate: NavigateFunction) => {
258 | const newUserData = {
259 | "username": `${userName}`,
260 | "password": `${password}`
261 | }
262 |
263 | if (password === repeatedPassword) {
264 |
265 | axios.post("/api/users/register", newUserData)
266 | .then(response => {
267 | console.log(response);
268 | navigate("/login");
269 | })
270 | .then(() => toast.success("Registration successful"))
271 | .catch((error) => {
272 | console.error(error);
273 | if (error.response.data.errors) {
274 | toast.error(error.response.data.errors[0].defaultMessage);
275 | } else {
276 | toast.error(error.response.data.message);
277 | }
278 | })
279 | } else {
280 | toast.error("Passwords do not match");
281 | setPassword("");
282 | setRepeatedPassword("");
283 | }
284 | },
285 |
286 | addImage: (data: FormData) => {
287 | set({addedImage: {id: "", name: "", url: ""}})
288 | return axios
289 | .post('/api/upload', data, {
290 | headers: {
291 | "Content-Type": "multipart/form-data",
292 | },
293 | }).then(response => {
294 | set({addedImage: response.data})
295 |
296 | toast.success(`Image ${get().addedImage.name} successfully added`);
297 | })
298 | .catch((error) => {
299 | console.log(error)
300 | toast.error('Error adding ImageProfile' + error.response?.statusText);
301 | });
302 | },
303 |
304 | setAddedImage: (image: Image) => {
305 | set({addedImage: image})
306 | },
307 |
308 | resetAddedImage: () => {
309 | const resetImage: Image = {
310 | id: "",
311 | name: "",
312 | url: ""
313 | }
314 | set({addedImage: resetImage})
315 | },
316 | }));
317 |
--------------------------------------------------------------------------------
/backend/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Apache Maven Wrapper startup batch script, version 3.2.0
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | # e.g. to debug Maven itself, use
32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | # ----------------------------------------------------------------------------
35 |
36 | if [ -z "$MAVEN_SKIP_RC" ] ; then
37 |
38 | if [ -f /usr/local/etc/mavenrc ] ; then
39 | . /usr/local/etc/mavenrc
40 | fi
41 |
42 | if [ -f /etc/mavenrc ] ; then
43 | . /etc/mavenrc
44 | fi
45 |
46 | if [ -f "$HOME/.mavenrc" ] ; then
47 | . "$HOME/.mavenrc"
48 | fi
49 |
50 | fi
51 |
52 | # OS specific support. $var _must_ be set to either true or false.
53 | cygwin=false;
54 | darwin=false;
55 | mingw=false
56 | case "$(uname)" in
57 | CYGWIN*) cygwin=true ;;
58 | MINGW*) mingw=true;;
59 | Darwin*) darwin=true
60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
62 | if [ -z "$JAVA_HOME" ]; then
63 | if [ -x "/usr/libexec/java_home" ]; then
64 | JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
65 | else
66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
67 | fi
68 | fi
69 | ;;
70 | esac
71 |
72 | if [ -z "$JAVA_HOME" ] ; then
73 | if [ -r /etc/gentoo-release ] ; then
74 | JAVA_HOME=$(java-config --jre-home)
75 | fi
76 | fi
77 |
78 | # For Cygwin, ensure paths are in UNIX format before anything is touched
79 | if $cygwin ; then
80 | [ -n "$JAVA_HOME" ] &&
81 | JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
82 | [ -n "$CLASSPATH" ] &&
83 | CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
84 | fi
85 |
86 | # For Mingw, ensure paths are in UNIX format before anything is touched
87 | if $mingw ; then
88 | [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
89 | JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
90 | fi
91 |
92 | if [ -z "$JAVA_HOME" ]; then
93 | javaExecutable="$(which javac)"
94 | if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
95 | # readlink(1) is not available as standard on Solaris 10.
96 | readLink=$(which readlink)
97 | if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
98 | if $darwin ; then
99 | javaHome="$(dirname "\"$javaExecutable\"")"
100 | javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
101 | else
102 | javaExecutable="$(readlink -f "\"$javaExecutable\"")"
103 | fi
104 | javaHome="$(dirname "\"$javaExecutable\"")"
105 | javaHome=$(expr "$javaHome" : '\(.*\)/bin')
106 | JAVA_HOME="$javaHome"
107 | export JAVA_HOME
108 | fi
109 | fi
110 | fi
111 |
112 | if [ -z "$JAVACMD" ] ; then
113 | if [ -n "$JAVA_HOME" ] ; then
114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
115 | # IBM's JDK on AIX uses strange locations for the executables
116 | JAVACMD="$JAVA_HOME/jre/sh/java"
117 | else
118 | JAVACMD="$JAVA_HOME/bin/java"
119 | fi
120 | else
121 | JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
122 | fi
123 | fi
124 |
125 | if [ ! -x "$JAVACMD" ] ; then
126 | echo "Error: JAVA_HOME is not defined correctly." >&2
127 | echo " We cannot execute $JAVACMD" >&2
128 | exit 1
129 | fi
130 |
131 | if [ -z "$JAVA_HOME" ] ; then
132 | echo "Warning: JAVA_HOME environment variable is not set."
133 | fi
134 |
135 | # traverses directory structure from process work directory to filesystem root
136 | # first directory with .mvn subdirectory is considered project base directory
137 | find_maven_basedir() {
138 | if [ -z "$1" ]
139 | then
140 | echo "Path not specified to find_maven_basedir"
141 | return 1
142 | fi
143 |
144 | basedir="$1"
145 | wdir="$1"
146 | while [ "$wdir" != '/' ] ; do
147 | if [ -d "$wdir"/.mvn ] ; then
148 | basedir=$wdir
149 | break
150 | fi
151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
152 | if [ -d "${wdir}" ]; then
153 | wdir=$(cd "$wdir/.." || exit 1; pwd)
154 | fi
155 | # end of workaround
156 | done
157 | printf '%s' "$(cd "$basedir" || exit 1; pwd)"
158 | }
159 |
160 | # concatenates all lines of a file
161 | concat_lines() {
162 | if [ -f "$1" ]; then
163 | # Remove \r in case we run on Windows within Git Bash
164 | # and check out the repository with auto CRLF management
165 | # enabled. Otherwise, we may read lines that are delimited with
166 | # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
167 | # splitting rules.
168 | tr -s '\r\n' ' ' < "$1"
169 | fi
170 | }
171 |
172 | log() {
173 | if [ "$MVNW_VERBOSE" = true ]; then
174 | printf '%s\n' "$1"
175 | fi
176 | }
177 |
178 | BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
179 | if [ -z "$BASE_DIR" ]; then
180 | exit 1;
181 | fi
182 |
183 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
184 | log "$MAVEN_PROJECTBASEDIR"
185 |
186 | ##########################################################################################
187 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
188 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
189 | ##########################################################################################
190 | wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
191 | if [ -r "$wrapperJarPath" ]; then
192 | log "Found $wrapperJarPath"
193 | else
194 | log "Couldn't find $wrapperJarPath, downloading it ..."
195 |
196 | if [ -n "$MVNW_REPOURL" ]; then
197 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
198 | else
199 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
200 | fi
201 | while IFS="=" read -r key value; do
202 | # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
203 | safeValue=$(echo "$value" | tr -d '\r')
204 | case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
205 | esac
206 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
207 | log "Downloading from: $wrapperUrl"
208 |
209 | if $cygwin; then
210 | wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
211 | fi
212 |
213 | if command -v wget > /dev/null; then
214 | log "Found wget ... using wget"
215 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
216 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
217 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
218 | else
219 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
220 | fi
221 | elif command -v curl > /dev/null; then
222 | log "Found curl ... using curl"
223 | [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
226 | else
227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
228 | fi
229 | else
230 | log "Falling back to using Java to download"
231 | javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
232 | javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
233 | # For Cygwin, switch paths to Windows format before running javac
234 | if $cygwin; then
235 | javaSource=$(cygpath --path --windows "$javaSource")
236 | javaClass=$(cygpath --path --windows "$javaClass")
237 | fi
238 | if [ -e "$javaSource" ]; then
239 | if [ ! -e "$javaClass" ]; then
240 | log " - Compiling MavenWrapperDownloader.java ..."
241 | ("$JAVA_HOME/bin/javac" "$javaSource")
242 | fi
243 | if [ -e "$javaClass" ]; then
244 | log " - Running MavenWrapperDownloader.java ..."
245 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
246 | fi
247 | fi
248 | fi
249 | fi
250 | ##########################################################################################
251 | # End of extension
252 | ##########################################################################################
253 |
254 | # If specified, validate the SHA-256 sum of the Maven wrapper jar file
255 | wrapperSha256Sum=""
256 | while IFS="=" read -r key value; do
257 | case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
258 | esac
259 | done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
260 | if [ -n "$wrapperSha256Sum" ]; then
261 | wrapperSha256Result=false
262 | if command -v sha256sum > /dev/null; then
263 | if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
264 | wrapperSha256Result=true
265 | fi
266 | elif command -v shasum > /dev/null; then
267 | if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
268 | wrapperSha256Result=true
269 | fi
270 | else
271 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
272 | echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
273 | exit 1
274 | fi
275 | if [ $wrapperSha256Result = false ]; then
276 | echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
277 | echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
278 | echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
279 | exit 1
280 | fi
281 | fi
282 |
283 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
284 |
285 | # For Cygwin, switch paths to Windows format before running java
286 | if $cygwin; then
287 | [ -n "$JAVA_HOME" ] &&
288 | JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
289 | [ -n "$CLASSPATH" ] &&
290 | CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
291 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
292 | MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
293 | fi
294 |
295 | # Provide a "standardized" way to retrieve the CLI args that will
296 | # work with both Windows and non-Windows executions.
297 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
298 | export MAVEN_CMD_LINE_ARGS
299 |
300 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
301 |
302 | # shellcheck disable=SC2086 # safe args
303 | exec "$JAVACMD" \
304 | $MAVEN_OPTS \
305 | $MAVEN_DEBUG_OPTS \
306 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
307 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
308 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
309 |
--------------------------------------------------------------------------------
/frontend/src/pages/AddEditProject.tsx:
--------------------------------------------------------------------------------
1 | import {useFetch} from "../hooks/useFetch.tsx";
2 | import React, {ChangeEvent, useEffect, useState} from "react";
3 | import {ImageCreation, Project, ProjectCreation} from "../models/models.tsx";
4 | import {
5 | Box,
6 | Chip,
7 | Input,
8 | InputLabel,
9 | MenuItem,
10 | OutlinedInput,
11 | Select,
12 | SelectChangeEvent,
13 | Theme,
14 | useTheme
15 | } from "@mui/material";
16 | import {useNavigate, useParams} from "react-router-dom";
17 | import SaveIcon from '@mui/icons-material/Save';
18 | import CancelIcon from '@mui/icons-material/Cancel';
19 | import {toast} from "react-toastify";
20 | import CardMedia from "@mui/material/CardMedia";
21 | import {
22 | StyledBody,
23 | StyledButton, StyledChipFormControl,
24 | StyledForm,
25 | StyledTextField,
26 | StyledToggleButton,
27 | StyledToggleGroup
28 | } from "../GlobalStyles.tsx";
29 | import styled from "@emotion/styled";
30 |
31 | export default function AddEditProject() {
32 |
33 | const navigate = useNavigate();
34 | const {id} = useParams();
35 | const getProjectById = useFetch(state => state.getProjectById);
36 | const putProject = useFetch(state => state.putProject);
37 | const postProject = useFetch(state => state.postProject);
38 | const [project, setProject] = useState(undefined);
39 | const [formData, setFormData] = useState({
40 | name: "",
41 | description: "",
42 | location: "",
43 | goal: "",
44 | });
45 | const [category, setCategory] = useState<"DONATION" | "PARTICIPATION">("DONATION");
46 | const chipTheme = useTheme();
47 | const [selectedDemands, setSelectedDemands] = useState([]);
48 | const possibleDemands = [
49 | "Money Donation",
50 | "Donation in Kind",
51 | "Food Donation",
52 | "Drug Donation"
53 | ];
54 | const mapDemandsToUserFriendly = useFetch(state => state.mapDemandsToUserFriendly);
55 | const mapDemandsToEnum = useFetch(state => state.mapDemandsToEnum);
56 | const [imageName, setImageName] = useState("");
57 | const [image, setImage] = useState();
58 | const addImage = useFetch(state => state.addImage);
59 | const addedImage = useFetch(state => state.addedImage);
60 | const [showImage, setShowImage] = useState(false);
61 | const resetAddedImage = useFetch(state => state.resetAddedImage);
62 | const setAddedImage = useFetch(state => state.setAddedImage);
63 |
64 | useEffect(() => {
65 | if (id) {
66 | getProjectById(id)
67 | .then((project) => {
68 | setProject(project);
69 | })
70 | }
71 | }, [id, getProjectById]);
72 |
73 | useEffect(() => {
74 | if (project) {
75 | setFormData({
76 | name: project.name.toString(),
77 | description: project.description.toString(),
78 | location: project.location.toString(),
79 | goal: project.goal.toString(),
80 | })
81 | setSelectedDemands(mapDemandsToUserFriendly(project.demands));
82 | setCategory(project.category);
83 | setAddedImage(project.image);
84 | }
85 | }, [setAddedImage, id, project, mapDemandsToUserFriendly])
86 |
87 | function initialiseAllFields() {
88 | setFormData({
89 | name: "",
90 | description: "",
91 | location: "",
92 | goal: "",
93 | })
94 | setSelectedDemands([]);
95 | setCategory("DONATION");
96 | setShowImage(false);
97 | resetAddedImage();
98 | }
99 |
100 | function handleSubmit(event: React.FormEvent) {
101 | event.preventDefault();
102 |
103 | if (!project) {
104 | const requestBody: ProjectCreation = {
105 | name: formData.name,
106 | description: formData.description,
107 | category: category,
108 | demands: mapDemandsToEnum(selectedDemands),
109 | location: formData.location,
110 | goal: formData.goal,
111 | image: addedImage,
112 | };
113 | postProject(requestBody)
114 | .then(() => {
115 | initialiseAllFields();
116 | });
117 | }
118 |
119 | if (project) {
120 | const requestBody: Project = {
121 | id: project.id,
122 | name: formData.name,
123 | description: formData.description,
124 | category: category,
125 | demands: mapDemandsToEnum(selectedDemands),
126 | progress: project.progress,
127 | goal: formData.goal,
128 | location: formData.location,
129 | donations: project.donations,
130 | participations: project.participations,
131 | userId: project.userId,
132 | image: addedImage,
133 | };
134 | putProject(requestBody)
135 | .then(() => {
136 | initialiseAllFields();
137 | navigate(`/details/${project.id}`)
138 | });
139 | }
140 | }
141 |
142 | function handleChange(event: React.ChangeEvent) {
143 | const {name, value} = event.target;
144 | setFormData((prevFormData) => ({
145 | ...prevFormData,
146 | [name]: value,
147 | }));
148 | }
149 |
150 | function handleCategoryChange(_: React.MouseEvent, newCategory: "DONATION" | "PARTICIPATION") {
151 | setCategory(newCategory)
152 | }
153 |
154 | function handleCancelButton() {
155 | if (project) {
156 | initialiseAllFields();
157 | navigate(`/details/${project.id}`)
158 | } else {
159 | initialiseAllFields();
160 | navigate("/")
161 | }
162 | window.scrollTo(0, 0);
163 | }
164 |
165 | function getStyles(name: string, personName: readonly string[], theme: Theme) {
166 | return {
167 | fontWeight:
168 | personName.indexOf(name) === -1
169 | ? theme.typography.fontWeightRegular
170 | : theme.typography.fontWeightMedium,
171 | };
172 | }
173 |
174 | const handleChipChange = (event: SelectChangeEvent) => {
175 | const {
176 | target: {value},
177 | } = event;
178 | setSelectedDemands(
179 | typeof value === 'string' ? value.split(',') : value,
180 | );
181 | };
182 |
183 | function handleImageNameChange(event: ChangeEvent) {
184 | setImageName(event.target.value);
185 | }
186 |
187 | function handleImageInput(event: ChangeEvent) {
188 | if (event.target.files) {
189 | setImage(event.target.files[0])
190 | }
191 | }
192 |
193 | function handleImageSubmit() {
194 | if (imageName.trim() === "") {
195 | toast.error("Please enter a name for the image")
196 | return
197 | }
198 | if (!image) {
199 | toast.error("Please select an image")
200 | return
201 | }
202 |
203 | const data = new FormData()
204 | const ImageCreation: ImageCreation = {
205 | name: imageName
206 | }
207 |
208 | if (image) {
209 | data.append("file", image)
210 | }
211 |
212 | data.append("data", new Blob([JSON.stringify(ImageCreation)], {type: "application/json"}))
213 | addImage(data)
214 | .then(() => {
215 | setImageName("")
216 | setImage(undefined)
217 | setShowImage(true)
218 | });
219 | }
220 |
221 | return (
222 |
223 |
224 |
227 |
232 |
236 |
239 | Donation
240 | Participation
241 |
242 |
245 |
246 | Demands
247 | }
254 | renderValue={(selected) => (
255 |
256 | {selected.map((value) => (
257 |
258 | ))}
259 |
260 | )}
261 | MenuProps={MenuProps}>
262 | {possibleDemands.map((name) => (
263 |
269 | ))}
270 |
271 |
272 |
276 |
277 | }>IMAGE
278 | UPLOAD
279 | {showImage &&
280 | <>
281 | IMAGE PREVIEW
282 |
288 | >}
289 | }>SAVE PROJECT
290 | }>CANCEL
292 |
293 |
294 | )
295 | }
296 |
297 | const ITEM_HEIGHT = 48;
298 | const ITEM_PADDING_TOP = 8;
299 | const MenuProps = {
300 | PaperProps: {
301 | style: {
302 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
303 | width: 250,
304 | backgroundColor: '#EBE7D8',
305 | },
306 | },
307 | };
308 |
309 | const StyledHeadLine = styled.h3`
310 | padding: 0;
311 | margin: 0;
312 | font-family: "Robot", sans-serif;
313 | font-weight: 400;
314 | color: #163E56;
315 | `;
316 |
--------------------------------------------------------------------------------
/backend/src/test/java/de/neuefische/capstone/backend/ProjectServiceTest.java:
--------------------------------------------------------------------------------
1 | package de.neuefische.capstone.backend;
2 |
3 | import de.neuefische.capstone.backend.models.*;
4 | import de.neuefische.capstone.backend.security.MongoUserService;
5 | import de.neuefische.capstone.backend.security.MongoUserWithoutPassword;
6 | import de.neuefische.capstone.backend.services.IdService;
7 | import org.junit.jupiter.api.Test;
8 | import org.mockito.Mock;
9 | import org.springframework.security.core.Authentication;
10 | import org.springframework.security.core.context.SecurityContext;
11 | import org.springframework.security.core.context.SecurityContextHolder;
12 |
13 | import java.math.BigDecimal;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 | import java.util.NoSuchElementException;
17 | import java.util.Optional;
18 |
19 | import static org.junit.jupiter.api.Assertions.assertEquals;
20 | import static org.junit.jupiter.api.Assertions.assertThrows;
21 | import static org.mockito.Mockito.*;
22 |
23 |
24 | class ProjectServiceTest {
25 |
26 | ProjectRepo projectRepo = mock(ProjectRepo.class);
27 | IdService idService = mock(IdService.class);
28 | ProjectCalculations projectCalculations = mock(ProjectCalculations.class);
29 | MongoUserService mongoUserService = mock(MongoUserService.class);
30 | ProjectService projectService = new ProjectService(projectRepo, idService, projectCalculations, mongoUserService);
31 | SecurityContext securityContext = mock(SecurityContext.class);
32 | @Mock
33 | Authentication authentication = mock(Authentication.class);
34 |
35 |
36 | @Test
37 | void whenProjectAdded_thenReturnId() {
38 | //Given
39 | String expectedId = "01A";
40 |
41 | //When
42 | when(idService.createRandomId())
43 | .thenReturn("01A");
44 | String actualId = idService.createRandomId();
45 |
46 | //Then
47 | verify(idService).createRandomId();
48 | assertEquals(expectedId, actualId);
49 | }
50 |
51 | @Test
52 | void whenProjectAdded_thenReturnAddedProject() {
53 | //Given
54 | List listOfDemands = new ArrayList<>(List.of(Demand.DONATIONINKIND));
55 |
56 | Project projectWithId = new Project(
57 | "01A",
58 | "Earthquake Turkey",
59 | "Help for the people in Turkey",
60 | Category.PARTICIPATION,
61 | listOfDemands,
62 | 0,
63 | 0,
64 | "Turkey",
65 | new ArrayList<>(),
66 | new ArrayList<>(),
67 | "userId123",
68 | new Image("", "", ""));
69 |
70 |
71 | //When
72 | when(idService.createRandomId())
73 | .thenReturn("01A");
74 | when(projectRepo.insert(projectWithId))
75 | .thenReturn(projectWithId);
76 | when(securityContext.getAuthentication())
77 | .thenReturn(authentication);
78 | when(authentication.getName())
79 | .thenReturn("test");
80 | SecurityContextHolder.setContext(securityContext);
81 | when(mongoUserService.findByUsername("test"))
82 | .thenReturn(new MongoUserWithoutPassword("userId123", "test", new ArrayList<>(), new ArrayList<>()));
83 |
84 |
85 | Project actualProject = projectService.addProject(new ProjectCreation(
86 | "Earthquake Turkey",
87 | "Help for the people in Turkey",
88 | Category.PARTICIPATION,
89 | listOfDemands,
90 | "Turkey",
91 | 0,
92 | new Image("", "", "")));
93 |
94 |
95 | //Then
96 | verify(projectRepo).insert(projectWithId);
97 | verify(idService).createRandomId();
98 | assertEquals(projectWithId, actualProject);
99 | }
100 |
101 | @Test
102 | void returnListOfProjects() {
103 | //Given
104 | List expectedProjectList = new ArrayList<>(List.of(new Project(
105 | "01A",
106 | "Earthquake Turkey",
107 | "Help for the people in Turkey",
108 | Category.PARTICIPATION,
109 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
110 | 50,
111 | 0,
112 | "Turkey",
113 | new ArrayList<>(),
114 | new ArrayList<>(),
115 | "userId123",
116 | new Image("", "", ""))));
117 |
118 | //When
119 | when(projectRepo.findAll())
120 | .thenReturn(expectedProjectList);
121 |
122 | List actualProjectList = projectService.getAllProjects();
123 |
124 | //Then
125 | verify(projectRepo).findAll();
126 | assertEquals(1, projectService.getAllProjects().size());
127 | assertEquals(expectedProjectList, actualProjectList);
128 | }
129 |
130 | @Test
131 | void returnProjectById() {
132 | //Given
133 | String id = "01A";
134 | Project expectedProject = new Project(
135 | "01A",
136 | "Earthquake Turkey",
137 | "Help for the people in Turkey",
138 | Category.PARTICIPATION,
139 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
140 | 50,
141 | 0,
142 | "Turkey",
143 | new ArrayList<>(),
144 | new ArrayList<>(),
145 | "userId123",
146 | new Image("", "", ""));
147 |
148 | //When
149 | when(projectRepo.findById(id))
150 | .thenReturn(Optional.of(expectedProject));
151 |
152 | Project actualProject = projectService.getProjectById(id);
153 |
154 | //Then
155 | verify(projectRepo).findById(id);
156 | assertEquals(expectedProject, actualProject);
157 | }
158 |
159 | @Test
160 | void whenProjectUpdated_thenReturnUpdatedProject() {
161 | //Given
162 | String id = "01A";
163 | ProjectNoId projectWithoutId = new ProjectNoId(
164 | "Earthquake Turkey",
165 | "Help for the people in Turkey",
166 | Category.PARTICIPATION,
167 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
168 | 50,
169 | 0,
170 | "Turkey",
171 | new ArrayList<>(),
172 | new ArrayList<>(),
173 | "userId123",
174 | new Image("", "", ""));
175 |
176 | Project expectedProject = new Project(
177 | "01A",
178 | projectWithoutId.name(),
179 | projectWithoutId.description(),
180 | projectWithoutId.category(),
181 | projectWithoutId.demands(),
182 | projectWithoutId.progress(),
183 | projectWithoutId.goal(),
184 | projectWithoutId.location(),
185 | projectWithoutId.donations(),
186 | projectWithoutId.participations(),
187 | projectWithoutId.userId(),
188 | projectWithoutId.image());
189 |
190 | //When
191 | when(projectRepo.save(expectedProject))
192 | .thenReturn(expectedProject);
193 | when(projectRepo.existsById(id))
194 | .thenReturn(true);
195 | when(securityContext.getAuthentication())
196 | .thenReturn(authentication);
197 | when(authentication.getName())
198 | .thenReturn("test");
199 | SecurityContextHolder.setContext(securityContext);
200 | when(mongoUserService.findByUsername("test"))
201 | .thenReturn(new MongoUserWithoutPassword("userId123", "test", new ArrayList<>(), new ArrayList<>()));
202 |
203 | Project actualProject = projectService.updateProject("01A", projectWithoutId);
204 |
205 |
206 | //Then
207 | verify(projectRepo).save(expectedProject);
208 | assertEquals(expectedProject, actualProject);
209 | }
210 |
211 | @Test
212 | void whenNoId_thenThrowException() {
213 | //Given
214 | String id = "01A";
215 | ProjectNoId projectWithoutId = new ProjectNoId(
216 | "Earthquake Turkey",
217 | "Help for the people in Turkey",
218 | Category.PARTICIPATION,
219 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
220 | 50,
221 | 0,
222 | "Turkey",
223 | new ArrayList<>(),
224 | new ArrayList<>(),
225 | "userId123",
226 | new Image("", "", ""));
227 |
228 |
229 | //When
230 | when(projectRepo.existsById("01A"))
231 | .thenReturn(false);
232 |
233 |
234 | //Then
235 | assertThrows(NoSuchElementException.class, () -> projectService.updateProject(id, projectWithoutId));
236 | verify(projectRepo).existsById(id);
237 | }
238 |
239 | @Test
240 | void whenProjectDeleted_verifyRepoCalls() {
241 | //Given
242 | String id = "01A";
243 |
244 |
245 | //When
246 | when(projectRepo.existsById(id))
247 | .thenReturn(true);
248 | projectService.deleteProject(id);
249 |
250 |
251 | //Then
252 | verify(projectRepo).existsById(id);
253 | verify(projectRepo).deleteById(id);
254 | }
255 |
256 | @Test
257 | void whenNoneExistingProjectDeleted_thenThrowException() {
258 | //Given
259 | String id = "01A";
260 |
261 | //When
262 | when(projectRepo.existsById(id))
263 | .thenReturn(false);
264 |
265 |
266 | //Then
267 | assertThrows(NoSuchElementException.class, () -> projectService.deleteProject(id));
268 | verify(projectRepo).existsById(id);
269 | verify(projectRepo, never()).deleteById(id);
270 | }
271 |
272 | @Test
273 | void whenDonationAdded_thenReturnProject() {
274 | //Given
275 | String projectId = "01A";
276 |
277 | Project repoProject = new Project(
278 | "01A",
279 | "Earthquake Turkey",
280 | "Help for the people in Turkey",
281 | Category.PARTICIPATION,
282 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
283 | 0,
284 | 100,
285 | "Turkey",
286 | new ArrayList<>(),
287 | new ArrayList<>(),
288 | "userId123",
289 | new Image("", "", ""));
290 |
291 | DonationCreation donationToAdd = new DonationCreation(
292 | repoProject.id(),
293 | repoProject.name(),
294 | new BigDecimal(50));
295 |
296 | Donation finalDonation = new Donation(
297 | "dono-02A",
298 | repoProject.id(),
299 | repoProject.name(),
300 | "test",
301 | new BigDecimal(50),
302 | "userId123");
303 |
304 | Project projectToSave = new Project(
305 | "01A",
306 | "Earthquake Turkey",
307 | "Help for the people in Turkey",
308 | Category.PARTICIPATION,
309 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
310 | 0,
311 | 100,
312 | "Turkey",
313 | List.of(finalDonation),
314 | new ArrayList<>(),
315 | "userId123",
316 | new Image("", "", ""));
317 |
318 | Project projectWithProgress = new Project(
319 | "01A",
320 | "Earthquake Turkey",
321 | "Help for the people in Turkey",
322 | Category.PARTICIPATION,
323 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
324 | 50,
325 | 100,
326 | "Turkey",
327 | List.of(finalDonation),
328 | new ArrayList<>(),
329 | "userId123",
330 | new Image("", "", ""));
331 |
332 |
333 | //When
334 | when(projectRepo.findById(projectId))
335 | .thenReturn(Optional.of(repoProject));
336 | when(projectCalculations.calculateProgressForDonations(projectToSave))
337 | .thenReturn(projectWithProgress);
338 | when(projectRepo.save(projectWithProgress))
339 | .thenReturn(projectWithProgress);
340 | when(idService.createRandomId())
341 | .thenReturn("dono-02A");
342 | when(securityContext.getAuthentication())
343 | .thenReturn(authentication);
344 | when(authentication.getName())
345 | .thenReturn("test");
346 | SecurityContextHolder.setContext(securityContext);
347 | when(mongoUserService.findByUsername("test"))
348 | .thenReturn(new MongoUserWithoutPassword("userId123", "test", new ArrayList<>(), new ArrayList<>()));
349 |
350 |
351 | Project actualProject = projectService.addDonation(projectId, donationToAdd);
352 |
353 |
354 | //Then
355 | verify(projectRepo).findById(projectId);
356 | verify(projectCalculations).calculateProgressForDonations(projectToSave);
357 | verify(projectRepo).save(projectWithProgress);
358 | verify(idService).createRandomId();
359 | assertEquals(projectWithProgress, actualProject);
360 | }
361 |
362 | @Test
363 | void returnProject_whenAddParticipation() {
364 | //Given
365 | String projectId = "01A";
366 |
367 | Project repoProject = new Project(
368 | "01A",
369 | "Earthquake Turkey",
370 | "Help for the people in Turkey",
371 | Category.PARTICIPATION,
372 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
373 | 0,
374 | 100,
375 | "Turkey",
376 | new ArrayList<>(),
377 | new ArrayList<>(),
378 | "userId123",
379 | new Image("", "", ""));
380 |
381 | ParticipationCreation participationToAdd = new ParticipationCreation(
382 | repoProject.id(),
383 | repoProject.name()
384 | );
385 |
386 | Participation finalParticipation = new Participation(
387 | "vol-02A",
388 | repoProject.id(),
389 | repoProject.name(),
390 | "test",
391 | "userId123"
392 | );
393 |
394 | Project projectToSave = new Project(
395 | "01A",
396 | "Earthquake Turkey",
397 | "Help for the people in Turkey",
398 | Category.PARTICIPATION,
399 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
400 | 0,
401 | 100,
402 | "Turkey",
403 | new ArrayList<>(),
404 | List.of(finalParticipation),
405 | "userId123",
406 | new Image("", "", ""));
407 |
408 | Project projectWithProgress = new Project(
409 | "01A",
410 | "Earthquake Turkey",
411 | "Help for the people in Turkey",
412 | Category.PARTICIPATION,
413 | List.of(Demand.DONATIONINKIND, Demand.MONEYDONATION),
414 | 1,
415 | 100,
416 | "Turkey",
417 | new ArrayList<>(),
418 | List.of(finalParticipation),
419 | "userId123",
420 | new Image("", "", ""));
421 |
422 |
423 | //When
424 | when(projectRepo.findById(projectId))
425 | .thenReturn(Optional.of(repoProject));
426 | when(projectCalculations.calculateProgressForParticipations(projectToSave))
427 | .thenReturn(projectWithProgress);
428 | when(projectRepo.save(projectWithProgress))
429 | .thenReturn(projectWithProgress);
430 | when(idService.createRandomId())
431 | .thenReturn("vol-02A");
432 | when(securityContext.getAuthentication())
433 | .thenReturn(authentication);
434 | when(authentication.getName())
435 | .thenReturn("test");
436 | SecurityContextHolder.setContext(securityContext);
437 | when(mongoUserService.findByUsername("test"))
438 | .thenReturn(new MongoUserWithoutPassword("userId123", "test", new ArrayList<>(), new ArrayList<>()));
439 |
440 | Project actualProject = projectService.addParticipation(projectId, participationToAdd);
441 |
442 |
443 | //Then
444 | verify(projectRepo).findById(projectId);
445 | verify(projectCalculations).calculateProgressForParticipations(projectToSave);
446 | verify(projectRepo).save(projectWithProgress);
447 | verify(idService).createRandomId();
448 | assertEquals(projectWithProgress, actualProject);
449 | }
450 | }
451 |
--------------------------------------------------------------------------------