diff --git a/postrify-backend/src/main/java/com/postrify/postrifybackend/controller/UserController.java b/postrify-backend/src/main/java/com/postrify/postrifybackend/controller/UserController.java new file mode 100644 index 0000000..dfe774b --- /dev/null +++ b/postrify-backend/src/main/java/com/postrify/postrifybackend/controller/UserController.java @@ -0,0 +1,31 @@ +package com.postrify.postrifybackend.controller; + +import com.postrify.postrifybackend.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired private UserService userService; + + @GetMapping("/{username}/image") + public ResponseEntity getUserImage(@PathVariable final String username) { + String base64Image = userService.getUserImage(username); + return ResponseEntity.ok(base64Image); + } + + @PutMapping("/{username}/image") + public ResponseEntity updateUserImage( + @PathVariable final String username, @RequestBody final String base64Image) { + userService.updateUserImage(username, base64Image); + return ResponseEntity.ok("User image updated successfully!"); + } +} diff --git a/postrify-backend/src/main/java/com/postrify/postrifybackend/dto/UserDTO.java b/postrify-backend/src/main/java/com/postrify/postrifybackend/dto/UserDTO.java index b9e6e93..ff36285 100644 --- a/postrify-backend/src/main/java/com/postrify/postrifybackend/dto/UserDTO.java +++ b/postrify-backend/src/main/java/com/postrify/postrifybackend/dto/UserDTO.java @@ -4,11 +4,13 @@ public class UserDTO { private Long id; private String username; private String email; + private String image; - public UserDTO(final Long id, final String username, final String email) { + public UserDTO(final Long id, final String username, final String email, final String image) { this.id = id; this.username = username; this.email = email; + this.image = image; } public Long getId() { @@ -34,4 +36,12 @@ public void setUsername(final String username) { public void setEmail(final String email) { this.email = email; } + + public String getImage() { + return image; + } + + public void setImage(final String image) { + this.image = image; + } } diff --git a/postrify-backend/src/main/java/com/postrify/postrifybackend/model/User.java b/postrify-backend/src/main/java/com/postrify/postrifybackend/model/User.java index 3a7938e..e7081d2 100644 --- a/postrify-backend/src/main/java/com/postrify/postrifybackend/model/User.java +++ b/postrify-backend/src/main/java/com/postrify/postrifybackend/model/User.java @@ -20,6 +20,9 @@ public class User { @Column(nullable = false, unique = true) private String email; + @Column(columnDefinition = "TEXT") + private String image; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private List posts; @@ -59,4 +62,12 @@ public List getPosts() { public void setPosts(final List posts) { this.posts = posts; } + + public String getImage() { + return image; + } + + public void setImage(final String image) { + this.image = image; + } } diff --git a/postrify-backend/src/main/java/com/postrify/postrifybackend/service/PostService.java b/postrify-backend/src/main/java/com/postrify/postrifybackend/service/PostService.java index fa91b90..2961bba 100644 --- a/postrify-backend/src/main/java/com/postrify/postrifybackend/service/PostService.java +++ b/postrify-backend/src/main/java/com/postrify/postrifybackend/service/PostService.java @@ -74,7 +74,8 @@ public void deletePost(final Long id, final User currentUser) { private PostResponseDTO convertToDTO(final Post post) { User user = post.getUser(); - UserDTO userDTO = new UserDTO(user.getId(), user.getUsername(), user.getEmail()); + UserDTO userDTO = + new UserDTO(user.getId(), user.getUsername(), user.getEmail(), user.getImage()); return new PostResponseDTO( post.getId(), diff --git a/postrify-backend/src/main/java/com/postrify/postrifybackend/service/UserService.java b/postrify-backend/src/main/java/com/postrify/postrifybackend/service/UserService.java index ea833cd..954a755 100644 --- a/postrify-backend/src/main/java/com/postrify/postrifybackend/service/UserService.java +++ b/postrify-backend/src/main/java/com/postrify/postrifybackend/service/UserService.java @@ -29,6 +29,17 @@ public User registerUser(final User user) { return userRepository.save(user); } + public String getUserImage(final String username) { + User user = findByUsername(username); + return user.getImage(); + } + + public void updateUserImage(final String username, final String base64Image) { + User user = findByUsername(username); + user.setImage(base64Image); + userRepository.save(user); + } + public User findByUsername(final String username) { return userRepository .findByUsername(username) diff --git a/postrify-backend/src/test/java/com/postrify/postrifybackend/PostControllerTest.java b/postrify-backend/src/test/java/com/postrify/postrifybackend/PostControllerTest.java index 72108ed..5009a94 100644 --- a/postrify-backend/src/test/java/com/postrify/postrifybackend/PostControllerTest.java +++ b/postrify-backend/src/test/java/com/postrify/postrifybackend/PostControllerTest.java @@ -48,7 +48,7 @@ void setUp() { @SuppressWarnings("unchecked") @Test void getAllPosts_Success() { - UserDTO userDTO = new UserDTO(1L, "jordi", "jordi@mail.com"); + UserDTO userDTO = new UserDTO(1L, "jordi", "jordi@mail.com", null); PostResponseDTO post1 = new PostResponseDTO( 1L, "Post 1", "Content 1", userDTO, LocalDateTime.now(), LocalDateTime.now()); @@ -69,10 +69,11 @@ void getAllPosts_Success() { verify(postService, times(1)).getAllPosts(pageable); } + @SuppressWarnings("null") @Test void getPostById_Found() { Long postId = 1L; - UserDTO userDTO = new UserDTO(1L, "jordi", "jordi@mail.com"); + UserDTO userDTO = new UserDTO(1L, "jordi", "jordi@mail.com", null); PostResponseDTO post = new PostResponseDTO( postId, "Post 1", "Content 1", userDTO, LocalDateTime.now(), LocalDateTime.now()); @@ -102,7 +103,7 @@ void getPostById_NotFound() { @Test void getPostsByUser_Success() { Long userId = 1L; - UserDTO userDTO = new UserDTO(userId, "jordi", "jordi@mail.com"); + UserDTO userDTO = new UserDTO(userId, "jordi", "jordi@mail.com", null); PostResponseDTO post1 = new PostResponseDTO( 1L, "Post 1", "Content 1", userDTO, LocalDateTime.now(), LocalDateTime.now()); @@ -120,6 +121,7 @@ void getPostsByUser_Success() { verify(postService, times(1)).getPostsByUser(userId); } + @SuppressWarnings("null") @Test void createPost_Success() { PostRequest postRequest = new PostRequest(); @@ -136,7 +138,7 @@ void createPost_Success() { 1L, postRequest.getTitle(), postRequest.getContent(), - new UserDTO(user.getId(), user.getUsername(), user.getEmail()), + new UserDTO(user.getId(), user.getUsername(), user.getEmail(), null), LocalDateTime.now(), LocalDateTime.now()); @@ -176,6 +178,7 @@ void createPost_UserNotFound() { verify(postService, times(0)).createPost(any(Post.class)); } + @SuppressWarnings("null") @Test void updatePost_Success() { Long postId = 1L; @@ -193,7 +196,7 @@ void updatePost_Success() { postId, postRequest.getTitle(), postRequest.getContent(), - new UserDTO(user.getId(), user.getUsername(), user.getEmail()), + new UserDTO(user.getId(), user.getUsername(), user.getEmail(), null), LocalDateTime.now(), LocalDateTime.now()); diff --git a/postrify-frontend/public/assets/placeholder.jpg b/postrify-frontend/public/assets/placeholder.jpg new file mode 100644 index 0000000..f31bb92 Binary files /dev/null and b/postrify-frontend/public/assets/placeholder.jpg differ diff --git a/postrify-frontend/src/app/components/header/header.component.ts b/postrify-frontend/src/app/components/header/header.component.ts index 6f6697a..913c1b4 100644 --- a/postrify-frontend/src/app/components/header/header.component.ts +++ b/postrify-frontend/src/app/components/header/header.component.ts @@ -1,14 +1,39 @@ import { Component, OnInit } from '@angular/core'; import { RouterLink } from '@angular/router'; import { AuthService } from '../../services/auth.service'; +import { SettingsModalComponent } from '../settings-modal/settings-modal.component'; +import { Subscription } from 'rxjs'; +import { UserImageService } from '../../services/user-image.service'; @Component({ selector: 'app-header', standalone: true, - imports: [RouterLink], + imports: [RouterLink, SettingsModalComponent], template: `
+ @if (authService.isAuthenticated()) { + + }
+ @if (isSettingsOpen) { + + } `, styles: [ ` @@ -146,13 +184,16 @@ import { AuthService } from '../../services/auth.service'; color: var(--header-text); font-weight: 500; padding-bottom: 5px; + margin-top: 5px; } .logout-button { + margin-top: 5px; color: var(--header-text); } - .toggle-button { + .toggle-button, + .settings-button { color: var(--header-text); } @@ -191,21 +232,49 @@ import { AuthService } from '../../services/auth.service'; margin-right: 0rem; } } + + @media (max-width: 450px) { + .username { + display: none; + } + } + + .current-photo { + width: 35px; + height: 35px; + border-radius: 50%; + background-size: cover; + background-position: center; + position: relative; + border: 2px solid var(--border-color); + margin-right: 0.5rem; + } `, ], }) export class HeaderComponent implements OnInit { + private imageUpdateSubscription: Subscription | undefined; + isDarkMode = false; logoSrc = 'assets/logo-light.png'; isAuthenticated = false; username: string | null = null; + userImage: string | null = null; + isSettingsOpen = false; - constructor(public authService: AuthService) {} + constructor( + public authService: AuthService, + private userImageService: UserImageService, + ) {} ngOnInit() { this.loadDarkModePreference(); this.updateLogo(); this.checkAuthentication(); + this.imageUpdateSubscription = + this.userImageService.imageUpdated$.subscribe(() => { + this.checkAuthentication(); + }); } toggleDarkMode() { @@ -239,6 +308,9 @@ export class HeaderComponent implements OnInit { this.isAuthenticated = this.authService.isAuthenticated(); if (this.isAuthenticated) { this.username = this.authService.getUsername(); + this.authService.getUserImage().subscribe((image: string) => { + this.userImage = image; + }); } } @@ -246,4 +318,12 @@ export class HeaderComponent implements OnInit { this.authService.logout(); this.isAuthenticated = false; } + + openSettings() { + this.isSettingsOpen = true; + } + + closeSettings() { + this.isSettingsOpen = false; + } } diff --git a/postrify-frontend/src/app/components/home/home.component.ts b/postrify-frontend/src/app/components/home/home.component.ts index c3b3918..edfb6fa 100644 --- a/postrify-frontend/src/app/components/home/home.component.ts +++ b/postrify-frontend/src/app/components/home/home.component.ts @@ -1,10 +1,12 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, OnDestroy } from '@angular/core'; import { PostResponseDTO } from '../../models/post-response.model'; import { PostService } from '../../services/post.service'; import { Router } from '@angular/router'; import { AuthService } from '../../services/auth.service'; import { CommonModule } from '@angular/common'; import { Page } from '../../models/page.model'; +import { Subscription } from 'rxjs'; +import { UserImageService } from '../../services/user-image.service'; @Component({ selector: 'app-home', @@ -29,31 +31,43 @@ import { Page } from '../../models/page.model'; }}{{ post.content.length > 100 ? '...' : '' }}

} -
- - - - - - -
+ + + + + + + } @if (isLogged) { + + + + + + `, + styles: [ + ` + .modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + } + + .modal-container { + background: var(--card-bg); + border-radius: 8px; + width: 90%; + max-width: 500px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + overflow: hidden; + color: var(--text-color); + } + + .modal-header { + padding: 1rem 1rem 1rem 1.75rem; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + } + + .modal-header h2 { + margin: 0; + font-size: 1.25rem; + color: var(--text-color); + } + + .close-button { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + color: var(--text-color); + padding: 0.5rem; + } + + .close-button:hover { + color: var(--primary-color); + } + + .modal-body { + padding: 2rem; + background-color: var(--card-bg); + } + + .photo-upload-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; + } + + .current-photo { + width: 150px; + height: 150px; + border-radius: 50%; + background-size: cover; + background-position: center; + position: relative; + cursor: pointer; + border: 3px solid var(--border-color); + } + + .photo-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.2s; + } + + .photo-overlay span { + color: white; + font-size: 0.875rem; + font-weight: 500; + } + + .current-photo:hover .photo-overlay { + opacity: 1; + } + + .hidden { + display: none; + } + + .photo-instructions { + text-align: center; + color: var(--secondary-text-color); + font-size: 0.875rem; + } + + .file-name { + font-size: 0.875rem; + color: var(--secondary-text-color); + margin-top: 0.5rem; + } + + .error-message { + margin-top: 1rem; + padding: 0.75rem; + background-color: var(--error-color); + border-radius: 4px; + color: white; + text-align: center; + font-size: 0.875rem; + } + + .modal-footer { + padding: 1rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 1rem; + background-color: var(--card-bg); + } + + button { + padding: 0.75rem 1rem; + border-radius: 4px; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + font-size: 16px; + font-family: 'Quicksand', sans-serif; + } + + button:disabled { + background-color: var(--disabled-color); + cursor: not-allowed; + } + + .cancel-button { + background-color: transparent; + border: 1px solid var(--border-color); + color: var(--text-color); + } + + .cancel-button:hover:not(:disabled) { + border-color: var(--primary-color); + color: var(--primary-color); + } + + .save-button { + background-color: var(--primary-color); + border: none; + color: white; + } + + .save-button:hover:not(:disabled) { + background-color: var(--primary-color-hover); + } + + @media (max-width: 480px) { + .modal-container { + width: 95%; + margin: 1rem; + } + + .modal-body { + padding: 1rem; + } + + .modal-footer { + padding: 1rem; + } + } + `, + ], +}) +export class SettingsModalComponent implements OnInit { + @Output() closeModalEvent = new EventEmitter(); + + selectedFile: File | null = null; + previewUrl: string | null = null; + errorMessage = ''; + isLoading = false; + + constructor( + private authService: AuthService, + private userImageService: UserImageService, + ) {} + + ngOnInit() { + this.loadCurrentUserImage(); + } + + loadCurrentUserImage() { + this.authService.getUserImage().subscribe({ + next: (imageData) => { + if (imageData) { + this.previewUrl = imageData; + } + }, + error: (error) => { + console.error('Error loading user image:', error); + }, + }); + } + + closeModal() { + this.closeModalEvent.emit(); + } + + onFileSelected(event: Event) { + const input = event.target as HTMLInputElement; + if (input.files && input.files.length > 0) { + const file = input.files[0]; + + if (!file.type.startsWith('image/')) { + this.errorMessage = 'Please select a valid image file'; + return; + } + + if (file.size > 5 * 1024 * 1024) { + this.errorMessage = 'The image file size must be less than 5MB'; + return; + } + + this.selectedFile = file; + this.errorMessage = ''; + + const reader = new FileReader(); + reader.onload = (e) => { + this.previewUrl = e.target?.result as string; + }; + reader.readAsDataURL(file); + } + } + + saveChanges() { + if (!this.selectedFile) return; + + this.isLoading = true; + this.errorMessage = ''; + + const reader = new FileReader(); + reader.onload = (e) => { + const base64Image = e.target?.result as string; + this.authService.uploadUserImage(base64Image).subscribe({ + next: () => { + this.isLoading = false; + this.userImageService.notifyImageUpdate(); + this.closeModal(); + }, + error: (error) => { + this.isLoading = false; + this.errorMessage = + 'An error occurred while saving the image. Please try again.'; + console.error('Error uploading image:', error); + }, + }); + }; + reader.readAsDataURL(this.selectedFile); + } +} diff --git a/postrify-frontend/src/app/models/user-dto.model.ts b/postrify-frontend/src/app/models/user-dto.model.ts index af216ef..ba50b7c 100644 --- a/postrify-frontend/src/app/models/user-dto.model.ts +++ b/postrify-frontend/src/app/models/user-dto.model.ts @@ -2,4 +2,5 @@ export interface UserDTO { id: number; username: string; email: string; + image: string; } diff --git a/postrify-frontend/src/app/services/auth.service.ts b/postrify-frontend/src/app/services/auth.service.ts index 4865400..0d47385 100644 --- a/postrify-frontend/src/app/services/auth.service.ts +++ b/postrify-frontend/src/app/services/auth.service.ts @@ -10,6 +10,7 @@ import { RegisterResponse } from '../models/register-response'; }) export class AuthService { private apiUrl = environment.apiUrl + '/api/auth'; + private userApiUrl = environment.apiUrl + '/api/users'; constructor(private http: HttpClient) {} @@ -55,4 +56,18 @@ export class AuthService { isAuthenticated(): boolean { return !!this.getToken(); } + + uploadUserImage(base64Image: string) { + const username = this.getUsername(); + return this.http.put(`${this.userApiUrl}/${username}/image`, base64Image, { + responseType: 'text', + }); + } + + getUserImage() { + const username = this.getUsername(); + return this.http.get(`${this.userApiUrl}/${username}/image`, { + responseType: 'text', + }); + } } diff --git a/postrify-frontend/src/app/services/user-image.service.ts b/postrify-frontend/src/app/services/user-image.service.ts new file mode 100644 index 0000000..f591a0c --- /dev/null +++ b/postrify-frontend/src/app/services/user-image.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class UserImageService { + private imageUpdateSource = new BehaviorSubject(undefined); + imageUpdated$ = this.imageUpdateSource.asObservable(); + + notifyImageUpdate() { + this.imageUpdateSource.next(); + } +}