generated from Gallardo7761/miarma-template-full
Compare commits
7 Commits
main
...
fdc3120aa7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc3120aa7 | ||
|
|
af65e3b51e | ||
|
|
5ef4d0f2e0 | ||
|
|
dcc1d55db6 | ||
|
|
95dd13595e | ||
|
|
aec029b670 | ||
|
|
859d5b88bc |
19
.gitignore
vendored
19
.gitignore
vendored
@@ -1,4 +1,15 @@
|
||||
.env
|
||||
node_modules/
|
||||
dist/
|
||||
package-lock.json
|
||||
frontend/.env
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/package-lock.json
|
||||
|
||||
backend/target/
|
||||
backend/.mvn/wrapper/maven-wrapper.jar
|
||||
backend/!**/src/main/**/target/
|
||||
backend/!**/src/test/**/target/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
backend/.idea
|
||||
backend/*.iws
|
||||
backend/*.iml
|
||||
backend/*.ipr
|
||||
|
||||
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
3
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
95
backend/pom.xml
Normal file
95
backend/pom.xml
Normal file
@@ -0,0 +1,95 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>4.0.2</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>es.adeptusminiaturium</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
|
||||
<version>1.0.0</version>
|
||||
<name>backend</name>
|
||||
<description>Adeptus Miniaturium's online site</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>25</maven.compiler.source>
|
||||
<maven.compiler.target>25</maven.compiler.target>
|
||||
<java.version>25</java.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>gitea</id>
|
||||
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mariadb.jdbc</groupId>
|
||||
<artifactId>mariadb-java-client</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.miarma</groupId>
|
||||
<artifactId>backlib</artifactId>
|
||||
<version>1.1.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,12 @@
|
||||
package es.adeptusminiaturium.backend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class BackendApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(BackendApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package es.adeptusminiaturium.backend.config;
|
||||
|
||||
import es.adeptusminiaturium.backend.service.CustomUserDetailsService;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
|
||||
public SecurityConfig(CustomUserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
//.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
//.requestMatchers("/posts/**").authenticated()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.formLogin(Customizer.withDefaults())
|
||||
.httpBasic(Customizer.withDefaults());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authManager(HttpSecurity http) {
|
||||
AuthenticationManagerBuilder authBuilder =
|
||||
http.getSharedObject(AuthenticationManagerBuilder.class);
|
||||
|
||||
authBuilder.userDetailsService(userDetailsService)
|
||||
.passwordEncoder(passwordEncoder());
|
||||
|
||||
return authBuilder.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package es.adeptusminiaturium.backend.controller;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||
import es.adeptusminiaturium.backend.service.MediaService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/media")
|
||||
public class MediaController {
|
||||
|
||||
private final MediaService service;
|
||||
|
||||
public MediaController(MediaService service) { this.service = service; }
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<MediaDto.Response>> getAll() {
|
||||
return ResponseEntity.ok(service.getAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<MediaDto.Response> getOne(@PathVariable Long id) {
|
||||
return ResponseEntity.ofNullable(service.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<MediaDto.Response> create(@RequestBody MediaDto.Request dto) {
|
||||
return ResponseEntity.ok(service.create(dto));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<MediaDto.Response> update(@PathVariable Long id, @RequestBody MediaDto.Request dto) {
|
||||
return ResponseEntity.ofNullable(service.update(id, dto));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
service.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package es.adeptusminiaturium.backend.controller;
|
||||
|
||||
import es.adeptusminiaturium.backend.service.PostService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/posts")
|
||||
public class PostController {
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
public PostController(PostService postService) {
|
||||
this.postService = postService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PostDto.Response>> getAll() {
|
||||
return ResponseEntity.ok(postService.getAllPosts());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PostDto.Response> getById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(postService.getPostById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PostDto.Response> create(@RequestBody PostDto.Request dto) {
|
||||
return ResponseEntity.ok(postService.createPost(dto));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package es.adeptusminiaturium.backend.controller;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||
import es.adeptusminiaturium.backend.service.PublicationService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/publications")
|
||||
public class PublicationController {
|
||||
|
||||
private final PublicationService service;
|
||||
|
||||
public PublicationController(PublicationService service) { this.service = service; }
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<PublicationDto.Response>> getAll() {
|
||||
return ResponseEntity.ok(service.getAll());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PublicationDto.Response> getOne(@PathVariable Long id) {
|
||||
return ResponseEntity.ofNullable(service.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PublicationDto.Response> create(@RequestBody PublicationDto.Request dto) {
|
||||
return ResponseEntity.ok(service.create(dto));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<PublicationDto.Response> update(@PathVariable Long id, @RequestBody PublicationDto.Request dto) {
|
||||
return ResponseEntity.ofNullable(service.update(id, dto));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable Long id) {
|
||||
service.delete(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package es.adeptusminiaturium.backend.controller;
|
||||
|
||||
import es.adeptusminiaturium.backend.service.UserService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService service;
|
||||
|
||||
public UserController(UserService service) { this.service = service; }
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<UserDto.Response>> getAll() {
|
||||
return ResponseEntity.ok(service.getAllUsers());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserDto.Response> getOne(@PathVariable UUID id) {
|
||||
return ResponseEntity.ofNullable(service.getUser(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserDto.Response> create(@RequestBody UserDto.Request dto) {
|
||||
return ResponseEntity.ok(service.createUser(dto));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserDto.Response> update(@PathVariable UUID id, @RequestBody UserDto.Request dto) {
|
||||
return ResponseEntity.ofNullable(service.updateUser(id, dto));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> delete(@PathVariable UUID id) {
|
||||
service.deleteUser(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package es.adeptusminiaturium.backend.dto;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.MediaType;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class MediaDto {
|
||||
|
||||
public static class Request {
|
||||
private Long postId;
|
||||
private MediaType mediaType;
|
||||
private String url;
|
||||
private Integer position;
|
||||
|
||||
public Long getPostId() { return postId; }
|
||||
public void setPostId(Long postId) { this.postId = postId; }
|
||||
public MediaType getMediaType() { return mediaType; }
|
||||
public void setMediaType(MediaType mediaType) { this.mediaType = mediaType; }
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
public Integer getPosition() { return position; }
|
||||
public void setPosition(Integer position) { this.position = position; }
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
private Long mediaId;
|
||||
private PostDto.Response post;
|
||||
private MediaType mediaType;
|
||||
private String url;
|
||||
private Integer position;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Long getMediaId() { return mediaId; }
|
||||
public void setMediaId(Long mediaId) { this.mediaId = mediaId; }
|
||||
public PostDto.Response getPost() { return post; }
|
||||
public void setPost(PostDto.Response post) { this.post = post; }
|
||||
public MediaType getMediaType() { return mediaType; }
|
||||
public void setMediaType(MediaType mediaType) { this.mediaType = mediaType; }
|
||||
public String getUrl() { return url; }
|
||||
public void setUrl(String url) { this.url = url; }
|
||||
public Integer getPosition() { return position; }
|
||||
public void setPosition(Integer position) { this.position = position; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package es.adeptusminiaturium.backend.dto;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.PostStatus;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PostDto {
|
||||
|
||||
public static class Request {
|
||||
private UUID authorId;
|
||||
private String title;
|
||||
private String body;
|
||||
private String hashtags;
|
||||
private PostStatus status;
|
||||
|
||||
public UUID getAuthorId() { return authorId; }
|
||||
public void setAuthorId(UUID authorId) { this.authorId = authorId; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getBody() { return body; }
|
||||
public void setBody(String body) { this.body = body; }
|
||||
public String getHashtags() { return hashtags; }
|
||||
public void setHashtags(String hashtags) { this.hashtags = hashtags; }
|
||||
public PostStatus getStatus() { return status; }
|
||||
public void setStatus(PostStatus status) { this.status = status; }
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
private Long postId;
|
||||
private UserDto.Response author;
|
||||
private String title;
|
||||
private String body;
|
||||
private String hashtags;
|
||||
private PostStatus status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private LocalDateTime publishedAt;
|
||||
|
||||
public Long getPostId() { return postId; }
|
||||
public void setPostId(Long postId) { this.postId = postId; }
|
||||
public UserDto.Response getAuthor() { return author; }
|
||||
public void setAuthor(UserDto.Response author) { this.author = author; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
public String getBody() { return body; }
|
||||
public void setBody(String body) { this.body = body; }
|
||||
public String getHashtags() { return hashtags; }
|
||||
public void setHashtags(String hashtags) { this.hashtags = hashtags; }
|
||||
public PostStatus getStatus() { return status; }
|
||||
public void setStatus(PostStatus status) { this.status = status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public LocalDateTime getPublishedAt() { return publishedAt; }
|
||||
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package es.adeptusminiaturium.backend.dto;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.Platform;
|
||||
import es.adeptusminiaturium.backend.enums.PublicationStatus;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PublicationDto {
|
||||
|
||||
public static class Request {
|
||||
private Long postId;
|
||||
private Platform platform;
|
||||
private String externalId;
|
||||
private PublicationStatus status;
|
||||
|
||||
// getters y setters
|
||||
public Long getPostId() { return postId; }
|
||||
public void setPostId(Long postId) { this.postId = postId; }
|
||||
public Platform getPlatform() { return platform; }
|
||||
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||
public String getExternalId() { return externalId; }
|
||||
public void setExternalId(String externalId) { this.externalId = externalId; }
|
||||
public PublicationStatus getStatus() { return status; }
|
||||
public void setStatus(PublicationStatus status) { this.status = status; }
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
private Long publicationId;
|
||||
private PostDto.Response post;
|
||||
private Platform platform;
|
||||
private String externalId;
|
||||
private PublicationStatus status;
|
||||
private LocalDateTime publishedAt;
|
||||
private String errorMessage;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
// getters y setters
|
||||
public Long getPublicationId() { return publicationId; }
|
||||
public void setPublicationId(Long publicationId) { this.publicationId = publicationId; }
|
||||
public PostDto.Response getPost() { return post; }
|
||||
public void setPost(PostDto.Response post) { this.post = post; }
|
||||
public Platform getPlatform() { return platform; }
|
||||
public void setPlatform(Platform platform) { this.platform = platform; }
|
||||
public String getExternalId() { return externalId; }
|
||||
public void setExternalId(String externalId) { this.externalId = externalId; }
|
||||
public PublicationStatus getStatus() { return status; }
|
||||
public void setStatus(PublicationStatus status) { this.status = status; }
|
||||
public LocalDateTime getPublishedAt() { return publishedAt; }
|
||||
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
|
||||
public String getErrorMessage() { return errorMessage; }
|
||||
public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package es.adeptusminiaturium.backend.dto;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UserDto {
|
||||
|
||||
public static class Request {
|
||||
private String displayName;
|
||||
private String userName;
|
||||
private String password;
|
||||
private String avatar;
|
||||
private UserRole role;
|
||||
private UserStatus status;
|
||||
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getUserName() { return userName; }
|
||||
public void setUserName(String userName) { this.userName = userName; }
|
||||
public String getPassword() { return password; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
public UserRole getRole() { return role; }
|
||||
public void setRole(UserRole role) { this.role = role; }
|
||||
public UserStatus getStatus() { return status; }
|
||||
public void setStatus(UserStatus status) { this.status = status; }
|
||||
}
|
||||
|
||||
public static class Response {
|
||||
private UUID userId;
|
||||
private String displayName;
|
||||
private String userName;
|
||||
private String avatar;
|
||||
private UserRole role;
|
||||
private UserStatus status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
public String getUserName() { return userName; }
|
||||
public void setUserName(String userName) { this.userName = userName; }
|
||||
public String getAvatar() { return avatar; }
|
||||
public void setAvatar(String avatar) { this.avatar = avatar; }
|
||||
public UserRole getRole() { return role; }
|
||||
public void setRole(UserRole role) { this.role = role; }
|
||||
public UserStatus getStatus() { return status; }
|
||||
public void setStatus(UserStatus status) { this.status = status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
public enum MediaType {
|
||||
IMAGE, // 0
|
||||
VIDEO // 1
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public enum Platform {
|
||||
WEB, // 0
|
||||
INSTAGRAM, // 1
|
||||
TIKTOK, // 2
|
||||
TWITTER; // 3
|
||||
|
||||
public static List<Platform> valuesList() {
|
||||
return List.of(values());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
public enum PostStatus {
|
||||
DRAFT, // 0
|
||||
PUBLISHED, // 1
|
||||
ARCHIVED // 2
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
public enum PublicationStatus {
|
||||
PENDING, // 0
|
||||
PUBLISHED, // 1
|
||||
FAILED // 2
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
public enum UserRole {
|
||||
USER, // 0
|
||||
ADMIN // 1
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package es.adeptusminiaturium.backend.enums;
|
||||
|
||||
public enum UserStatus {
|
||||
INACTIVE, // 0
|
||||
ACTIVE // 1
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package es.adeptusminiaturium.backend.mapper;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||
import es.adeptusminiaturium.backend.model.Media;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class MediaMapper {
|
||||
|
||||
public static MediaDto.Response toResponse(Media entity) {
|
||||
MediaDto.Response dto = new MediaDto.Response();
|
||||
dto.setMediaId(entity.getMediaId());
|
||||
dto.setPost(PostMapper.toResponse(entity.getPost()));
|
||||
dto.setMediaType(entity.getMediaType());
|
||||
dto.setUrl(entity.getUrl());
|
||||
dto.setPosition(entity.getPosition());
|
||||
dto.setCreatedAt(entity.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static Media toEntity(MediaDto.Request dto) {
|
||||
Media entity = new Media();
|
||||
entity.setMediaId(null); // autoincrement
|
||||
entity.setPost(null); // se setea después con repo.findById(dto.getPostId())
|
||||
entity.setMediaType(dto.getMediaType());
|
||||
entity.setUrl(dto.getUrl());
|
||||
entity.setPosition(dto.getPosition());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package es.adeptusminiaturium.backend.mapper;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||
import es.adeptusminiaturium.backend.model.Post;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PostMapper {
|
||||
|
||||
public static PostDto.Response toResponse(Post entity) {
|
||||
PostDto.Response dto = new PostDto.Response();
|
||||
dto.setPostId(entity.getPostId());
|
||||
dto.setAuthor(UserMapper.toResponse(entity.getAuthor()));
|
||||
dto.setTitle(entity.getTitle());
|
||||
dto.setBody(entity.getBody());
|
||||
dto.setHashtags(entity.getHashtags());
|
||||
dto.setStatus(entity.getStatus());
|
||||
dto.setCreatedAt(entity.getCreatedAt());
|
||||
dto.setUpdatedAt(entity.getUpdatedAt());
|
||||
dto.setPublishedAt(entity.getPublishedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static Post toEntity(PostDto.Request dto) {
|
||||
Post entity = new Post();
|
||||
entity.setPostId(null);
|
||||
entity.setAuthor(null);
|
||||
entity.setTitle(dto.getTitle());
|
||||
entity.setBody(dto.getBody());
|
||||
entity.setHashtags(dto.getHashtags());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
entity.setPublishedAt(null);
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package es.adeptusminiaturium.backend.mapper;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||
import es.adeptusminiaturium.backend.model.Publication;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public class PublicationMapper {
|
||||
|
||||
public static PublicationDto.Response toResponse(Publication entity) {
|
||||
PublicationDto.Response dto = new PublicationDto.Response();
|
||||
dto.setPublicationId(entity.getPublicationId());
|
||||
dto.setPost(PostMapper.toResponse(entity.getPost()));
|
||||
dto.setPlatform(entity.getPlatform());
|
||||
dto.setExternalId(entity.getExternalId());
|
||||
dto.setStatus(entity.getStatus());
|
||||
dto.setPublishedAt(entity.getPublishedAt());
|
||||
dto.setErrorMessage(entity.getErrorMessage());
|
||||
dto.setCreatedAt(entity.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static Publication toEntity(PublicationDto.Request dto) {
|
||||
Publication entity = new Publication();
|
||||
entity.setPublicationId(null);
|
||||
entity.setPost(null);
|
||||
entity.setPlatform(dto.getPlatform());
|
||||
entity.setExternalId(dto.getExternalId());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setPublishedAt(null);
|
||||
entity.setErrorMessage(null);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package es.adeptusminiaturium.backend.mapper;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||
import es.adeptusminiaturium.backend.model.User;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
public class UserMapper {
|
||||
|
||||
public static UserDto.Response toResponse(User entity) {
|
||||
UserDto.Response dto = new UserDto.Response();
|
||||
dto.setUserId(entity.getUserId());
|
||||
dto.setDisplayName(entity.getDisplayName());
|
||||
dto.setUserName(entity.getUserName());
|
||||
dto.setAvatar(entity.getAvatar());
|
||||
dto.setRole(entity.getRole());
|
||||
dto.setStatus(entity.getStatus());
|
||||
dto.setCreatedAt(entity.getCreatedAt());
|
||||
dto.setUpdatedAt(entity.getUpdatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
public static User toEntity(UserDto.Request dto) {
|
||||
User entity = new User();
|
||||
entity.setUserId(UUID.randomUUID());
|
||||
entity.setDisplayName(dto.getDisplayName());
|
||||
entity.setUserName(dto.getUserName());
|
||||
entity.setPassword(dto.getPassword());
|
||||
entity.setAvatar(dto.getAvatar());
|
||||
entity.setRole(dto.getRole());
|
||||
entity.setStatus(dto.getStatus());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package es.adeptusminiaturium.backend.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||
import es.adeptusminiaturium.backend.enums.MediaType;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "media")
|
||||
public class Media {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "media_id")
|
||||
private Long mediaId;
|
||||
|
||||
@JsonManagedReference
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "post_id", nullable = false)
|
||||
private Post post;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(name = "media_type", nullable = false)
|
||||
private MediaType mediaType = MediaType.IMAGE;
|
||||
|
||||
@Column(nullable = false, length = 512)
|
||||
private String url;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Integer position = 0;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", insertable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Long getMediaId() {
|
||||
return mediaId;
|
||||
}
|
||||
|
||||
public void setMediaId(Long mediaId) {
|
||||
this.mediaId = mediaId;
|
||||
}
|
||||
|
||||
public Post getPost() {
|
||||
return post;
|
||||
}
|
||||
|
||||
public void setPost(Post post) {
|
||||
this.post = post;
|
||||
}
|
||||
|
||||
public MediaType getMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
public void setMediaType(MediaType mediaType) {
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public Integer getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(Integer position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package es.adeptusminiaturium.backend.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||
import es.adeptusminiaturium.backend.enums.PostStatus;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Table(name = "posts")
|
||||
public class Post {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "post_id")
|
||||
private Long postId;
|
||||
|
||||
@JsonManagedReference
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "author_id", nullable = false)
|
||||
private User author;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String body;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String hashtags;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(nullable = false)
|
||||
private PostStatus status = PostStatus.DRAFT;
|
||||
|
||||
@Column(name = "published_at")
|
||||
private LocalDateTime publishedAt;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", insertable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", insertable = false, updatable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Media> media;
|
||||
|
||||
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private List<Publication> publications;
|
||||
|
||||
public Long getPostId() {
|
||||
return postId;
|
||||
}
|
||||
|
||||
public void setPostId(Long postId) {
|
||||
this.postId = postId;
|
||||
}
|
||||
|
||||
public User getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(User author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setBody(String body) {
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getHashtags() {
|
||||
return hashtags;
|
||||
}
|
||||
|
||||
public void setHashtags(String hashtags) {
|
||||
this.hashtags = hashtags;
|
||||
}
|
||||
|
||||
public PostStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(PostStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getPublishedAt() {
|
||||
return publishedAt;
|
||||
}
|
||||
|
||||
public void setPublishedAt(LocalDateTime publishedAt) {
|
||||
this.publishedAt = publishedAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
|
||||
public List<Media> getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
public void setMedia(List<Media> media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public List<Publication> getPublications() {
|
||||
return publications;
|
||||
}
|
||||
|
||||
public void setPublications(List<Publication> publications) {
|
||||
this.publications = publications;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package es.adeptusminiaturium.backend.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||
import es.adeptusminiaturium.backend.enums.Platform;
|
||||
import es.adeptusminiaturium.backend.enums.PublicationStatus;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(
|
||||
name = "publications",
|
||||
uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "platform"})
|
||||
)
|
||||
public class Publication {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "publication_id")
|
||||
private Long publicationId;
|
||||
|
||||
@JsonManagedReference
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "post_id", nullable = false)
|
||||
private Post post;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(nullable = false)
|
||||
private Platform platform;
|
||||
|
||||
private String externalId;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(nullable = false)
|
||||
private PublicationStatus status = PublicationStatus.PENDING;
|
||||
|
||||
@Column(name = "published_at")
|
||||
private LocalDateTime publishedAt;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", insertable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public Long getPublicationId() {
|
||||
return publicationId;
|
||||
}
|
||||
|
||||
public void setPublicationId(Long publicationId) {
|
||||
this.publicationId = publicationId;
|
||||
}
|
||||
|
||||
public Post getPost() {
|
||||
return post;
|
||||
}
|
||||
|
||||
public void setPost(Post post) {
|
||||
this.post = post;
|
||||
}
|
||||
|
||||
public Platform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(Platform platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getExternalId() {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
public void setExternalId(String externalId) {
|
||||
this.externalId = externalId;
|
||||
}
|
||||
|
||||
public PublicationStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(PublicationStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getPublishedAt() {
|
||||
return publishedAt;
|
||||
}
|
||||
|
||||
public void setPublishedAt(LocalDateTime publishedAt) {
|
||||
this.publishedAt = publishedAt;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public void setErrorMessage(String errorMessage) {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package es.adeptusminiaturium.backend.model;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.CreationTimestamp;
|
||||
import org.hibernate.annotations.UpdateTimestamp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@Column(name = "user_id", nullable = false, updatable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(name = "display_name")
|
||||
private String displayName;
|
||||
|
||||
@Column(name = "user_name", nullable = false, unique = true)
|
||||
private String userName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String password;
|
||||
|
||||
private String avatar;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(nullable = false)
|
||||
private UserRole role = UserRole.USER;
|
||||
|
||||
@Enumerated(EnumType.ORDINAL)
|
||||
@Column(nullable = false)
|
||||
private UserStatus status = UserStatus.ACTIVE;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", insertable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@UpdateTimestamp
|
||||
@Column(name = "updated_at", insertable = false, updatable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public UUID getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(UUID userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getUserName() {
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName) {
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getAvatar() {
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public void setAvatar(String avatar) {
|
||||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public UserRole getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
public void setRole(UserRole role) {
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public UserStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(UserStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public LocalDateTime getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) {
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package es.adeptusminiaturium.backend.repository;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.Media;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface MediaRepository extends JpaRepository<Media, Long> {
|
||||
List<Media> findByPostPostId(Long postId);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package es.adeptusminiaturium.backend.repository;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.Post;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package es.adeptusminiaturium.backend.repository;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.Publication;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface PublicationRepository extends JpaRepository<Publication, Long> {
|
||||
List<Publication> findByPostPostId(Long postId);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package es.adeptusminiaturium.backend.repository;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, UUID> {
|
||||
Optional<User> findByUserName(String userName);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package es.adeptusminiaturium.backend.security;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.UserStatus;
|
||||
import es.adeptusminiaturium.backend.model.User;
|
||||
import es.adeptusminiaturium.backend.enums.UserRole;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class CustomUserDetails implements UserDetails {
|
||||
|
||||
private final User user;
|
||||
|
||||
public CustomUserDetails(User user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return user.getPassword();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return user.getUserName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return user.getStatus().equals(UserStatus.ACTIVE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package es.adeptusminiaturium.backend.service;
|
||||
|
||||
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||
import es.adeptusminiaturium.backend.security.CustomUserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserRepository userRepo;
|
||||
|
||||
public CustomUserDetailsService(UserRepository userRepo) {
|
||||
this.userRepo = userRepo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
return userRepo.findByUserName(username)
|
||||
.map(CustomUserDetails::new)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package es.adeptusminiaturium.backend.service;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.MediaDto;
|
||||
import es.adeptusminiaturium.backend.mapper.MediaMapper;
|
||||
import es.adeptusminiaturium.backend.model.Media;
|
||||
import es.adeptusminiaturium.backend.repository.MediaRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class MediaService {
|
||||
|
||||
private final MediaRepository repo;
|
||||
|
||||
public MediaService(MediaRepository repo) { this.repo = repo; }
|
||||
|
||||
@Transactional
|
||||
public List<MediaDto.Response> getAll() {
|
||||
return repo.findAll().stream().map(MediaMapper::toResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public MediaDto.Response getById(Long id) {
|
||||
return repo.findById(id).map(MediaMapper::toResponse).orElse(null);
|
||||
}
|
||||
|
||||
public MediaDto.Response create(MediaDto.Request dto) {
|
||||
Media media = MediaMapper.toEntity(dto);
|
||||
return MediaMapper.toResponse(repo.save(media));
|
||||
}
|
||||
|
||||
public MediaDto.Response update(Long id, MediaDto.Request dto) {
|
||||
return repo.findById(id).map(media -> {
|
||||
media.setMediaType(dto.getMediaType());
|
||||
media.setUrl(dto.getUrl());
|
||||
media.setPosition(dto.getPosition());
|
||||
return MediaMapper.toResponse(repo.save(media));
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
public void delete(Long id) { repo.deleteById(id); }
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package es.adeptusminiaturium.backend.service;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PostDto;
|
||||
import es.adeptusminiaturium.backend.mapper.PostMapper;
|
||||
import es.adeptusminiaturium.backend.model.Post;
|
||||
import es.adeptusminiaturium.backend.repository.PostRepository;
|
||||
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||
import es.adeptusminiaturium.backend.validator.PostValidator;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PostService {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public PostService(PostRepository postRepository, UserRepository userRepository) {
|
||||
this.postRepository = postRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<PostDto.Response> getAllPosts() {
|
||||
return postRepository.findAll().stream()
|
||||
.map(PostMapper::toResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public PostDto.Response createPost(PostDto.Request dto) {
|
||||
Post post = PostMapper.toEntity(dto);
|
||||
post.setAuthor(userRepository.findById(dto.getAuthorId()).orElseThrow());
|
||||
Post saved = postRepository.save(post);
|
||||
return PostMapper.toResponse(saved);
|
||||
}
|
||||
|
||||
public PostDto.Response getPostById(Long postId) {
|
||||
Post post = postRepository.findById(postId).orElseThrow();
|
||||
return PostMapper.toResponse(post);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package es.adeptusminiaturium.backend.service;
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.PublicationDto;
|
||||
import es.adeptusminiaturium.backend.mapper.PublicationMapper;
|
||||
import es.adeptusminiaturium.backend.model.Publication;
|
||||
import es.adeptusminiaturium.backend.repository.PublicationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class PublicationService {
|
||||
|
||||
private final PublicationRepository repo;
|
||||
|
||||
public PublicationService(PublicationRepository repo) { this.repo = repo; }
|
||||
|
||||
@Transactional
|
||||
public List<PublicationDto.Response> getAll() {
|
||||
return repo.findAll().stream().map(PublicationMapper::toResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public PublicationDto.Response getById(Long id) {
|
||||
return repo.findById(id).map(PublicationMapper::toResponse).orElse(null);
|
||||
}
|
||||
|
||||
public PublicationDto.Response create(PublicationDto.Request dto) {
|
||||
Publication pub = PublicationMapper.toEntity(dto);
|
||||
return PublicationMapper.toResponse(repo.save(pub));
|
||||
}
|
||||
|
||||
public PublicationDto.Response update(Long id, PublicationDto.Request dto) {
|
||||
return repo.findById(id).map(pub -> {
|
||||
pub.setPlatform(dto.getPlatform());
|
||||
pub.setExternalId(dto.getExternalId());
|
||||
pub.setStatus(dto.getStatus());
|
||||
return PublicationMapper.toResponse(repo.save(pub));
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
public void delete(Long id) { repo.deleteById(id); }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package es.adeptusminiaturium.backend.service;
|
||||
|
||||
|
||||
import es.adeptusminiaturium.backend.dto.UserDto;
|
||||
import es.adeptusminiaturium.backend.mapper.UserMapper;
|
||||
import es.adeptusminiaturium.backend.model.User;
|
||||
import es.adeptusminiaturium.backend.repository.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository repo;
|
||||
|
||||
public UserService(UserRepository repo) { this.repo = repo; }
|
||||
|
||||
public List<UserDto.Response> getAllUsers() {
|
||||
return repo.findAll().stream().map(UserMapper::toResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public UserDto.Response getUser(UUID userId) {
|
||||
return repo.findById(userId).map(UserMapper::toResponse).orElse(null);
|
||||
}
|
||||
|
||||
public UserDto.Response createUser(UserDto.Request dto) {
|
||||
User user = UserMapper.toEntity(dto);
|
||||
return UserMapper.toResponse(repo.save(user));
|
||||
}
|
||||
|
||||
public UserDto.Response updateUser(UUID userId, UserDto.Request dto) {
|
||||
return repo.findById(userId).map(user -> {
|
||||
user.setDisplayName(dto.getDisplayName());
|
||||
user.setUserName(dto.getUserName());
|
||||
user.setPassword(dto.getPassword());
|
||||
user.setRole(dto.getRole());
|
||||
user.setStatus(dto.getStatus());
|
||||
user.setUpdatedAt(LocalDateTime.now());
|
||||
return UserMapper.toResponse(repo.save(user));
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
public void deleteUser(UUID userId) { repo.deleteById(userId); }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package es.adeptusminiaturium.backend.validator;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.Media;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MediaValidator {
|
||||
|
||||
public static List<String> validate(Media media) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (media.getPost() == null) {
|
||||
errors.add("Media must belong to a post");
|
||||
}
|
||||
|
||||
if (media.getUrl() == null || media.getUrl().isBlank()) {
|
||||
errors.add("URL cannot be empty");
|
||||
} else if (media.getUrl().length() > 512) {
|
||||
errors.add("URL too long");
|
||||
}
|
||||
|
||||
if (media.getPosition() == null || media.getPosition() < 0) {
|
||||
errors.add("Position cannot be negative");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static void validateOrThrow(Media media) {
|
||||
List<String> errors = validate(media);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join(", ", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package es.adeptusminiaturium.backend.validator;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.Post;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PostValidator {
|
||||
|
||||
public static List<String> validate(Post post) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (post.getAuthor() == null) {
|
||||
errors.add("Post must have an author");
|
||||
}
|
||||
|
||||
if (post.getTitle() == null || post.getTitle().isBlank()) {
|
||||
errors.add("Title cannot be empty");
|
||||
} else if (post.getTitle().length() > 255) {
|
||||
errors.add("Title too long");
|
||||
}
|
||||
|
||||
if (post.getBody() != null && post.getBody().length() > 10000) {
|
||||
errors.add("Body too long");
|
||||
}
|
||||
|
||||
if (post.getHashtags() != null && post.getHashtags().length() > 1000) {
|
||||
errors.add("Hashtags too long");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static void validateOrThrow(Post post) {
|
||||
List<String> errors = validate(post);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join(", ", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package es.adeptusminiaturium.backend.validator;
|
||||
|
||||
import es.adeptusminiaturium.backend.enums.Platform;
|
||||
import es.adeptusminiaturium.backend.model.Publication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class PublicationValidator {
|
||||
|
||||
public static List<String> validate(Publication pub) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (pub.getPost() == null) {
|
||||
errors.add("Publication must belong to a post");
|
||||
}
|
||||
|
||||
if (pub.getPlatform() == null) {
|
||||
errors.add("Platform cannot be null");
|
||||
} else if (!Platform.valuesList().contains(pub.getPlatform())) {
|
||||
errors.add("Invalid platform");
|
||||
}
|
||||
|
||||
if (pub.getStatus() == null) {
|
||||
errors.add("Status cannot be null");
|
||||
}
|
||||
|
||||
if (pub.getExternalId() != null && pub.getExternalId().length() > 255) {
|
||||
errors.add("ExternalId too long");
|
||||
}
|
||||
|
||||
if (pub.getErrorMessage() != null && pub.getErrorMessage().length() > 1000) {
|
||||
errors.add("ErrorMessage too long");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static void validateOrThrow(Publication pub) {
|
||||
List<String> errors = validate(pub);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join(", ", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package es.adeptusminiaturium.backend.validator;
|
||||
|
||||
import es.adeptusminiaturium.backend.model.User;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class UserValidator {
|
||||
|
||||
public static List<String> validate(User user) {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (user.getUserName() == null || user.getUserName().isBlank()) {
|
||||
errors.add("Username cannot be empty");
|
||||
}
|
||||
|
||||
if (user.getPassword() == null || user.getPassword().length() < 8) {
|
||||
errors.add("Password must have at least 8 characters");
|
||||
}
|
||||
|
||||
if (user.getDisplayName() != null && user.getDisplayName().length() > 255) {
|
||||
errors.add("DisplayName too long");
|
||||
}
|
||||
|
||||
if (user.getAvatar() != null && user.getAvatar().length() > 255) {
|
||||
errors.add("Avatar URL too long");
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
public static void validateOrThrow(User user) {
|
||||
List<String> errors = validate(user);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new IllegalArgumentException(String.join(", ", errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
16
backend/src/main/resources/application-dev.yaml
Normal file
16
backend/src/main/resources/application-dev.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
jpa:
|
||||
show-sql: true
|
||||
datasource:
|
||||
url: jdbc:mariadb://localhost:3306/miniaturium
|
||||
username: admin
|
||||
password: ${DB_PASS}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: DEBUG
|
||||
org.hibernate.orm.jdbc.bind: TRACE
|
||||
13
backend/src/main/resources/application-prod.yaml
Normal file
13
backend/src/main/resources/application-prod.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mariadb://mariadb:3306/miniaturium
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASS}
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.hibernate.SQL: WARN
|
||||
25
backend/src/main/resources/application.yaml
Normal file
25
backend/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
spring:
|
||||
application:
|
||||
name: backend
|
||||
|
||||
jpa:
|
||||
open-in-view: false
|
||||
hibernate:
|
||||
ddl-auto: validate
|
||||
properties:
|
||||
hibernate:
|
||||
jdbc:
|
||||
time_zone: UTC
|
||||
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
time-zone: Europe/Madrid
|
||||
|
||||
jwt:
|
||||
expiration-ms: 3600000
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info
|
||||
52
backend/src/main/resources/bd.sql
Normal file
52
backend/src/main/resources/bd.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
CREATE TABLE users(
|
||||
user_id UUID PRIMARY KEY,
|
||||
display_name VARCHAR(255),
|
||||
user_name VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
avatar VARCHAR(255) DEFAULT NULL,
|
||||
role TINYINT NOT NULL DEFAULT 0, -- 0 = user, 1 = admin
|
||||
status TINYINT NOT NULL DEFAULT 1, -- 0 = inactivo, 1 = activo
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()
|
||||
);
|
||||
|
||||
CREATE TABLE posts(
|
||||
post_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
author_id UUID NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
body TEXT,
|
||||
hashtags TEXT,
|
||||
status TINYINT NOT NULL DEFAULT 0, -- 0 = draft, 1 = published, 2 = archived
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP(),
|
||||
published_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (author_id) REFERENCES users(user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE media(
|
||||
media_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
post_id BIGINT NOT NULL,
|
||||
media_type TINYINT NOT NULL DEFAULT 0, -- 0 = image, 1 = video
|
||||
url VARCHAR(512) NOT NULL,
|
||||
position BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE publications(
|
||||
publication_id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
post_id BIGINT NOT NULL,
|
||||
platform TINYINT NOT NULL, -- 0 = web, 1 = instagram, 2 = tiktok, 3 = twitter
|
||||
external_id VARCHAR(255) DEFAULT NULL,
|
||||
status TINYINT NOT NULL DEFAULT 0, -- 0 = pending, 1 = published, 2 = failed
|
||||
published_at TIMESTAMP NULL,
|
||||
error_message TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
|
||||
UNIQUE (post_id, platform),
|
||||
FOREIGN KEY (post_id) REFERENCES posts(post_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_posts_author ON posts(author_id);
|
||||
CREATE INDEX idx_posts_status_published ON posts(status, published_at);
|
||||
CREATE INDEX idx_media_post_position ON media(post_id, position);
|
||||
CREATE INDEX idx_publications_status ON publications(status);
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title></title>
|
||||
<title>Adeptus Miniaturium</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="p-0 m-0"></div>
|
||||
@@ -19,10 +19,13 @@
|
||||
"bootstrap": "^5.3.7",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.8.10",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^16.5.4",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-slick": "^0.30.3",
|
||||
"slick-carousel": "^1.8.1"
|
||||
},
|
||||
BIN
frontend/public/images/mini_1.jpeg
Normal file
BIN
frontend/public/images/mini_1.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 237 KiB |
BIN
frontend/public/images/mini_2.jpeg
Normal file
BIN
frontend/public/images/mini_2.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 156 KiB |
BIN
frontend/public/images/mini_3.jpeg
Normal file
BIN
frontend/public/images/mini_3.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
frontend/public/images/mini_4.jpeg
Normal file
BIN
frontend/public/images/mini_4.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
BIN
frontend/public/images/mini_5.jpeg
Normal file
BIN
frontend/public/images/mini_5.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 233 KiB |
BIN
frontend/public/images/pfp.jpg
Normal file
BIN
frontend/public/images/pfp.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
26
frontend/src/App.jsx
Normal file
26
frontend/src/App.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Footer from '@/components/Footer'
|
||||
import Header from '@/components/Header'
|
||||
import NavBar from '@/components/NavBar/NavBar'
|
||||
import Home from '@/pages/Home'
|
||||
import { Route, Routes, useLocation } from 'react-router-dom'
|
||||
import Login from './pages/Login'
|
||||
import "./i18n";
|
||||
|
||||
const App = () => {
|
||||
const withoutFooter = ["/login"]
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header profilePic={"/images/pfp.jpg"}/>
|
||||
<NavBar />
|
||||
<Routes>
|
||||
<Route path='/' element={<Home />} />
|
||||
<Route path='/login' element={<Login />} />
|
||||
</Routes>
|
||||
{!withoutFooter.includes(location.pathname) && <Footer />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -11,10 +11,12 @@ import CustomContainer from '@/components/CustomContainer.jsx';
|
||||
import ContentWrapper from '@/components/ContentWrapper.jsx';
|
||||
|
||||
import '@/css/LoginForm.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const LoginForm = () => {
|
||||
const { login, error } = useContext(AuthContext);
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [formState, setFormState] = useState({
|
||||
emailOrUserName: "",
|
||||
@@ -47,24 +49,21 @@ const LoginForm = () => {
|
||||
await login(loginBody);
|
||||
navigate("/");
|
||||
} catch (err) {
|
||||
console.error("Error de login:", err.message);
|
||||
console.error("Error:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomContainer>
|
||||
<ContentWrapper>
|
||||
<div className="login-card card shadow p-5 rounded-5 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
||||
<h1 className="text-center">Inicio de sesión</h1>
|
||||
<div className="login-card card shadow rounded-0 mx-auto col-12 col-md-8 col-lg-6 col-xl-5 d-flex flex-column gap-4">
|
||||
<h1 className="text-center">{t("login.title")}</h1>
|
||||
<Form className="d-flex flex-column gap-5" onSubmit={handleSubmit}>
|
||||
<div className="d-flex flex-column gap-3">
|
||||
<FloatingLabel
|
||||
controlId="floatingUsuario"
|
||||
label={
|
||||
<>
|
||||
<FontAwesomeIcon icon={faUser} className="me-2" />
|
||||
Usuario o Email
|
||||
</>
|
||||
t("login.user_label")
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
@@ -73,7 +72,7 @@ const LoginForm = () => {
|
||||
name="emailOrUserName"
|
||||
value={formState.emailOrUserName}
|
||||
onChange={handleChange}
|
||||
className="rounded-4"
|
||||
className="rounded-0"
|
||||
/>
|
||||
</FloatingLabel>
|
||||
|
||||
@@ -82,20 +81,6 @@ const LoginForm = () => {
|
||||
onChange={handleChange}
|
||||
name="password"
|
||||
/>
|
||||
|
||||
<div className="d-flex flex-column flex-sm-row justify-content-between align-items-center gap-2">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
name="keepLoggedIn"
|
||||
label="Mantener sesión iniciada"
|
||||
className="text-secondary"
|
||||
value={formState.keepLoggedIn}
|
||||
onChange={(e) => { formState.keepLoggedIn = e.target.checked; setFormState({ ...formState }) }}
|
||||
/>
|
||||
{/*<Link disabled to="#" className="muted">
|
||||
Olvidé mi contraseña
|
||||
</Link>*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
@@ -105,8 +90,8 @@ const LoginForm = () => {
|
||||
)}
|
||||
|
||||
<div className="text-center">
|
||||
<Button type="submit" className="w-75 padding-4 rounded-4 border-0 shadow-sm login-button">
|
||||
Iniciar sesión
|
||||
<Button type="submit" className="border-0 w-75 padding-4 rounded-0 shadow-sm login-button">
|
||||
{t("login.button")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -4,9 +4,11 @@ import '@/css/PasswordInput.css';
|
||||
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const PasswordInput = ({ value, onChange, name = "password" }) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleShow = () => setShow(prev => !prev);
|
||||
|
||||
@@ -15,10 +17,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
||||
<FloatingLabel
|
||||
controlId="passwordInput"
|
||||
label={
|
||||
<>
|
||||
<FontAwesomeIcon icon={faKey} className="me-2" />
|
||||
Contraseña
|
||||
</>
|
||||
t("login.password_label")
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
@@ -27,7 +26,7 @@ const PasswordInput = ({ value, onChange, name = "password" }) => {
|
||||
value={value}
|
||||
placeholder=""
|
||||
onChange={onChange}
|
||||
className="rounded-4 pe-5"
|
||||
className="rounded-0 pe-5"
|
||||
/>
|
||||
</FloatingLabel>
|
||||
|
||||
@@ -2,8 +2,10 @@ import PropTypes from 'prop-types';
|
||||
|
||||
const CustomContainer = ({ children }) => {
|
||||
return (
|
||||
<main className="px-4 py-5">
|
||||
{children}
|
||||
<main className="container my-5">
|
||||
<div className="d-flex flex-column gap-5">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
10
frontend/src/components/Footer.jsx
Normal file
10
frontend/src/components/Footer.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import '@/css/Footer.css'
|
||||
|
||||
const Footer = () => (
|
||||
<footer className="text-center py-5">
|
||||
<span className="skull-icon mb-2">☠</span>
|
||||
<p className="m-0">Adeptus Miniaturium © 41st Millennium</p>
|
||||
</footer>
|
||||
);
|
||||
|
||||
export default Footer;
|
||||
21
frontend/src/components/Header.jsx
Normal file
21
frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import '@/css/Header.css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<header className="imperial-header py-5 text-center position-relative">
|
||||
<img
|
||||
src="/images/purity.png"
|
||||
alt="Purity Seal"
|
||||
className="purity-seal left"
|
||||
/>
|
||||
|
||||
<h1 className="mb-2">Adeptus Miniaturium</h1>
|
||||
<p className="m-0">{t("header.subtitle")}</p>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
37
frontend/src/components/Home/ScrollBackgroundSpin.jsx
Normal file
37
frontend/src/components/Home/ScrollBackgroundSpin.jsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const images = [
|
||||
"/images/mini_1.jpeg",
|
||||
"/images/mini_2.jpeg",
|
||||
"/images/mini_3.jpeg",
|
||||
"/images/mini_4.jpeg",
|
||||
"/images/mini_5.jpeg",
|
||||
];
|
||||
|
||||
const TOTAL_FRAMES = images.length;
|
||||
|
||||
const ScrollBackgroundSpin = () => {
|
||||
const [frame, setFrame] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollTop = window.scrollY;
|
||||
const maxScroll = document.body.scrollHeight - window.innerHeight;
|
||||
const progress = scrollTop / maxScroll;
|
||||
|
||||
const currentFrame = Math.floor(progress * TOTAL_FRAMES);
|
||||
setFrame(Math.min(currentFrame, TOTAL_FRAMES - 1));
|
||||
};
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
return () => window.removeEventListener("scroll", handleScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="spin-background">
|
||||
<img src={images[frame]} alt="Miniatura fondo" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScrollBackgroundSpin;
|
||||
33
frontend/src/components/LanguageButton.jsx
Normal file
33
frontend/src/components/LanguageButton.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import NavItem from '@/components/NavBar/NavItem';
|
||||
|
||||
const LanguageButton = ({ as = "button", index = null }) => {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
const toggleLanguage = () => {
|
||||
console.log(i18n.language);
|
||||
const nextLang = i18n.language === "es" ? "en" : "es";
|
||||
i18n.changeLanguage(nextLang);
|
||||
};
|
||||
|
||||
if (as === "navitem") {
|
||||
return (
|
||||
<NavItem
|
||||
item={{
|
||||
label: i18n.language === "es" ? "🇪🇸" : "🇺🇸",
|
||||
href: "#"
|
||||
}}
|
||||
index={index}
|
||||
onClick={toggleLanguage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button onClick={toggleLanguage} className="btn-imperial">
|
||||
{i18n.language === "es" ? "🇪🇸" : "🇺🇸"}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default LanguageButton;
|
||||
26
frontend/src/components/NavBar/NavBar.jsx
Normal file
26
frontend/src/components/NavBar/NavBar.jsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useWindowWidth } from '@/hooks/useWindowWidth';
|
||||
import NavBarDesktop from '@/components/NavBar/desktop/NavBarDesktop';
|
||||
import NavBarMobile from '@/components/NavBar/mobile/NavBarMobile';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const NavBar = () => {
|
||||
const { t } = useTranslation();
|
||||
const width = useWindowWidth();
|
||||
const navItems = [
|
||||
{ label: t("nav.inicio.label"), href: `/#${t("nav.inicio.href")}` },
|
||||
{ label: t("nav.muestras.label"), href: `/#${t("nav.muestras.href")}` },
|
||||
{ label: t("nav.pedidos.label"), href: `/#${t("nav.pedidos.href")}` },
|
||||
{ label: t("nav.sobre.label"), href: `/#${t("nav.sobre.href")}` },
|
||||
{ label: t("nav.login.label"), href: `/${t("nav.login.href")}` }
|
||||
];
|
||||
|
||||
return width > 991.98 ? (
|
||||
<nav className="nav-container p-0">
|
||||
<NavBarDesktop navItems={navItems} />
|
||||
</nav>
|
||||
) : (
|
||||
<NavBarMobile navItems={navItems} />
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
38
frontend/src/components/NavBar/NavItem.jsx
Normal file
38
frontend/src/components/NavBar/NavItem.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { HashLink } from "react-router-hash-link/dist/react-router-hash-link.cjs.production";
|
||||
|
||||
const NavItem = ({ item, index, onClick, onCloseNav }) => {
|
||||
let borderClass = `${index === 0 ? "border-left border-right" : "border-right"}`;
|
||||
|
||||
const handleClick = (e) => {
|
||||
if (onClick) {
|
||||
e.preventDefault();
|
||||
onClick();
|
||||
} else if (item.href === "#") {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
if (onCloseNav) {
|
||||
onCloseNav();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={`mx-0 ${borderClass}`}>
|
||||
{onClick || item.href === "#" ? (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="nav-button"
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
) : (
|
||||
<HashLink smooth to={item.href} className="nav-link-custom" onClick={handleClick}>
|
||||
{item.label}
|
||||
</HashLink>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavItem;
|
||||
30
frontend/src/components/NavBar/desktop/NavBarDesktop.jsx
Normal file
30
frontend/src/components/NavBar/desktop/NavBarDesktop.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import NavItem from '@/components/NavBar/NavItem';
|
||||
import LanguageButton from '@/components/LanguageButton';
|
||||
|
||||
import '@/css/NavBar.css';
|
||||
|
||||
const NavBarDesktop = ({ navItems }) => {
|
||||
const centerItems = navItems.slice(0, navItems.length - 1);
|
||||
const rightItems = navItems.slice(-1);
|
||||
|
||||
return (
|
||||
<ul className="d-flex align-items-center list-unstyled m-0 w-100 justify-content-center position-relative">
|
||||
<li className="position-absolute start-0 d-flex">
|
||||
<LanguageButton as='navitem' index={0} total={rightItems.length + 1} />
|
||||
</li>
|
||||
|
||||
{centerItems.map((item, index) => (
|
||||
<NavItem key={index} item={item} index={index} total={centerItems.length} />
|
||||
))}
|
||||
|
||||
<li className="position-absolute end-0 d-flex">
|
||||
{rightItems.map((item, index) => (
|
||||
<NavItem key={index} item={item} index={index} total={rightItems.length} />
|
||||
))}
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBarDesktop;
|
||||
41
frontend/src/components/NavBar/mobile/NavBarMobile.jsx
Normal file
41
frontend/src/components/NavBar/mobile/NavBarMobile.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import NavItem from '@/components/NavBar/NavItem';
|
||||
import LanguageButton from '@/components/LanguageButton';
|
||||
import '@/css/NavBarMobile.css';
|
||||
|
||||
const NavBarMobile = ({ navItems }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="nav-container d-lg-none">
|
||||
<div className="nav-mobile-header d-flex justify-content-between align-items-center">
|
||||
{/* Por si algun dia se pone
|
||||
<div className="nav-mobile-brand"></div>
|
||||
*/}
|
||||
<button
|
||||
className={`nav-mobile-toggle ms-2 ${open ? 'open' : ''}`}
|
||||
onClick={() => setOpen(!open)}
|
||||
>
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{open && (
|
||||
<ul className="nav-list-mobile d-flex flex-column align-items-center list-unstyled m-0 w-100">
|
||||
{navItems.map((item, index) => (
|
||||
<NavItem key={index} item={item} onCloseNav={handleClose} />
|
||||
))}
|
||||
<LanguageButton as="navitem" />
|
||||
</ul>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBarMobile;
|
||||
9
frontend/src/components/TechCard.jsx
Normal file
9
frontend/src/components/TechCard.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import '@/css/TechCard.css'
|
||||
|
||||
const TechCard = ({ children, className = '' }) => (
|
||||
<div className={`tech-card ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default TechCard;
|
||||
@@ -8,8 +8,12 @@ export const AuthProvider = ({ children }) => {
|
||||
const axios = createAxiosInstance();
|
||||
const { config } = useConfig();
|
||||
|
||||
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem("user")) || null);
|
||||
const [token, setToken] = useState(() => localStorage.getItem("token"));
|
||||
const [identity, setIdentity] = useState(() => {
|
||||
const stored = localStorage.getItem("identity");
|
||||
return stored ? JSON.parse(stored) : null;
|
||||
});
|
||||
|
||||
const [authStatus, setAuthStatus] = useState("checking");
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
@@ -29,6 +33,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const res = await axios.get(VALIDATE_URL, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (res.status === 200) {
|
||||
setAuthStatus("authenticated");
|
||||
} else {
|
||||
@@ -45,53 +50,121 @@ export const AuthProvider = ({ children }) => {
|
||||
|
||||
const login = async (formData) => {
|
||||
setError(null);
|
||||
|
||||
const BASE_URL = config.apiConfig.baseUrl;
|
||||
const LOGIN_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.login}`;
|
||||
|
||||
|
||||
try {
|
||||
const res = await axios.post(LOGIN_URL, formData);
|
||||
const { token, member, tokenTime } = res.data.data;
|
||||
|
||||
|
||||
const { token, user, account, metadata } = res.data;
|
||||
|
||||
const identity = {
|
||||
user,
|
||||
account,
|
||||
metadata,
|
||||
};
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("user", JSON.stringify(member));
|
||||
localStorage.setItem("tokenTime", tokenTime);
|
||||
|
||||
localStorage.setItem("identity", JSON.stringify(identity));
|
||||
|
||||
setToken(token);
|
||||
setUser(member);
|
||||
setIdentity(identity);
|
||||
setAuthStatus("authenticated");
|
||||
} catch (err) {
|
||||
console.error("Error al iniciar sesión:", err);
|
||||
|
||||
|
||||
let message = "Ha ocurrido un error inesperado.";
|
||||
|
||||
|
||||
if (err.response) {
|
||||
const { status, data } = err.response;
|
||||
|
||||
|
||||
if (status === 400) {
|
||||
message = "Usuario o contraseña incorrectos.";
|
||||
} else if (status === 403) {
|
||||
message = "Tu cuenta está inactiva o ha sido suspendida.";
|
||||
message = "Tu cuenta está inactiva o suspendida.";
|
||||
} else if (status === 404) {
|
||||
message = "Usuario no encontrado.";
|
||||
} else if (data?.message) {
|
||||
message = data.message;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setError(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (formData) => {
|
||||
setError(null);
|
||||
|
||||
const BASE_URL = config.apiConfig.baseUrl;
|
||||
const REGISTER_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.register}`;
|
||||
|
||||
try {
|
||||
const res = await axios.post(REGISTER_URL, formData);
|
||||
|
||||
const { token, user, account, metadata } = res.data;
|
||||
|
||||
const identity = {
|
||||
user,
|
||||
account,
|
||||
metadata,
|
||||
};
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
localStorage.setItem("identity", JSON.stringify(identity));
|
||||
|
||||
setToken(token);
|
||||
setIdentity(identity);
|
||||
setAuthStatus("authenticated");
|
||||
} catch (err) {
|
||||
console.error("Error al registrarse:", err);
|
||||
|
||||
let message = "Ha ocurrido un error inesperado.";
|
||||
|
||||
if (err.response) {
|
||||
const { status, data } = err.response;
|
||||
|
||||
if (status === 400) {
|
||||
message = "Usuario o contraseña incorrectos.";
|
||||
} else if (status === 403) {
|
||||
message = "Tu cuenta está inactiva o suspendida.";
|
||||
} else if (status === 404) {
|
||||
message = "Usuario no encontrado.";
|
||||
} else if (data?.message) {
|
||||
message = data.message;
|
||||
}
|
||||
}
|
||||
|
||||
setError(message);
|
||||
throw new Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.clear();
|
||||
setUser(null);
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("identity");
|
||||
setIdentity(null);
|
||||
setToken(null);
|
||||
setAuthStatus("unauthenticated");
|
||||
};
|
||||
|
||||
const clearError = () => setError(null);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, authStatus, login, logout, error }}>
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
identity, // { user, account, metadata }
|
||||
token,
|
||||
authStatus,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
error,
|
||||
clearError
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
@@ -4,8 +4,8 @@ import { useData } from "../hooks/useData";
|
||||
|
||||
export const DataContext = createContext();
|
||||
|
||||
export const DataProvider = ({ config, children }) => {
|
||||
const data = useData(config);
|
||||
export const DataProvider = ({ config, onError, children }) => {
|
||||
const data = useData(config, onError);
|
||||
|
||||
return (
|
||||
<DataContext.Provider value={data}>
|
||||
40
frontend/src/context/ErrorContext.jsx
Normal file
40
frontend/src/context/ErrorContext.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createContext, useState, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import NotificationModal from '@/components/NotificationModal';
|
||||
|
||||
const ErrorContext = createContext();
|
||||
|
||||
export const ErrorProvider = ({ children }) => {
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const showError = (err) => {
|
||||
setError({
|
||||
title: err.status ? `Error ${err.status}` : "Error",
|
||||
message: err.message,
|
||||
variant: 'danger'
|
||||
});
|
||||
};
|
||||
|
||||
const closeError = () => setError(null);
|
||||
|
||||
return (
|
||||
<ErrorContext.Provider value={{ showError }}>
|
||||
{children}
|
||||
{error && (
|
||||
<NotificationModal
|
||||
show={true}
|
||||
onClose={closeError}
|
||||
title={error.title}
|
||||
message={error.message}
|
||||
variant='danger'
|
||||
buttons={[{ label: "Aceptar", variant: "danger", onClick: closeError }]}
|
||||
/>
|
||||
)}
|
||||
</ErrorContext.Provider>
|
||||
);
|
||||
};
|
||||
ErrorProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export const useError = () => useContext(ErrorContext);
|
||||
46
frontend/src/css/AnimatedDropdown.css
Normal file
46
frontend/src/css/AnimatedDropdown.css
Normal file
@@ -0,0 +1,46 @@
|
||||
.dropdown-menu .dropdown-divider {
|
||||
border-top: 1px solid var(--dirty-gold);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
background: #0f1114 !important;
|
||||
border: 1px solid var(--dirty-gold) !important;
|
||||
box-shadow: 0 0 20px rgba(0,0,0,0.9);
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
clip-path: polygon(
|
||||
10px 0, 100% 0,
|
||||
100% calc(100% - 10px),
|
||||
calc(100% - 10px) 100%,
|
||||
0 100%, 0 10px
|
||||
);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
background: #000 !important;
|
||||
box-shadow: 0 0 25px rgba(138, 11, 11, 0.4);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
background: transparent !important;
|
||||
color: var(--parchment) !important;
|
||||
font-family: 'Cinzel', serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
padding: 12px 20px;
|
||||
transition: 0.3s;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: var(--blood-god) !important;
|
||||
color: #fff !important;
|
||||
text-shadow: 0 0 5px #fff;
|
||||
box-shadow: inset 0 0 10px #000;
|
||||
}
|
||||
|
||||
.dropdown-item.disabled,
|
||||
.disabled.text-muted {
|
||||
color: #444 !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
12
frontend/src/css/Footer.css
Normal file
12
frontend/src/css/Footer.css
Normal file
@@ -0,0 +1,12 @@
|
||||
footer {
|
||||
background-color: #020202;
|
||||
border-top: 1px solid var(--dirty-gold);
|
||||
color: #555;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.skull-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--parchment);
|
||||
display: block;
|
||||
}
|
||||
75
frontend/src/css/Header.css
Normal file
75
frontend/src/css/Header.css
Normal file
@@ -0,0 +1,75 @@
|
||||
header {
|
||||
background:
|
||||
linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,1)),
|
||||
url('https://www.transparenttextures.com/patterns/dark-matter.png');
|
||||
border-bottom: 4px double var(--imperial-gold);
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
.imperial-header {
|
||||
overflow: visible;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.purity-seal {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
width: 196px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.purity-seal.left {
|
||||
left: 8%;
|
||||
}
|
||||
|
||||
.purity-seal.right {
|
||||
right: 5%;
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 3.5rem;
|
||||
font-weight: 900;
|
||||
color: var(--imperial-gold);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 5px;
|
||||
text-shadow: 0 0 15px rgba(198, 163, 77, 0.6);
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--blood-god);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 3px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 5px var(--blood-god);
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
header h1 {
|
||||
font-size: 2.6rem;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 1rem;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
header {
|
||||
padding: 2.5rem 1rem !important;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
header p {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
97
frontend/src/css/LoginForm.css
Normal file
97
frontend/src/css/LoginForm.css
Normal file
@@ -0,0 +1,97 @@
|
||||
.login-card {
|
||||
background: #111 !important;
|
||||
border: 1px solid var(--dirty-gold) !important;
|
||||
color: var(--parchment) !important;
|
||||
padding: 40px;
|
||||
clip-path: polygon(
|
||||
20px 0, 100% 0,
|
||||
100% calc(100% - 20px),
|
||||
calc(100% - 20px) 100%,
|
||||
0 100%, 0 20px
|
||||
);
|
||||
box-shadow: 0 0 25px rgba(0,0,0,0.9);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border: 1px dashed #222;
|
||||
pointer-events: none;
|
||||
clip-path: inherit;
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
background-color: rgba(0,0,0,0.8) !important;
|
||||
border: 1px solid var(--plasteel) !important;
|
||||
color: var(--imperial-gold) !important;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
letter-spacing: 1px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
input.form-control:focus {
|
||||
background-color: #000 !important;
|
||||
border-color: var(--blood-god) !important;
|
||||
box-shadow: 0 0 10px rgba(138, 11, 11, 0.4) !important;
|
||||
color: var(--imperial-gold) !important;
|
||||
}
|
||||
|
||||
.form-floating > label {
|
||||
font-family: 'Cinzel', serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: var(--dirty-gold) !important;
|
||||
}
|
||||
|
||||
.form-floating > label::after {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
font-family: 'Cinzel', serif !important;
|
||||
font-weight: bold !important;
|
||||
text-transform: uppercase !important;
|
||||
letter-spacing: 2px !important;
|
||||
background: #000 !important;
|
||||
border: 1px solid var(--imperial-gold) !important;
|
||||
color: var(--imperial-gold) !important;
|
||||
padding: 15px 40px !important;
|
||||
clip-path: polygon(
|
||||
10px 0, 100% 0,
|
||||
100% calc(100% - 10px),
|
||||
calc(100% - 10px) 100%,
|
||||
0 100%, 0 10px
|
||||
);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
background: var(--blood-god) !important;
|
||||
color: #fff !important;
|
||||
border-color: var(--plasma-glow) !important;
|
||||
box-shadow: 0 0 20px var(--blood-god) !important;
|
||||
text-shadow: 0 0 5px #fff !important;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.login-card {
|
||||
padding: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.login-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
padding: 12px 24px !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
72
frontend/src/css/NavBar.css
Normal file
72
frontend/src/css/NavBar.css
Normal file
@@ -0,0 +1,72 @@
|
||||
.nav-container {
|
||||
background-color: #0f1114;
|
||||
border-bottom: 1px solid var(--dirty-gold);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 900;
|
||||
}
|
||||
|
||||
.nav-link-custom {
|
||||
display: block;
|
||||
padding: 15px 30px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
transition: 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li.border-left .nav-link-custom {
|
||||
border-left: 1px solid #222;
|
||||
}
|
||||
|
||||
li.border-right .nav-link-custom {
|
||||
border-right: 1px solid #222;
|
||||
}
|
||||
|
||||
.nav-link-custom:hover {
|
||||
color: var(--parchment);
|
||||
background-color: var(--blood-god);
|
||||
box-shadow: inset 0 0 10px #000;
|
||||
}
|
||||
|
||||
.nav-link-custom::before { content: '[ '; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||
.nav-link-custom::after { content: ' ]'; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||
.nav-link-custom:hover::before, .nav-link-custom:hover::after { opacity: 1; }
|
||||
|
||||
.nav-button {
|
||||
all: unset;
|
||||
display: block;
|
||||
padding: 15px 30px;
|
||||
color: #666;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
transition: 0.3s;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.border-left .nav-button {
|
||||
border-left: 1px solid #222;
|
||||
}
|
||||
|
||||
.border-right .nav-button {
|
||||
border-right: 1px solid #222;
|
||||
}
|
||||
|
||||
.nav-button:hover {
|
||||
color: var(--parchment);
|
||||
background-color: var(--blood-god);
|
||||
box-shadow: inset 0 0 10px #000;
|
||||
}
|
||||
|
||||
.nav-button::before { content: '[ '; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||
.nav-button::after { content: ' ]'; opacity: 0; transition: 0.3s; color: var(--imperial-gold); }
|
||||
|
||||
.nav-button:hover::before,
|
||||
.nav-button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
67
frontend/src/css/NavBarMobile.css
Normal file
67
frontend/src/css/NavBarMobile.css
Normal file
@@ -0,0 +1,67 @@
|
||||
.nav-container {
|
||||
background-color: #0f1114;
|
||||
border-bottom: 1px solid var(--dirty-gold);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 900;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-mobile-header {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.nav-mobile-brand {
|
||||
color: #666;
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-mobile-toggle {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nav-mobile-toggle span {
|
||||
display: block;
|
||||
width: 25px;
|
||||
height: 2px;
|
||||
background-color: var(--dirty-gold);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-mobile-toggle.open span:nth-child(1) {
|
||||
transform: rotate(45deg) translate(5px, 5px);
|
||||
}
|
||||
|
||||
.nav-mobile-toggle.open span:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-mobile-toggle.open span:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(5px, -5px);
|
||||
}
|
||||
|
||||
.nav-list-mobile {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-list-mobile li {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-list-mobile .nav-link-custom,
|
||||
.nav-list-mobile .nav-button {
|
||||
padding: 12px 0;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #222;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ button.show-button {
|
||||
color: var(--show-btn-color);
|
||||
|
||||
}
|
||||
|
||||
button.show-button:hover {
|
||||
color: var(--show-btn-hover);
|
||||
}
|
||||
47
frontend/src/css/TechCard.css
Normal file
47
frontend/src/css/TechCard.css
Normal file
@@ -0,0 +1,47 @@
|
||||
.tech-card {
|
||||
background: #111 !important;
|
||||
border: 1px solid var(--dirty-gold);
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
clip-path: polygon(
|
||||
20px 0, 100% 0,
|
||||
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||
0 100%, 0 20px
|
||||
);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.tech-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px; left: 5px; right: 5px; bottom: 5px;
|
||||
border: 1px dashed #333;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
clip-path: polygon(
|
||||
20px 0, 100% 0,
|
||||
100% calc(100% - 20px), calc(100% - 20px) 100%,
|
||||
0 100%, 0 20px
|
||||
);
|
||||
}
|
||||
|
||||
.tech-card p {
|
||||
line-height: 1.6;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
.tech-card {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.tech-card p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.tech-card {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
308
frontend/src/css/index.css
Normal file
308
frontend/src/css/index.css
Normal file
@@ -0,0 +1,308 @@
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt7-GT7LEc.woff2) format('woff2');
|
||||
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/cinzel/v26/8vIJ7ww63mVu7gt79mT7.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Share Tech Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(https://fonts.gstatic.com/s/sharetechmono/v16/J7aHnp1uDWRBEqV98dVQztYldFcLowEF.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
:root {
|
||||
--void-black: #050505;
|
||||
--blood-god: #8a0b0b;
|
||||
--plasma-glow: #ff3333;
|
||||
--imperial-gold: #c6a34d;
|
||||
--dirty-gold: #8c7335;
|
||||
--plasteel: #2f353b;
|
||||
--mechanicus-green: #23382c;
|
||||
--parchment: #c2bbad;
|
||||
--crt-line: rgba(18, 16, 16, 0.5);
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
background-color: var(--void-black);
|
||||
color: var(--parchment);
|
||||
letter-spacing: 1px;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* CRT */
|
||||
body::after {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.06), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.06));
|
||||
z-index: 1000;
|
||||
background-size: 100% 2px, 3px 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* SECCIONES Y TITULOS */
|
||||
section {
|
||||
padding: 80px 20px !important;
|
||||
max-width: 1200px !important;
|
||||
margin: auto !important;
|
||||
scroll-margin-top: 40px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 2.5rem;
|
||||
color: var(--parchment);
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.section-title::before, .section-title::after {
|
||||
content: "+++";
|
||||
color: var(--dirty-gold);
|
||||
margin: 0 15px;
|
||||
font-size: 1.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* SPIN */
|
||||
.spin-background {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spin-background img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.15;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
/* BOTONES */
|
||||
.btn-imperial {
|
||||
display: inline-block;
|
||||
padding: 15px 40px;
|
||||
background: #000;
|
||||
border: 1px solid var(--imperial-gold);
|
||||
color: var(--imperial-gold);
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
cursor: pointer;
|
||||
transition: 0.4s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-imperial:hover {
|
||||
background: var(--blood-god);
|
||||
color: #fff;
|
||||
border-color: var(--plasma-glow);
|
||||
box-shadow: 0 0 20px var(--blood-god);
|
||||
text-shadow: 0 0 5px #fff;
|
||||
}
|
||||
|
||||
/* FOTOS */
|
||||
.foto-frame {
|
||||
position: relative;
|
||||
border: 2px solid var(--plasteel);
|
||||
padding: 5px;
|
||||
background: #000;
|
||||
transition: 0.3s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.foto-frame:hover {
|
||||
border-color: var(--imperial-gold);
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.foto-placeholder {
|
||||
height: 300px;
|
||||
background: radial-gradient(circle, #222, #000);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
color: #444;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
|
||||
.foto-placeholder span {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.foto-caption {
|
||||
background: var(--plasteel);
|
||||
color: #aaa;
|
||||
padding: 10px;
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* FORMULARIO */
|
||||
.tech-input, .tech-textarea {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
border: 1px solid var(--plasteel);
|
||||
color: var(--imperial-gold);
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
font-size: 1rem;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.tech-input:focus, .tech-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--blood-god);
|
||||
box-shadow: 0 0 10px rgba(138, 11, 11, 0.3);
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
@keyframes textFlicker {
|
||||
0%, 95%, 97%, 99%, 100% { opacity: 1; }
|
||||
96% { opacity: 0.8; }
|
||||
98% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
h1, h2 { animation: textFlicker 5s infinite; }
|
||||
|
||||
@media (max-width: 991.98px) {
|
||||
section {
|
||||
padding: 60px 16px !important;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.section-title::before,
|
||||
.section-title::after {
|
||||
margin: 0 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.foto-placeholder {
|
||||
height: 240px;
|
||||
}
|
||||
|
||||
.btn-imperial {
|
||||
padding: 12px 28px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.tech-input,
|
||||
.tech-textarea {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
section {
|
||||
padding: 40px 12px !important;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.section-title::before,
|
||||
.section-title::after {
|
||||
margin: 0 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.foto-placeholder {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.foto-placeholder span {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.foto-caption {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-imperial {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user