commit
92026f71e2
@ -4,9 +4,12 @@ import io.swagger.v3.oas.models.OpenAPI;
|
|||||||
import io.swagger.v3.oas.models.info.Info;
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||||
|
import io.swagger.v3.oas.models.servers.Server;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class OpenApiConfig {
|
public class OpenApiConfig {
|
||||||
|
|
||||||
@ -17,12 +20,17 @@ public class OpenApiConfig {
|
|||||||
.title("Wesal API")
|
.title("Wesal API")
|
||||||
.description("Social media application API")
|
.description("Social media application API")
|
||||||
.version("1.0.0"))
|
.version("1.0.0"))
|
||||||
|
.servers(List.of(
|
||||||
|
new Server().url("http://localhost:8080").description("Development server"),
|
||||||
|
new Server().url("https://api.wesal.online").description("Production server")
|
||||||
|
))
|
||||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||||
.components(new io.swagger.v3.oas.models.Components()
|
.components(new io.swagger.v3.oas.models.Components()
|
||||||
.addSecuritySchemes("Bearer Authentication",
|
.addSecuritySchemes("Bearer Authentication",
|
||||||
new SecurityScheme()
|
new SecurityScheme()
|
||||||
.type(SecurityScheme.Type.HTTP)
|
.type(SecurityScheme.Type.HTTP)
|
||||||
.scheme("bearer")
|
.scheme("bearer")
|
||||||
.bearerFormat("JWT")));
|
.bearerFormat("JWT")
|
||||||
|
.description("Enter JWT token (without 'Bearer ' prefix)")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ public class SecurityConfig {
|
|||||||
.csrf(csrf -> csrf.disable())
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/", "/login", "/swagger-ui/**", "/v3/api-docs/**", "/docs/**", "/docs").permitAll()
|
||||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1,187 @@
|
|||||||
|
package online.wesal.wesal.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import online.wesal.wesal.dto.ApiResponse;
|
||||||
|
import online.wesal.wesal.dto.PostCreateRequestDTO;
|
||||||
|
import online.wesal.wesal.dto.PostLikeRequestDTO;
|
||||||
|
import online.wesal.wesal.dto.PostResponseDTO;
|
||||||
|
import online.wesal.wesal.entity.Post;
|
||||||
|
import online.wesal.wesal.service.PostService;
|
||||||
|
import online.wesal.wesal.service.UserService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.validation.BindingResult;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/posts")
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
@Tag(name = "Posts", description = "Post management endpoints")
|
||||||
|
public class PostController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostService postService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@PostMapping(value = "/create", consumes = "application/json", produces = "application/json")
|
||||||
|
@Operation(summary = "Create post", description = "Create a new post")
|
||||||
|
public ResponseEntity<ApiResponse<PostResponseDTO>> createPost(
|
||||||
|
@Valid @RequestBody PostCreateRequestDTO request,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
String errorMessage = bindingResult.getFieldErrors().stream()
|
||||||
|
.map(error -> {
|
||||||
|
String field = error.getField();
|
||||||
|
if ("body".equals(field)) return "Post body is required and cannot exceed 2000 characters";
|
||||||
|
return error.getDefaultMessage();
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElse("Invalid input");
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PostResponseDTO response = postService.createPostWithResponse(request.getBody());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("User not found")) {
|
||||||
|
message = "Authentication error. Please log in again.";
|
||||||
|
} else {
|
||||||
|
message = "Something went wrong.. We're sorry but try again later";
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
@Operation(summary = "Get all recent posts", description = "Get all posts from the last 7 days, ordered by creation date (latest first)")
|
||||||
|
public ResponseEntity<ApiResponse<List<PostResponseDTO>>> getAllRecentPosts() {
|
||||||
|
try {
|
||||||
|
List<PostResponseDTO> response = postService.getAllRecentPosts();
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user")
|
||||||
|
@Operation(summary = "Get user posts", description = "Get all posts by a specific user, ordered by creation date (latest first). If no id provided, returns authenticated user's posts.")
|
||||||
|
public ResponseEntity<ApiResponse<List<PostResponseDTO>>> getUserPosts(
|
||||||
|
@RequestParam(required = false) Long id,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
Long targetUserId;
|
||||||
|
if (id == null) {
|
||||||
|
// Use authenticated user's ID when no id is provided
|
||||||
|
String userEmail = authentication.getName();
|
||||||
|
targetUserId = userService.getCurrentUser().getId();
|
||||||
|
} else {
|
||||||
|
if (id <= 0) {
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error("Valid user ID is required"));
|
||||||
|
}
|
||||||
|
targetUserId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<PostResponseDTO> response = postService.getUserPosts(targetUserId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("User not found")) {
|
||||||
|
message = "User not found";
|
||||||
|
} else {
|
||||||
|
message = "Something went wrong.. We're sorry but try again later";
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/like", consumes = "application/json", produces = "application/json")
|
||||||
|
@Operation(summary = "Like post", description = "Like a post. Returns the updated post with new like count.")
|
||||||
|
public ResponseEntity<ApiResponse<PostResponseDTO>> likePost(
|
||||||
|
@Valid @RequestBody PostLikeRequestDTO request,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
String errorMessage = bindingResult.getFieldErrors().stream()
|
||||||
|
.map(error -> {
|
||||||
|
String field = error.getField();
|
||||||
|
if ("postId".equals(field)) return "Valid post ID is required";
|
||||||
|
return error.getDefaultMessage();
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElse("Invalid input");
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PostResponseDTO response = postService.likePost(request.getPostId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("Post not found")) {
|
||||||
|
message = "Post not found";
|
||||||
|
} else if (e.getMessage().contains("User not found")) {
|
||||||
|
message = "Authentication error. Please log in again.";
|
||||||
|
} else {
|
||||||
|
message = "Something went wrong.. We're sorry but try again later";
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/unlike", consumes = "application/json", produces = "application/json")
|
||||||
|
@Operation(summary = "Unlike post", description = "Unlike a post. Returns the updated post with new like count.")
|
||||||
|
public ResponseEntity<ApiResponse<PostResponseDTO>> unlikePost(
|
||||||
|
@Valid @RequestBody PostLikeRequestDTO request,
|
||||||
|
BindingResult bindingResult,
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
|
if (bindingResult.hasErrors()) {
|
||||||
|
String errorMessage = bindingResult.getFieldErrors().stream()
|
||||||
|
.map(error -> {
|
||||||
|
String field = error.getField();
|
||||||
|
if ("postId".equals(field)) return "Valid post ID is required";
|
||||||
|
return error.getDefaultMessage();
|
||||||
|
})
|
||||||
|
.findFirst()
|
||||||
|
.orElse("Invalid input");
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PostResponseDTO response = postService.unlikePost(request.getPostId());
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(response));
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
String message;
|
||||||
|
if (e.getMessage().contains("Post not found")) {
|
||||||
|
message = "Post not found";
|
||||||
|
} else if (e.getMessage().contains("User not found")) {
|
||||||
|
message = "Authentication error. Please log in again.";
|
||||||
|
} else {
|
||||||
|
message = "Something went wrong.. We're sorry but try again later";
|
||||||
|
}
|
||||||
|
return ResponseEntity.badRequest().body(ApiResponse.error(message));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.status(500).body(ApiResponse.error("Something went wrong.. We're sorry but try again later"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ public class CreateUserRequest {
|
|||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(min = 8)
|
@Size(min = 6)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
|
|||||||
32
backend/src/main/java/online/wesal/wesal/dto/CreatorDTO.java
Normal file
32
backend/src/main/java/online/wesal/wesal/dto/CreatorDTO.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
|
||||||
|
public class CreatorDTO {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String displayName;
|
||||||
|
|
||||||
|
public CreatorDTO() {}
|
||||||
|
|
||||||
|
public CreatorDTO(User user) {
|
||||||
|
this.id = String.valueOf(user.getId());
|
||||||
|
this.displayName = user.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDisplayName(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
public class PostCreateRequestDTO {
|
||||||
|
|
||||||
|
@NotBlank(message = "Post body cannot be empty")
|
||||||
|
@Size(max = 2000, message = "Post body cannot exceed 2000 characters")
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
public PostCreateRequestDTO() {}
|
||||||
|
|
||||||
|
public PostCreateRequestDTO(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import jakarta.validation.constraints.Positive;
|
||||||
|
|
||||||
|
public class PostLikeRequestDTO {
|
||||||
|
|
||||||
|
@NotNull(message = "Post ID is required")
|
||||||
|
@Positive(message = "Post ID must be a positive number")
|
||||||
|
private Long postId;
|
||||||
|
|
||||||
|
public PostLikeRequestDTO() {}
|
||||||
|
|
||||||
|
public PostLikeRequestDTO(Long postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPostId() {
|
||||||
|
return postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostId(Long postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package online.wesal.wesal.dto;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.Post;
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
public class PostResponseDTO {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private CreatorDTO creator;
|
||||||
|
private String body;
|
||||||
|
private String likes;
|
||||||
|
private String comments;
|
||||||
|
private boolean liked;
|
||||||
|
private LocalDateTime creationDate;
|
||||||
|
|
||||||
|
public PostResponseDTO() {}
|
||||||
|
|
||||||
|
public PostResponseDTO(Post post, User creator) {
|
||||||
|
this.id = String.valueOf(post.getId());
|
||||||
|
this.creator = new CreatorDTO(creator);
|
||||||
|
this.body = post.getBody();
|
||||||
|
this.likes = String.valueOf(post.getLikes());
|
||||||
|
this.comments = String.valueOf(post.getComments());
|
||||||
|
this.liked = false; // Default value, will be set by service
|
||||||
|
this.creationDate = post.getCreationDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostResponseDTO(Post post, User creator, boolean liked) {
|
||||||
|
this.id = String.valueOf(post.getId());
|
||||||
|
this.creator = new CreatorDTO(creator);
|
||||||
|
this.body = post.getBody();
|
||||||
|
this.likes = String.valueOf(post.getLikes());
|
||||||
|
this.comments = String.valueOf(post.getComments());
|
||||||
|
this.liked = liked;
|
||||||
|
this.creationDate = post.getCreationDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreatorDTO getCreator() {
|
||||||
|
return creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreator(CreatorDTO creator) {
|
||||||
|
this.creator = creator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLikes() {
|
||||||
|
return likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLikes(String likes) {
|
||||||
|
this.likes = likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getComments() {
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComments(String comments) {
|
||||||
|
this.comments = comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLiked() {
|
||||||
|
return liked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLiked(boolean liked) {
|
||||||
|
this.liked = liked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreationDate() {
|
||||||
|
return creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreationDate(LocalDateTime creationDate) {
|
||||||
|
this.creationDate = creationDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ public class UpdateUserRequest {
|
|||||||
|
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
@Size(min = 8, message = "Password must be at least 8 characters long")
|
@Size(min = 6, message = "Password must be at least 8 characters long")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
public UpdateUserRequest() {}
|
public UpdateUserRequest() {}
|
||||||
|
|||||||
85
backend/src/main/java/online/wesal/wesal/entity/Post.java
Normal file
85
backend/src/main/java/online/wesal/wesal/entity/Post.java
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "posts")
|
||||||
|
public class Post {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private Long creatorId;
|
||||||
|
|
||||||
|
@Column(nullable = false, length = 2000)
|
||||||
|
@NotBlank
|
||||||
|
private String body;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int likes = 0;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private int comments = 0;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime creationDate = LocalDateTime.now();
|
||||||
|
|
||||||
|
public Post() {}
|
||||||
|
|
||||||
|
public Post(Long creatorId, String body) {
|
||||||
|
this.creatorId = creatorId;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCreatorId() {
|
||||||
|
return creatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatorId(Long creatorId) {
|
||||||
|
this.creatorId = creatorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBody() {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBody(String body) {
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLikes() {
|
||||||
|
return likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLikes(int likes) {
|
||||||
|
this.likes = likes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getComments() {
|
||||||
|
return comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setComments(int comments) {
|
||||||
|
this.comments = comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreationDate() {
|
||||||
|
return creationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreationDate(LocalDateTime creationDate) {
|
||||||
|
this.creationDate = creationDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package online.wesal.wesal.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "post_likes", uniqueConstraints = {
|
||||||
|
@UniqueConstraint(columnNames = {"post_id", "user_id"})
|
||||||
|
})
|
||||||
|
public class PostLike {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, name = "post_id")
|
||||||
|
private Long postId;
|
||||||
|
|
||||||
|
@Column(nullable = false, name = "user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt = LocalDateTime.now();
|
||||||
|
|
||||||
|
public PostLike() {}
|
||||||
|
|
||||||
|
public PostLike(Long postId, Long userId) {
|
||||||
|
this.postId = postId;
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPostId() {
|
||||||
|
return postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPostId(Long postId) {
|
||||||
|
this.postId = postId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatedAt(LocalDateTime createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,7 +23,7 @@ public class User {
|
|||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Size(min = 8)
|
@Size(min = 6)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.PostLike;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PostLikeRepository extends JpaRepository<PostLike, Long> {
|
||||||
|
Optional<PostLike> findByPostIdAndUserId(Long postId, Long userId);
|
||||||
|
boolean existsByPostIdAndUserId(Long postId, Long userId);
|
||||||
|
void deleteByPostIdAndUserId(Long postId, Long userId);
|
||||||
|
long countByPostId(Long postId);
|
||||||
|
|
||||||
|
@Query("SELECT pl.postId FROM PostLike pl WHERE pl.userId = :userId AND pl.postId IN :postIds")
|
||||||
|
Set<Long> findLikedPostIdsByUserIdAndPostIds(@Param("userId") Long userId, @Param("postIds") List<Long> postIds);
|
||||||
|
|
||||||
|
@Query("SELECT pl.postId FROM PostLike pl WHERE pl.userId = :userId")
|
||||||
|
Set<Long> findAllLikedPostIdsByUserId(@Param("userId") Long userId);
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
package online.wesal.wesal.repository;
|
||||||
|
|
||||||
|
import online.wesal.wesal.entity.Post;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||||
|
List<Post> findByCreatorIdOrderByCreationDateDesc(Long creatorId);
|
||||||
|
|
||||||
|
@Query("SELECT p FROM Post p WHERE p.creationDate >= :sevenDaysAgo ORDER BY p.creationDate DESC")
|
||||||
|
List<Post> findAllPostsWithinLast7Days(@Param("sevenDaysAgo") LocalDateTime sevenDaysAgo);
|
||||||
|
|
||||||
|
List<Post> findAllByOrderByCreationDateDesc();
|
||||||
|
}
|
||||||
@ -0,0 +1,158 @@
|
|||||||
|
package online.wesal.wesal.service;
|
||||||
|
|
||||||
|
import online.wesal.wesal.dto.PostResponseDTO;
|
||||||
|
import online.wesal.wesal.entity.Post;
|
||||||
|
import online.wesal.wesal.entity.PostLike;
|
||||||
|
import online.wesal.wesal.entity.User;
|
||||||
|
import online.wesal.wesal.repository.PostLikeRepository;
|
||||||
|
import online.wesal.wesal.repository.PostRepository;
|
||||||
|
import online.wesal.wesal.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PostService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostRepository postRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PostLikeRepository postLikeRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository userRepository;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
public Post createPost(String body) {
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Post post = new Post(currentUser.getId(), body);
|
||||||
|
return postRepository.save(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PostResponseDTO> getAllRecentPosts() {
|
||||||
|
LocalDateTime sevenDaysAgo = LocalDateTime.now().minusDays(7);
|
||||||
|
List<Post> posts = postRepository.findAllPostsWithinLast7Days(sevenDaysAgo);
|
||||||
|
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Long currentUserId = currentUser.getId();
|
||||||
|
|
||||||
|
// Get all unique creator IDs
|
||||||
|
List<Long> creatorIds = posts.stream()
|
||||||
|
.map(Post::getCreatorId)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Get all post IDs
|
||||||
|
List<Long> postIds = posts.stream()
|
||||||
|
.map(Post::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Fetch creators in one query
|
||||||
|
Map<Long, User> creators = userRepository.findAllById(creatorIds).stream()
|
||||||
|
.collect(Collectors.toMap(User::getId, user -> user));
|
||||||
|
|
||||||
|
// Fetch user's likes for these posts in one query
|
||||||
|
Set<Long> likedPostIds = postLikeRepository.findLikedPostIdsByUserIdAndPostIds(currentUserId, postIds);
|
||||||
|
|
||||||
|
return posts.stream()
|
||||||
|
.map(post -> new PostResponseDTO(post, creators.get(post.getCreatorId()),
|
||||||
|
likedPostIds.contains(post.getId())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<PostResponseDTO> getUserPosts(Long userId) {
|
||||||
|
List<Post> posts = postRepository.findByCreatorIdOrderByCreationDateDesc(userId);
|
||||||
|
User creator = userRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
|
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Long currentUserId = currentUser.getId();
|
||||||
|
|
||||||
|
// Get all post IDs
|
||||||
|
List<Long> postIds = posts.stream()
|
||||||
|
.map(Post::getId)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// Fetch user's likes for these posts in one query
|
||||||
|
Set<Long> likedPostIds = postLikeRepository.findLikedPostIdsByUserIdAndPostIds(currentUserId, postIds);
|
||||||
|
|
||||||
|
return posts.stream()
|
||||||
|
.map(post -> new PostResponseDTO(post, creator, likedPostIds.contains(post.getId())))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostResponseDTO createPostWithResponse(String body) {
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Post post = new Post(currentUser.getId(), body);
|
||||||
|
post = postRepository.save(post);
|
||||||
|
return new PostResponseDTO(post, currentUser, false); // New post is not liked by default
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PostResponseDTO likePost(Long postId) {
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Long userId = currentUser.getId();
|
||||||
|
|
||||||
|
// Check if post exists
|
||||||
|
Post post = postRepository.findById(postId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Post not found"));
|
||||||
|
|
||||||
|
// Check if user already liked this post
|
||||||
|
if (!postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
|
||||||
|
// Add like record
|
||||||
|
PostLike postLike = new PostLike(postId, userId);
|
||||||
|
postLikeRepository.save(postLike);
|
||||||
|
|
||||||
|
// Update post likes count
|
||||||
|
post.setLikes(post.getLikes() + 1);
|
||||||
|
post = postRepository.save(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creator for response
|
||||||
|
User creator = userRepository.findById(post.getCreatorId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Creator not found"));
|
||||||
|
|
||||||
|
// Check if user now likes this post
|
||||||
|
boolean isLiked = postLikeRepository.existsByPostIdAndUserId(postId, userId);
|
||||||
|
|
||||||
|
return new PostResponseDTO(post, creator, isLiked);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PostResponseDTO unlikePost(Long postId) {
|
||||||
|
User currentUser = userService.getCurrentUser();
|
||||||
|
Long userId = currentUser.getId();
|
||||||
|
|
||||||
|
// Check if post exists
|
||||||
|
Post post = postRepository.findById(postId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Post not found"));
|
||||||
|
|
||||||
|
// Check if user has liked this post
|
||||||
|
if (postLikeRepository.existsByPostIdAndUserId(postId, userId)) {
|
||||||
|
// Remove like record
|
||||||
|
postLikeRepository.deleteByPostIdAndUserId(postId, userId);
|
||||||
|
|
||||||
|
// Update post likes count
|
||||||
|
post.setLikes(Math.max(0, post.getLikes() - 1));
|
||||||
|
post = postRepository.save(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get creator for response
|
||||||
|
User creator = userRepository.findById(post.getCreatorId())
|
||||||
|
.orElseThrow(() -> new RuntimeException("Creator not found"));
|
||||||
|
|
||||||
|
// Check if user now likes this post
|
||||||
|
boolean isLiked = postLikeRepository.existsByPostIdAndUserId(postId, userId);
|
||||||
|
|
||||||
|
return new PostResponseDTO(post, creator, isLiked);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,3 +19,9 @@ spring:
|
|||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
|
||||||
|
springdoc:
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
|
swagger-ui:
|
||||||
|
path: /docs
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
class ApiConstants {
|
class ApiConstants {
|
||||||
static const String baseUrl = 'https://api.wesal.online';
|
static const String baseUrl = 'http://localhost:8080';
|
||||||
|
|
||||||
// Auth endpoints
|
// Auth endpoints
|
||||||
static const String loginEndpoint = '/login';
|
static const String loginEndpoint = '/login';
|
||||||
@ -13,4 +13,7 @@ class ApiConstants {
|
|||||||
static const String getAllInvitationsEndpoint = '/invitations/all';
|
static const String getAllInvitationsEndpoint = '/invitations/all';
|
||||||
static const String acceptInvitationEndpoint = '/invitations/accept';
|
static const String acceptInvitationEndpoint = '/invitations/accept';
|
||||||
static const String createInvitationEndpoint = '/invitations/create';
|
static const String createInvitationEndpoint = '/invitations/create';
|
||||||
|
|
||||||
|
// Post endpoints
|
||||||
|
static const String createPostEndpoint = '/posts/create';
|
||||||
}
|
}
|
||||||
|
|||||||
105
frontend/lib/models/post_models.dart
Normal file
105
frontend/lib/models/post_models.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
class PostCreator {
|
||||||
|
final String id;
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
|
PostCreator({
|
||||||
|
required this.id,
|
||||||
|
required this.displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PostCreator.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PostCreator(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
displayName: json['displayName'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'displayName': displayName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Post {
|
||||||
|
final String id;
|
||||||
|
final String creatorId;
|
||||||
|
final PostCreator creator;
|
||||||
|
final String body;
|
||||||
|
final int likes;
|
||||||
|
final int comments;
|
||||||
|
final DateTime creationDate;
|
||||||
|
final bool liked;
|
||||||
|
|
||||||
|
Post({
|
||||||
|
required this.id,
|
||||||
|
required this.creatorId,
|
||||||
|
required this.creator,
|
||||||
|
required this.body,
|
||||||
|
required this.likes,
|
||||||
|
required this.comments,
|
||||||
|
required this.creationDate,
|
||||||
|
this.liked = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory Post.fromJson(Map<String, dynamic> json) {
|
||||||
|
return Post(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
creatorId: json['creatorId']?.toString() ?? '',
|
||||||
|
creator: PostCreator.fromJson(json['creator'] ?? {}),
|
||||||
|
body: json['body'] ?? '',
|
||||||
|
likes: int.tryParse(json['likes']?.toString() ?? '0') ?? 0,
|
||||||
|
comments: int.tryParse(json['comments']?.toString() ?? '0') ?? 0,
|
||||||
|
creationDate: DateTime.parse(json['creationDate'] ?? DateTime.now().toIso8601String()),
|
||||||
|
liked: json['liked'] == true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'creatorId': creatorId,
|
||||||
|
'creator': creator.toJson(),
|
||||||
|
'body': body,
|
||||||
|
'likes': likes,
|
||||||
|
'comments': comments,
|
||||||
|
'creationDate': creationDate.toIso8601String(),
|
||||||
|
'liked': liked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Post copyWith({
|
||||||
|
String? id,
|
||||||
|
String? creatorId,
|
||||||
|
PostCreator? creator,
|
||||||
|
String? body,
|
||||||
|
int? likes,
|
||||||
|
int? comments,
|
||||||
|
DateTime? creationDate,
|
||||||
|
bool? liked,
|
||||||
|
}) {
|
||||||
|
return Post(
|
||||||
|
id: id ?? this.id,
|
||||||
|
creatorId: creatorId ?? this.creatorId,
|
||||||
|
creator: creator ?? this.creator,
|
||||||
|
body: body ?? this.body,
|
||||||
|
likes: likes ?? this.likes,
|
||||||
|
comments: comments ?? this.comments,
|
||||||
|
creationDate: creationDate ?? this.creationDate,
|
||||||
|
liked: liked ?? this.liked,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreatePostRequest {
|
||||||
|
final String body;
|
||||||
|
|
||||||
|
CreatePostRequest({required this.body});
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'body': body,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
251
frontend/lib/screens/create_post_screen.dart
Normal file
251
frontend/lib/screens/create_post_screen.dart
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../services/post_service.dart';
|
||||||
|
|
||||||
|
class CreatePostScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
_CreatePostScreenState createState() => _CreatePostScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreatePostScreenState extends State<CreatePostScreen> {
|
||||||
|
final TextEditingController _bodyController = TextEditingController();
|
||||||
|
bool _isLoading = false;
|
||||||
|
final int _maxCharacters = 280;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bodyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _createPost() async {
|
||||||
|
if (_bodyController.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Please write something before posting'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await PostService.createPost(_bodyController.text.trim());
|
||||||
|
|
||||||
|
if (result['success']) {
|
||||||
|
Navigator.of(context).pop(true); // Return true to indicate success
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Post created successfully!'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(result['message']),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to create post. Please try again.'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final remainingChars = _maxCharacters - _bodyController.text.length;
|
||||||
|
final isOverLimit = remainingChars < 0;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Create Post', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
foregroundColor: Color(0xFF6A4C93),
|
||||||
|
elevation: 0,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: Size.fromHeight(1),
|
||||||
|
child: Container(height: 1, color: Colors.grey[200]),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: _isLoading || isOverLimit || _bodyController.text.trim().isEmpty
|
||||||
|
? null
|
||||||
|
: _createPost,
|
||||||
|
child: _isLoading
|
||||||
|
? SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
'Post',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _bodyController.text.trim().isEmpty || isOverLimit
|
||||||
|
? Colors.grey
|
||||||
|
: Color(0xFF6A4C93),
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF32B0A5).withOpacity(0.05),
|
||||||
|
Color(0xFF4600B9).withOpacity(0.05),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"What's on your mind?",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _bodyController,
|
||||||
|
maxLines: 6,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Share your thoughts...',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey[500]),
|
||||||
|
border: InputBorder.none,
|
||||||
|
enabledBorder: InputBorder.none,
|
||||||
|
focusedBorder: InputBorder.none,
|
||||||
|
),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.4,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
onChanged: (text) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Share your thoughts with the community',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$remainingChars',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isOverLimit ? Colors.red : Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isOverLimit)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8),
|
||||||
|
child: Text(
|
||||||
|
'Post is too long. Please keep it under $_maxCharacters characters.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.lightbulb_outline,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Keep it friendly and respectful. Your post will be visible to everyone in the community.',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontSize: 13,
|
||||||
|
height: 1.3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import '../create_post_screen.dart';
|
||||||
|
import '../../services/post_service.dart';
|
||||||
|
import '../../widgets/posts_list.dart';
|
||||||
|
|
||||||
class FeedPage extends StatefulWidget {
|
class FeedPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -6,63 +9,8 @@ class FeedPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _FeedPageState extends State<FeedPage> {
|
class _FeedPageState extends State<FeedPage> {
|
||||||
final List<Map<String, dynamic>> mockPosts = [
|
final GlobalKey<PostsListState> _postsListKey = GlobalKey<PostsListState>();
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'user': {
|
|
||||||
'displayName': 'Abu Khalid (Aqeel)',
|
|
||||||
'avatar': 'A',
|
|
||||||
'avatar_color': Color(0xFF32B0A5),
|
|
||||||
},
|
|
||||||
'content': 'Free hasawi khalas dates! Drop by my office to grab some 😉',
|
|
||||||
'timestamp': '42 minutes ago',
|
|
||||||
'likes': 12,
|
|
||||||
'comments': 3,
|
|
||||||
'isLiked': false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'user': {
|
|
||||||
'displayName': 'Sarah Khalid',
|
|
||||||
'avatar': 'S',
|
|
||||||
'avatar_color': Color(0xFF4600B9),
|
|
||||||
},
|
|
||||||
'content':
|
|
||||||
'Alhamdulillah, I am happy to tell you I have been blessed with a baby ❤️',
|
|
||||||
'timestamp': '4 hours ago',
|
|
||||||
'likes': 28,
|
|
||||||
'comments': 7,
|
|
||||||
'isLiked': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'user': {
|
|
||||||
'displayName': 'Omar Hassan',
|
|
||||||
'avatar': 'O',
|
|
||||||
'avatar_color': Color(0xFF6A4C93),
|
|
||||||
},
|
|
||||||
'content':
|
|
||||||
'The sunset view from my balcony tonight is absolutely breathtaking. Sometimes you just need to pause and appreciate the beauty around us.',
|
|
||||||
'timestamp': '1 day ago',
|
|
||||||
'likes': 45,
|
|
||||||
'comments': 12,
|
|
||||||
'isLiked': false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '4',
|
|
||||||
'user': {
|
|
||||||
'displayName': 'Fatima Al-Zahra',
|
|
||||||
'avatar': 'F',
|
|
||||||
'avatar_color': Color(0xFF32B0A5),
|
|
||||||
},
|
|
||||||
'content':
|
|
||||||
'Finished reading an incredible book today. "The Seven Habits of Highly Effective People" - highly recommend it to anyone looking for personal development!',
|
|
||||||
'timestamp': '2 days ago',
|
|
||||||
'likes': 19,
|
|
||||||
'comments': 5,
|
|
||||||
'isLiked': true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -77,236 +25,40 @@ class _FeedPageState extends State<FeedPage> {
|
|||||||
child: Container(height: 1, color: Colors.grey[200]),
|
child: Container(height: 1, color: Colors.grey[200]),
|
||||||
),
|
),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _postsListKey.currentState?.refreshPosts(),
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
tooltip: 'Refresh',
|
||||||
),
|
),
|
||||||
body: RefreshIndicator(
|
],
|
||||||
onRefresh: () async {
|
|
||||||
await Future.delayed(Duration(seconds: 1));
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Feed refreshed!')));
|
|
||||||
},
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8),
|
|
||||||
itemCount: mockPosts.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
return PostCard(
|
|
||||||
post: mockPosts[index],
|
|
||||||
onLikePressed: () {
|
|
||||||
setState(() {
|
|
||||||
if (mockPosts[index]['isLiked']) {
|
|
||||||
mockPosts[index]['likes']--;
|
|
||||||
mockPosts[index]['isLiked'] = false;
|
|
||||||
} else {
|
|
||||||
mockPosts[index]['likes']++;
|
|
||||||
mockPosts[index]['isLiked'] = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
body: PostsList(
|
||||||
|
key: _postsListKey,
|
||||||
|
fetchPosts: PostService.getAllPosts,
|
||||||
|
emptyStateTitle: 'Nothing here..',
|
||||||
|
emptyStateSubtitle: 'Create the first post!',
|
||||||
|
showRefreshIndicator: true,
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: _navigateToCreatePost,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('Create post functionality coming soon!')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
child: Icon(Icons.edit, color: Colors.white),
|
child: Icon(Icons.edit, color: Colors.white),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class PostCard extends StatefulWidget {
|
|
||||||
final Map<String, dynamic> post;
|
|
||||||
final VoidCallback onLikePressed;
|
|
||||||
|
|
||||||
const PostCard({Key? key, required this.post, required this.onLikePressed})
|
Future<void> _navigateToCreatePost() async {
|
||||||
: super(key: key);
|
final result = await Navigator.push(
|
||||||
|
|
||||||
@override
|
|
||||||
_PostCardState createState() => _PostCardState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PostCardState extends State<PostCard>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late AnimationController _likeAnimationController;
|
|
||||||
late Animation<double> _likeAnimation;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_likeAnimationController = AnimationController(
|
|
||||||
duration: Duration(milliseconds: 300),
|
|
||||||
vsync: this,
|
|
||||||
);
|
|
||||||
_likeAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
|
|
||||||
CurvedAnimation(
|
|
||||||
parent: _likeAnimationController,
|
|
||||||
curve: Curves.elasticOut,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_likeAnimationController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleLike() {
|
|
||||||
widget.onLikePressed();
|
|
||||||
_likeAnimationController.forward().then((_) {
|
|
||||||
_likeAnimationController.reverse();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final user = widget.post['user'];
|
|
||||||
final isLiked = widget.post['isLiked'];
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 20,
|
|
||||||
backgroundColor: user['avatar_color'],
|
|
||||||
child: Text(
|
|
||||||
user['avatar'],
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
user['displayName'],
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 16,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 2),
|
|
||||||
Text(
|
|
||||||
widget.post['timestamp'],
|
|
||||||
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text('More options pressed')),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: Icon(Icons.more_horiz, color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
widget.post['content'],
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
height: 1.4,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
AnimatedBuilder(
|
|
||||||
animation: _likeAnimation,
|
|
||||||
builder: (context, child) {
|
|
||||||
return Transform.scale(
|
|
||||||
scale: _likeAnimation.value,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: _handleLike,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(12),
|
|
||||||
child: Icon(
|
|
||||||
isLiked ? Icons.favorite : Icons.favorite_border,
|
|
||||||
color: isLiked ? Colors.red : Colors.grey[600],
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${widget.post['likes']}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
context,
|
||||||
).showSnackBar(SnackBar(content: Text('Comments pressed')));
|
MaterialPageRoute(builder: (context) => CreatePostScreen()),
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.chat_bubble_outline,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'${widget.post['comments']}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text('Share pressed')));
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
Icons.share_outlined,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If post was created successfully, refresh the feed
|
||||||
|
if (result == true) {
|
||||||
|
_postsListKey.currentState?.refreshPosts();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import 'package:flutter/services.dart';
|
|||||||
import '../../services/notification_service.dart';
|
import '../../services/notification_service.dart';
|
||||||
import '../../services/user_service.dart';
|
import '../../services/user_service.dart';
|
||||||
import '../../services/auth_service.dart';
|
import '../../services/auth_service.dart';
|
||||||
|
import '../../services/post_service.dart';
|
||||||
|
import '../../models/post_models.dart';
|
||||||
|
import '../../widgets/posts_list.dart';
|
||||||
|
import '../../utils/password_generator.dart';
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -15,42 +19,15 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
Map<String, dynamic>? userData;
|
Map<String, dynamic>? userData;
|
||||||
bool isLoadingUser = true;
|
bool isLoadingUser = true;
|
||||||
|
int totalLikes = 0;
|
||||||
final List<Map<String, dynamic>> mockUserPosts = [
|
int totalPosts = 0;
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'content':
|
|
||||||
'Just finished working on a new Flutter project! The development experience keeps getting better.',
|
|
||||||
'timestamp': '2 hours ago',
|
|
||||||
'likes': 15,
|
|
||||||
'comments': 4,
|
|
||||||
'isLiked': true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'content':
|
|
||||||
'Beautiful sunset from my office window today. Sometimes you need to take a moment to appreciate the simple things.',
|
|
||||||
'timestamp': '1 day ago',
|
|
||||||
'likes': 23,
|
|
||||||
'comments': 8,
|
|
||||||
'isLiked': false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '3',
|
|
||||||
'content':
|
|
||||||
'Excited to share that I completed my certification today! Hard work pays off.',
|
|
||||||
'timestamp': '3 days ago',
|
|
||||||
'likes': 42,
|
|
||||||
'comments': 12,
|
|
||||||
'isLiked': true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadFCMToken();
|
_loadFCMToken();
|
||||||
_loadUserData();
|
_loadUserData();
|
||||||
|
_loadUserStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -98,6 +75,21 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserStats() async {
|
||||||
|
final result = await PostService.getUserPosts();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
if (result['success'] == true) {
|
||||||
|
final posts = result['posts'] as List<Post>;
|
||||||
|
totalPosts = posts.length;
|
||||||
|
totalLikes = posts.fold(0, (sum, post) => sum + post.likes);
|
||||||
|
} else {
|
||||||
|
totalPosts = 0;
|
||||||
|
totalLikes = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _showErrorAlert(String message) {
|
void _showErrorAlert(String message) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -223,7 +215,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${mockUserPosts.length}',
|
'$totalPosts',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -242,7 +234,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'127',
|
'$totalLikes',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@ -250,7 +242,7 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Followers',
|
'Likes Received',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
@ -270,126 +262,16 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
margin: EdgeInsets.symmetric(horizontal: 16),
|
margin: EdgeInsets.symmetric(horizontal: 16),
|
||||||
),
|
),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
ListView.builder(
|
ProfilePostsList(
|
||||||
shrinkWrap: true,
|
fetchPosts: PostService.getUserPosts,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
onStatsUpdate: (posts, likes) {
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
setState(() {
|
||||||
itemCount: mockUserPosts.length,
|
totalPosts = posts;
|
||||||
itemBuilder: (context, index) {
|
totalLikes = likes;
|
||||||
final post = mockUserPosts[index];
|
});
|
||||||
return Container(
|
|
||||||
margin: EdgeInsets.only(bottom: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor: Color(0xFF6A4C93),
|
|
||||||
child: Text(
|
|
||||||
(userData!['displayName'] ?? 'U')
|
|
||||||
.substring(0, 1)
|
|
||||||
.toUpperCase(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
userData!['displayName'] ??
|
|
||||||
'Unknown User',
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
post['timestamp'],
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
post['content'],
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
height: 1.4,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 12),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
post['isLiked']
|
|
||||||
? Icons.favorite
|
|
||||||
: Icons.favorite_border,
|
|
||||||
color: post['isLiked']
|
|
||||||
? Colors.red
|
|
||||||
: Colors.grey[600],
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${post['likes']}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 16),
|
|
||||||
Icon(
|
|
||||||
Icons.chat_bubble_outline,
|
|
||||||
color: Colors.grey[600],
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${post['comments']}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
SizedBox(height: 24),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -407,15 +289,28 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
final TextEditingController _tokenController = TextEditingController();
|
final TextEditingController _tokenController = TextEditingController();
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
|
|
||||||
|
// Admin user creation
|
||||||
|
Map<String, dynamic>? userData;
|
||||||
|
bool isLoadingUser = true;
|
||||||
|
bool isCreatingUser = false;
|
||||||
|
final TextEditingController _emailController = TextEditingController();
|
||||||
|
final TextEditingController _passwordController = TextEditingController();
|
||||||
|
final TextEditingController _displayNameController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadFCMToken();
|
_loadFCMToken();
|
||||||
|
_loadUserData();
|
||||||
|
_generatePassword();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_tokenController.dispose();
|
_tokenController.dispose();
|
||||||
|
_emailController.dispose();
|
||||||
|
_passwordController.dispose();
|
||||||
|
_displayNameController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,6 +347,68 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadUserData() async {
|
||||||
|
setState(() {
|
||||||
|
isLoadingUser = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
final result = await UserService.getCurrentUser();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isLoadingUser = false;
|
||||||
|
if (result['success'] == true) {
|
||||||
|
userData = result['data'];
|
||||||
|
} else {
|
||||||
|
userData = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _generatePassword() {
|
||||||
|
_passwordController.text = PasswordGenerator.generateReadablePassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isAdmin {
|
||||||
|
return userData?['role'] == 'ADMIN';
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> _createUserAccount() async {
|
||||||
|
if (_emailController.text.trim().isEmpty ||
|
||||||
|
_displayNameController.text.trim().isEmpty) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Please fill in all fields'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return {'success': false, 'message': 'Please fill in all fields'};
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isCreatingUser = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await UserService.createUser(
|
||||||
|
email: _emailController.text.trim(),
|
||||||
|
password: _passwordController.text,
|
||||||
|
displayName: _displayNameController.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
isCreatingUser = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
isCreatingUser = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {'success': false, 'message': 'Error creating user: $e'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _signOut() async {
|
void _signOut() async {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -482,6 +439,193 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showCredentialsDialog(String email, String password) {
|
||||||
|
final credentialsText = 'Email: $email\nPassword: $password';
|
||||||
|
final TextEditingController credentialsController = TextEditingController(
|
||||||
|
text: credentialsText,
|
||||||
|
);
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Account Created Successfully!'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Share these credentials with the user:',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: credentialsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(Icons.copy),
|
||||||
|
onPressed: () async {
|
||||||
|
await Clipboard.setData(
|
||||||
|
ClipboardData(text: credentialsText),
|
||||||
|
);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Credentials copied to clipboard'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
tooltip: 'Copy credentials',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
maxLines: 2,
|
||||||
|
readOnly: true,
|
||||||
|
style: TextStyle(fontFamily: 'monospace', fontSize: 14),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text('Done'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showCreateAccountDialog(BuildContext context) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext dialogContext) {
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setDialogState) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Create Account'),
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
controller: _emailController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Email',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.email),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: _displayNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Display Name',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.person),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
TextField(
|
||||||
|
controller: _passwordController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Password',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.lock),
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(Icons.refresh),
|
||||||
|
onPressed: () {
|
||||||
|
setDialogState(() {
|
||||||
|
_generatePassword();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
tooltip: 'Generate new password',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
readOnly: true,
|
||||||
|
style: TextStyle(
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Password is auto-generated for security',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||||
|
child: Text('Cancel'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: isCreatingUser
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final email = _emailController.text.trim();
|
||||||
|
final password = _passwordController.text;
|
||||||
|
|
||||||
|
final result = await _createUserAccount();
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(dialogContext).pop();
|
||||||
|
|
||||||
|
if (result['success']) {
|
||||||
|
_showCredentialsDialog(email, password);
|
||||||
|
// Clear form
|
||||||
|
_emailController.clear();
|
||||||
|
_displayNameController.clear();
|
||||||
|
_generatePassword();
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
result['message'] ??
|
||||||
|
'Failed to create user',
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: isCreatingUser
|
||||||
|
? SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text('Create'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -501,6 +645,37 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
if (!isLoadingUser && _isAdmin) ...[
|
||||||
|
Text(
|
||||||
|
'Admin Tools',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _showCreateAccountDialog(context),
|
||||||
|
icon: Icon(Icons.person_add, size: 18),
|
||||||
|
label: Text('Create an Account'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
SizedBox(height: 32),
|
||||||
|
],
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
'Development Tools',
|
'Development Tools',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
158
frontend/lib/services/post_service.dart
Normal file
158
frontend/lib/services/post_service.dart
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import '../models/post_models.dart';
|
||||||
|
import 'http_service.dart';
|
||||||
|
|
||||||
|
class PostService {
|
||||||
|
static Future<Map<String, dynamic>> getAllPosts() async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.get('/posts/all');
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'posts':
|
||||||
|
(responseData['data'] as List?)
|
||||||
|
?.map((post) => Post.fromJson(post))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to fetch posts',
|
||||||
|
'posts': <Post>[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching posts: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error: $e',
|
||||||
|
'posts': <Post>[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> getUserPosts() async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.get('/posts/user');
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'posts': (responseData['data'] as List?)?.map((post) => Post.fromJson(post)).toList() ?? [],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to fetch user posts',
|
||||||
|
'posts': <Post>[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching user posts: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error: $e',
|
||||||
|
'posts': <Post>[],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> createPost(String body) async {
|
||||||
|
try {
|
||||||
|
final createPostRequest = CreatePostRequest(body: body);
|
||||||
|
final response = await HttpService.post(
|
||||||
|
'/posts/create',
|
||||||
|
createPostRequest.toJson(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'post': responseData['data'] != null
|
||||||
|
? Post.fromJson(responseData['data'])
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to create post',
|
||||||
|
'post': null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating post: $e');
|
||||||
|
return {'success': false, 'message': 'Network error: $e', 'post': null};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> likePost(String postId) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(
|
||||||
|
'/posts/like',
|
||||||
|
{'postId': postId},
|
||||||
|
);
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'post': responseData['data'] != null
|
||||||
|
? Post.fromJson(responseData['data'])
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to like post',
|
||||||
|
'post': null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error liking post: $e');
|
||||||
|
return {'success': false, 'message': 'Network error: $e', 'post': null};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> unlikePost(String postId) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post(
|
||||||
|
'/posts/unlike',
|
||||||
|
{'postId': postId},
|
||||||
|
);
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? '',
|
||||||
|
'post': responseData['data'] != null
|
||||||
|
? Post.fromJson(responseData['data'])
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to unlike post',
|
||||||
|
'post': null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error unliking post: $e');
|
||||||
|
return {'success': false, 'message': 'Network error: $e', 'post': null};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -78,4 +78,39 @@ class UserService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, dynamic>> createUser({
|
||||||
|
required String email,
|
||||||
|
required String password,
|
||||||
|
required String displayName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await HttpService.post('/admin/createUser', {
|
||||||
|
'email': email,
|
||||||
|
'password': password,
|
||||||
|
'displayName': displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
final responseData = jsonDecode(response.body);
|
||||||
|
|
||||||
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
|
return {
|
||||||
|
'success': responseData['status'] ?? false,
|
||||||
|
'message': responseData['message'] ?? 'User created successfully',
|
||||||
|
'data': responseData['data'],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': responseData['message'] ?? 'Failed to create user',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error creating user: $e');
|
||||||
|
return {
|
||||||
|
'success': false,
|
||||||
|
'message': 'Network error. Please check your connection.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
frontend/lib/utils/password_generator.dart
Normal file
16
frontend/lib/utils/password_generator.dart
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
class PasswordGenerator {
|
||||||
|
static const String _readableChars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||||
|
static final Random _random = Random();
|
||||||
|
|
||||||
|
static String generateReadablePassword({int length = 6}) {
|
||||||
|
StringBuffer password = StringBuffer();
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
password.write(_readableChars[_random.nextInt(_readableChars.length)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
577
frontend/lib/widgets/posts_list.dart
Normal file
577
frontend/lib/widgets/posts_list.dart
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../models/post_models.dart';
|
||||||
|
import '../services/post_service.dart';
|
||||||
|
import '../utils/invitation_utils.dart';
|
||||||
|
|
||||||
|
class PostsList extends StatefulWidget {
|
||||||
|
final Future<Map<String, dynamic>> Function() fetchPosts;
|
||||||
|
final String emptyStateTitle;
|
||||||
|
final String emptyStateSubtitle;
|
||||||
|
final bool showRefreshIndicator;
|
||||||
|
final Widget? floatingActionButton;
|
||||||
|
|
||||||
|
const PostsList({
|
||||||
|
Key? key,
|
||||||
|
required this.fetchPosts,
|
||||||
|
this.emptyStateTitle = 'Nothing here..',
|
||||||
|
this.emptyStateSubtitle = 'Create the first post!',
|
||||||
|
this.showRefreshIndicator = true,
|
||||||
|
this.floatingActionButton,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
PostsListState createState() => PostsListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostsListState extends State<PostsList> {
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<Post> _posts = [];
|
||||||
|
String _errorMessage = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPosts() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await widget.fetchPosts();
|
||||||
|
setState(() {
|
||||||
|
if (result['success']) {
|
||||||
|
_posts = result['posts'];
|
||||||
|
} else {
|
||||||
|
_errorMessage = result['message'] ?? 'Failed to load posts';
|
||||||
|
}
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Network error: $e';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshPosts() async {
|
||||||
|
await _loadPosts();
|
||||||
|
|
||||||
|
if (widget.showRefreshIndicator) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Posts refreshed!'),
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget body = _buildBody();
|
||||||
|
|
||||||
|
if (widget.showRefreshIndicator) {
|
||||||
|
body = RefreshIndicator(
|
||||||
|
onRefresh: _refreshPosts,
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBody() {
|
||||||
|
if (_isLoading) {
|
||||||
|
return Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage.isNotEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _loadPosts,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text('Try Again'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_posts.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.post_add,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
widget.emptyStateTitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
widget.emptyStateSubtitle,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: _posts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return PostCard(
|
||||||
|
post: _posts[index],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshPosts() {
|
||||||
|
_loadPosts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostCard extends StatefulWidget {
|
||||||
|
final Post post;
|
||||||
|
|
||||||
|
const PostCard({Key? key, required this.post})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_PostCardState createState() => _PostCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PostCardState extends State<PostCard> {
|
||||||
|
late Post _currentPost;
|
||||||
|
bool _isLiking = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentPost = widget.post;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PostCard oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
// Update current post if the widget's post changed
|
||||||
|
if (oldWidget.post.id != widget.post.id) {
|
||||||
|
_currentPost = widget.post;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _getAvatarColor(String displayName) {
|
||||||
|
final colors = [
|
||||||
|
Color(0xFF32B0A5),
|
||||||
|
Color(0xFF4600B9),
|
||||||
|
Color(0xFF6A4C93),
|
||||||
|
Color(0xFFFF6347),
|
||||||
|
Color(0xFF32CD32),
|
||||||
|
Color(0xFF9932CC),
|
||||||
|
];
|
||||||
|
|
||||||
|
int hash = displayName.hashCode;
|
||||||
|
return colors[hash.abs() % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getAvatarLetter(String displayName) {
|
||||||
|
return displayName.isNotEmpty ? displayName[0].toUpperCase() : '?';
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sharePost(Post post) {
|
||||||
|
final shareText = '${post.creator.displayName} posted on Wesal.online:\n\n${post.body}';
|
||||||
|
Share.share(shareText);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleLike() async {
|
||||||
|
if (_isLiking) return; // Prevent multiple simultaneous requests
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLiking = true;
|
||||||
|
// Optimistic update - immediately change UI
|
||||||
|
_currentPost = _currentPost.copyWith(
|
||||||
|
liked: !_currentPost.liked,
|
||||||
|
likes: _currentPost.liked ? _currentPost.likes - 1 : _currentPost.likes + 1,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, dynamic> result;
|
||||||
|
if (widget.post.liked) {
|
||||||
|
result = await PostService.unlikePost(widget.post.id);
|
||||||
|
} else {
|
||||||
|
result = await PostService.likePost(widget.post.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result['success'] && result['post'] != null) {
|
||||||
|
// Update with server response
|
||||||
|
setState(() {
|
||||||
|
_currentPost = result['post'];
|
||||||
|
_isLiking = false;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Revert optimistic update on failure
|
||||||
|
setState(() {
|
||||||
|
_currentPost = widget.post;
|
||||||
|
_isLiking = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(result['message'] ?? 'Failed to update like'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Revert optimistic update on error
|
||||||
|
setState(() {
|
||||||
|
_currentPost = widget.post;
|
||||||
|
_isLiking = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Network error occurred'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final creator = _currentPost.creator;
|
||||||
|
final avatarColor = _getAvatarColor(creator.displayName);
|
||||||
|
final avatarLetter = _getAvatarLetter(creator.displayName);
|
||||||
|
final relativeTime = InvitationUtils.getRelativeTime(_currentPost.creationDate);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 20,
|
||||||
|
backgroundColor: avatarColor,
|
||||||
|
child: Text(
|
||||||
|
avatarLetter,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
creator.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
relativeTime,
|
||||||
|
style: TextStyle(color: Colors.grey[600], fontSize: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('More options pressed')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(Icons.more_horiz, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
_currentPost.body,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
height: 1.4,
|
||||||
|
color: Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _toggleLike,
|
||||||
|
icon: Icon(
|
||||||
|
_currentPost.liked ? Icons.favorite : Icons.favorite_border,
|
||||||
|
color: _currentPost.liked ? Colors.red : Colors.grey[600],
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_currentPost.likes}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 16),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Comments feature coming soon!')),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.chat_bubble_outline,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${_currentPost.comments}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => _sharePost(_currentPost),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.share_outlined,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfilePostsList extends StatefulWidget {
|
||||||
|
final Future<Map<String, dynamic>> Function() fetchPosts;
|
||||||
|
final Function(int posts, int likes)? onStatsUpdate;
|
||||||
|
|
||||||
|
const ProfilePostsList({
|
||||||
|
Key? key,
|
||||||
|
required this.fetchPosts,
|
||||||
|
this.onStatsUpdate,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ProfilePostsListState createState() => _ProfilePostsListState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ProfilePostsListState extends State<ProfilePostsList> {
|
||||||
|
bool _isLoading = true;
|
||||||
|
List<Post> _posts = [];
|
||||||
|
String _errorMessage = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadPosts() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await widget.fetchPosts();
|
||||||
|
setState(() {
|
||||||
|
if (result['success']) {
|
||||||
|
_posts = result['posts'];
|
||||||
|
// Update parent stats
|
||||||
|
if (widget.onStatsUpdate != null) {
|
||||||
|
final totalLikes = _posts.fold(0, (sum, post) => sum + post.likes);
|
||||||
|
widget.onStatsUpdate!(_posts.length, totalLikes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_errorMessage = result['message'] ?? 'Failed to load posts';
|
||||||
|
}
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'Network error: $e';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage.isNotEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _loadPosts,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Color(0xFF6A4C93),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text('Try Again'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_posts.isEmpty) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.post_add,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'No posts yet',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Start sharing your thoughts!',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.grey[500],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
..._posts.map((post) =>
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(bottom: 16, left: 16, right: 16),
|
||||||
|
child: PostCard(post: post),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,9 +7,13 @@
|
|||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
@ -9,10 +9,12 @@ import firebase_core
|
|||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import share_plus
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
cross_file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: cross_file
|
||||||
|
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.4+2"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -89,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
file:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: file
|
||||||
|
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.1"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -137,6 +153,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.10.9"
|
version: "3.10.9"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -304,6 +328,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.16.0"
|
||||||
|
mime:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: mime
|
||||||
|
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.0"
|
||||||
path:
|
path:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -376,6 +408,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.0"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -389,6 +437,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
|
sprintf:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: sprintf
|
||||||
|
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "7.0.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -437,6 +493,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
url_launcher_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_linux
|
||||||
|
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.1"
|
||||||
|
url_launcher_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_platform_interface
|
||||||
|
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.2"
|
||||||
|
url_launcher_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_web
|
||||||
|
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
url_launcher_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: url_launcher_windows
|
||||||
|
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.4"
|
||||||
|
uuid:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: uuid
|
||||||
|
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.1"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -15,6 +15,7 @@ dependencies:
|
|||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
googleapis_auth: ^1.6.0
|
googleapis_auth: ^1.6.0
|
||||||
flutter_secure_storage: ^9.2.2
|
flutter_secure_storage: ^9.2.2
|
||||||
|
share_plus: ^11.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -8,10 +8,16 @@
|
|||||||
|
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
firebase_core
|
firebase_core
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
share_plus
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user