From 62cdbef0eb7527040ff77cc1683dabbc084f65d7 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:24:22 +0300 Subject: [PATCH 01/14] some config --- build.gradle | 42 +++++++++++++++++++ src/main/java/fic/writer/Application.java | 11 +++++ .../java/fic/writer/config/AuditConfig.java | 9 ++++ 3 files changed, 62 insertions(+) create mode 100644 build.gradle create mode 100644 src/main/java/fic/writer/Application.java create mode 100644 src/main/java/fic/writer/config/AuditConfig.java diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3d950a6 --- /dev/null +++ b/build.gradle @@ -0,0 +1,42 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.1.3.RELEASE") + } +} + +apply plugin: 'java' +apply plugin: 'idea' +apply plugin: 'org.springframework.boot' +apply plugin: 'io.spring.dependency-management' + +bootJar { + baseName = 'writer-fic' + version = '0.1.0' +} + +repositories { + mavenCentral() +} + +sourceCompatibility = 1.8 +targetCompatibility = 1.8 +compileJava.options.encoding = 'UTF-8' + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} +dependencies { + compile("org.springframework.boot:spring-boot-starter-data-jpa") + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-test") + compile("org.springframework.boot:spring-boot-starter-hateoas") + + compile("org.projectlombok:lombok") + compile("mysql:mysql-connector-java") + compile("com.h2database:h2") + + testCompile("junit:junit") +} \ No newline at end of file diff --git a/src/main/java/fic/writer/Application.java b/src/main/java/fic/writer/Application.java new file mode 100644 index 0000000..7da4aa1 --- /dev/null +++ b/src/main/java/fic/writer/Application.java @@ -0,0 +1,11 @@ +package fic.writer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/src/main/java/fic/writer/config/AuditConfig.java b/src/main/java/fic/writer/config/AuditConfig.java new file mode 100644 index 0000000..d052953 --- /dev/null +++ b/src/main/java/fic/writer/config/AuditConfig.java @@ -0,0 +1,9 @@ +package fic.writer.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class AuditConfig { +} From d442655ee06772390825bed963d57985e249dc0d Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:27:47 +0300 Subject: [PATCH 02/14] Main entities --- .../java/fic/writer/domain/entity/Actor.java | 28 ++++++++++ .../fic/writer/domain/entity/ActorState.java | 25 +++++++++ .../writer/domain/entity/ActorStateId.java | 22 ++++++++ .../fic/writer/domain/entity/Article.java | 35 ++++++++++++ .../java/fic/writer/domain/entity/Book.java | 54 +++++++++++++++++++ .../java/fic/writer/domain/entity/Genre.java | 23 ++++++++ .../java/fic/writer/domain/entity/User.java | 27 ++++++++++ .../writer/domain/entity/auth/CustomUser.java | 22 ++++++++ .../writer/domain/entity/auth/OauthUser.java | 23 ++++++++ .../fic/writer/domain/entity/enums/Size.java | 5 ++ .../fic/writer/domain/entity/enums/State.java | 5 ++ 11 files changed, 269 insertions(+) create mode 100644 src/main/java/fic/writer/domain/entity/Actor.java create mode 100644 src/main/java/fic/writer/domain/entity/ActorState.java create mode 100644 src/main/java/fic/writer/domain/entity/ActorStateId.java create mode 100644 src/main/java/fic/writer/domain/entity/Article.java create mode 100644 src/main/java/fic/writer/domain/entity/Book.java create mode 100644 src/main/java/fic/writer/domain/entity/Genre.java create mode 100644 src/main/java/fic/writer/domain/entity/User.java create mode 100644 src/main/java/fic/writer/domain/entity/auth/CustomUser.java create mode 100644 src/main/java/fic/writer/domain/entity/auth/OauthUser.java create mode 100644 src/main/java/fic/writer/domain/entity/enums/Size.java create mode 100644 src/main/java/fic/writer/domain/entity/enums/State.java diff --git a/src/main/java/fic/writer/domain/entity/Actor.java b/src/main/java/fic/writer/domain/entity/Actor.java new file mode 100644 index 0000000..8c88ef2 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/Actor.java @@ -0,0 +1,28 @@ +package fic.writer.domain.entity; + +import lombok.*; + +import javax.persistence.*; +import java.util.Set; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Actor { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(updatable = false) + private Long id; + private String name; + private String description; + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "actors") + private Set books; + @OneToMany(cascade = {CascadeType.ALL}, + fetch = FetchType.LAZY, + orphanRemoval = true, + mappedBy = "actor") + private Set actorStates; +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/entity/ActorState.java b/src/main/java/fic/writer/domain/entity/ActorState.java new file mode 100644 index 0000000..b3ef976 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/ActorState.java @@ -0,0 +1,25 @@ +package fic.writer.domain.entity; + +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EqualsAndHashCode(of = "id") +public class ActorState { + @EmbeddedId + private ActorStateId id; + @MapsId("articleId") + @ManyToOne(fetch = FetchType.LAZY) + private Article article; + @MapsId("actorId") + @ManyToOne(fetch = FetchType.LAZY) + private Actor actor; + private String title; + private String content; +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/entity/ActorStateId.java b/src/main/java/fic/writer/domain/entity/ActorStateId.java new file mode 100644 index 0000000..43425e0 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/ActorStateId.java @@ -0,0 +1,22 @@ +package fic.writer.domain.entity; + +import lombok.*; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; + +@Embeddable +@NoArgsConstructor +@Getter +@Setter +@Builder +@AllArgsConstructor +@EqualsAndHashCode +public class ActorStateId implements Serializable { + @Column(name = "article_id") + private Long articleId; + @Column(name = "actor_id") + private Long actorId; + +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/entity/Article.java b/src/main/java/fic/writer/domain/entity/Article.java new file mode 100644 index 0000000..5e1f6f1 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/Article.java @@ -0,0 +1,35 @@ +package fic.writer.domain.entity; + +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.*; +import java.util.Date; +import java.util.Set; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@EntityListeners(AuditingEntityListener.class) +public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String title; + @CreatedDate + private Date created; + @LastModifiedDate + private Date lastModify; + @Column(columnDefinition = "text") + private String content; + private String annotation; + @ManyToOne(fetch = FetchType.LAZY) + private Book book; + @OneToMany(cascade = CascadeType.REMOVE) + private Set actorStates; +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/entity/Book.java b/src/main/java/fic/writer/domain/entity/Book.java new file mode 100644 index 0000000..d3ad3e8 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/Book.java @@ -0,0 +1,54 @@ +package fic.writer.domain.entity; + +import fic.writer.domain.entity.enums.Size; +import fic.writer.domain.entity.enums.State; +import lombok.*; + +import javax.persistence.*; +import java.util.Set; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Book { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String title; + @ManyToOne + private User author; + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "book_subauthors", + joinColumns = {@JoinColumn(name = "book_id")}, + inverseJoinColumns = {@JoinColumn(name = "user_id")} + ) + @Singular("subAuthors") + private Set subAuthors; + @OneToMany(fetch = FetchType.EAGER) + @Singular("source") + private Set source; + private String description; + @Enumerated + private Size size; + @Enumerated + private State state; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true) + @Singular("articles") + private Set
articles; + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "book_genres", + joinColumns = {@JoinColumn(name = "book_id")}, + inverseJoinColumns = {@JoinColumn(name = "genre_id")} + ) + private Set genres; + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "book_actors", + joinColumns = {@JoinColumn(name = "book_id")}, + inverseJoinColumns = {@JoinColumn(name = "actor_id")} + ) + @Singular("actors") + private Set actors; +} diff --git a/src/main/java/fic/writer/domain/entity/Genre.java b/src/main/java/fic/writer/domain/entity/Genre.java new file mode 100644 index 0000000..9e14053 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/Genre.java @@ -0,0 +1,23 @@ +package fic.writer.domain.entity; + +import lombok.*; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import java.util.Set; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Genre { + @Id + private Long id; + private String name; + @ManyToMany(mappedBy = "genres", fetch = FetchType.LAZY) + private Set book; +} diff --git a/src/main/java/fic/writer/domain/entity/User.java b/src/main/java/fic/writer/domain/entity/User.java new file mode 100644 index 0000000..d84375a --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/User.java @@ -0,0 +1,27 @@ +package fic.writer.domain.entity; + +import lombok.*; + +import javax.persistence.*; +import java.util.Set; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String username; + private String about; + private String information; + @ManyToMany(mappedBy = "subAuthors", fetch = FetchType.LAZY) + @Singular("booksAsSubAuthor") + private Set booksAsSubAuthor; + @OneToMany(fetch = FetchType.LAZY) + @Singular("booksAsAuthor") + private Set booksAsAuthor; +} diff --git a/src/main/java/fic/writer/domain/entity/auth/CustomUser.java b/src/main/java/fic/writer/domain/entity/auth/CustomUser.java new file mode 100644 index 0000000..22f2961 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/auth/CustomUser.java @@ -0,0 +1,22 @@ +package fic.writer.domain.entity.auth; + +import fic.writer.domain.entity.User; +import lombok.*; + +import javax.persistence.*; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CustomUser { + @Id + @GeneratedValue + private Long id; + @OneToOne(fetch = FetchType.EAGER) + private User profile; + private String email; + private String password; +} diff --git a/src/main/java/fic/writer/domain/entity/auth/OauthUser.java b/src/main/java/fic/writer/domain/entity/auth/OauthUser.java new file mode 100644 index 0000000..d9c38b4 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/auth/OauthUser.java @@ -0,0 +1,23 @@ +package fic.writer.domain.entity.auth; + +import fic.writer.domain.entity.User; +import lombok.*; + +import javax.persistence.*; +import java.util.Date; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class OauthUser { + @Id + @GeneratedValue + private Long id; + @OneToOne(fetch = FetchType.EAGER) + private User profile; + private String token; + private Date expireDate; +} diff --git a/src/main/java/fic/writer/domain/entity/enums/Size.java b/src/main/java/fic/writer/domain/entity/enums/Size.java new file mode 100644 index 0000000..1b3b0a0 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/enums/Size.java @@ -0,0 +1,5 @@ +package fic.writer.domain.entity.enums; + +public enum Size { + MINI, MEDIUM, LONG +} diff --git a/src/main/java/fic/writer/domain/entity/enums/State.java b/src/main/java/fic/writer/domain/entity/enums/State.java new file mode 100644 index 0000000..318ac06 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/enums/State.java @@ -0,0 +1,5 @@ +package fic.writer.domain.entity.enums; + +public enum State { + FROZEN, IN_PROGRESS, DONE +} From ead01962014a2f795bd3f08c6d3be92b57a8276d Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:29:02 +0300 Subject: [PATCH 03/14] Create dto for entities --- .../writer/domain/entity/dto/ActorDto.java | 24 ++++++++++++++++ .../domain/entity/dto/ActorStateDto.java | 23 +++++++++++++++ .../writer/domain/entity/dto/ArticleDto.java | 23 +++++++++++++++ .../fic/writer/domain/entity/dto/BookDto.java | 28 +++++++++++++++++++ .../fic/writer/domain/entity/dto/UserDto.java | 23 +++++++++++++++ 5 files changed, 121 insertions(+) create mode 100644 src/main/java/fic/writer/domain/entity/dto/ActorDto.java create mode 100644 src/main/java/fic/writer/domain/entity/dto/ActorStateDto.java create mode 100644 src/main/java/fic/writer/domain/entity/dto/ArticleDto.java create mode 100644 src/main/java/fic/writer/domain/entity/dto/BookDto.java create mode 100644 src/main/java/fic/writer/domain/entity/dto/UserDto.java diff --git a/src/main/java/fic/writer/domain/entity/dto/ActorDto.java b/src/main/java/fic/writer/domain/entity/dto/ActorDto.java new file mode 100644 index 0000000..d8272e7 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/dto/ActorDto.java @@ -0,0 +1,24 @@ +package fic.writer.domain.entity.dto; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.ActorState; +import lombok.Builder; +import lombok.Data; + +import java.util.Set; + +@Builder +@Data +public class ActorDto { + private String name; + private String description; + private Set actorStates; + + public static ActorDto of(Actor actor) { + return builder() + .name(actor.getName()) + .description(actor.getDescription()) + .actorStates(actor.getActorStates()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/entity/dto/ActorStateDto.java b/src/main/java/fic/writer/domain/entity/dto/ActorStateDto.java new file mode 100644 index 0000000..9c38538 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/dto/ActorStateDto.java @@ -0,0 +1,23 @@ +package fic.writer.domain.entity.dto; + +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class ActorStateDto { + private ActorStateId id; + private String title; + private String content; + + + public static ActorStateDto of(ActorState actorState) { + return builder() + .id(actorState.getId()) + .title(actorState.getTitle()) + .content(actorState.getContent()) + .build(); + } +} diff --git a/src/main/java/fic/writer/domain/entity/dto/ArticleDto.java b/src/main/java/fic/writer/domain/entity/dto/ArticleDto.java new file mode 100644 index 0000000..4bb8458 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/dto/ArticleDto.java @@ -0,0 +1,23 @@ +package fic.writer.domain.entity.dto; + +import fic.writer.domain.entity.Article; +import lombok.*; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class ArticleDto { + private String title; + private String content; + private String annotation; + + public static ArticleDto of(Article article) { + return builder() + .title(article.getTitle()) + .content(article.getContent()) + .annotation(article.getAnnotation()) + .build(); + } +} diff --git a/src/main/java/fic/writer/domain/entity/dto/BookDto.java b/src/main/java/fic/writer/domain/entity/dto/BookDto.java new file mode 100644 index 0000000..ba254c0 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/dto/BookDto.java @@ -0,0 +1,28 @@ +package fic.writer.domain.entity.dto; + +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.enums.Size; +import fic.writer.domain.entity.enums.State; +import lombok.*; + + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class BookDto { + private String title; + private String description; + private Size size; + private State state; + + public static BookDto of(Book book) { + return builder() + .title(book.getTitle()) + .description(book.getDescription()) + .size(book.getSize()) + .state(book.getState()) + .build(); + } +} diff --git a/src/main/java/fic/writer/domain/entity/dto/UserDto.java b/src/main/java/fic/writer/domain/entity/dto/UserDto.java new file mode 100644 index 0000000..716a430 --- /dev/null +++ b/src/main/java/fic/writer/domain/entity/dto/UserDto.java @@ -0,0 +1,23 @@ +package fic.writer.domain.entity.dto; + +import fic.writer.domain.entity.User; +import lombok.*; + +@Builder +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class UserDto { + private String username; + private String about; + private String information; + + public static UserDto of(User user) { + return builder() + .username(user.getUsername()) + .about(user.getAbout()) + .information(user.getInformation()) + .build(); + } +} From e799f71b172b863f76ae62e48f77f5688e4bab4b Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:32:19 +0300 Subject: [PATCH 04/14] Add config to db in properties --- src/main/resources/application-db-mysql.yml | 17 +++ src/main/resources/application.yml | 9 ++ src/main/resources/data/actor.sql | 10 ++ src/main/resources/data/actor_state.sql | 7 + src/main/resources/data/article.sql | 146 ++++++++++++++++++++ src/main/resources/data/book.sql | 11 ++ src/main/resources/data/book_article.sql | 2 + src/main/resources/data/user.sql | 2 + 8 files changed, 204 insertions(+) create mode 100644 src/main/resources/application-db-mysql.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/data/actor.sql create mode 100644 src/main/resources/data/actor_state.sql create mode 100644 src/main/resources/data/article.sql create mode 100644 src/main/resources/data/book.sql create mode 100644 src/main/resources/data/book_article.sql create mode 100644 src/main/resources/data/user.sql diff --git a/src/main/resources/application-db-mysql.yml b/src/main/resources/application-db-mysql.yml new file mode 100644 index 0000000..c27a72f --- /dev/null +++ b/src/main/resources/application-db-mysql.yml @@ -0,0 +1,17 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/ficwriter + username: dl + password: p@ssword + initialization-mode: always + + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL5InnoDBDialect + hbm2ddl: + import_files_sql_extractor: org.hibernate.tool.hbm2ddl.MultipleLinesSqlCommandExtractor + import_files: data/user.sql, data/actor.sql, data/book.sql, data/article.sql,data/book_article.sql, data/actor_state.sql + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..596c249 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,9 @@ +spring: + profiles: + active: db-mysql +logging: + level: + org: + springframework: INFO +server: + port: 8080 \ No newline at end of file diff --git a/src/main/resources/data/actor.sql b/src/main/resources/data/actor.sql new file mode 100644 index 0000000..bb9597c --- /dev/null +++ b/src/main/resources/data/actor.sql @@ -0,0 +1,10 @@ +INSERT INTO actor(id,description,name)VALUES(1,'description1','name1'); +INSERT INTO actor(id,description,name)VALUES(2,'description2','name2'); +INSERT INTO actor(id,description,name)VALUES(3,'description3','name3'); +INSERT INTO actor(id,description,name)VALUES(987,'description','name'); +INSERT INTO actor(id,description,name)VALUES(333,'description','name'); +INSERT INTO actor(id,description,name)VALUES(334,'description','name'); + + + + diff --git a/src/main/resources/data/actor_state.sql b/src/main/resources/data/actor_state.sql new file mode 100644 index 0000000..6e99aca --- /dev/null +++ b/src/main/resources/data/actor_state.sql @@ -0,0 +1,7 @@ +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content1','title1',1,1); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content2','title2',1,2); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content3','title3',2,1); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',2,2); + +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',333,3); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',334,3); \ No newline at end of file diff --git a/src/main/resources/data/article.sql b/src/main/resources/data/article.sql new file mode 100644 index 0000000..1572fba --- /dev/null +++ b/src/main/resources/data/article.sql @@ -0,0 +1,146 @@ +INSERT INTO article(id,annotation,content,title,book_id)VALUES(1,'Place for annotation','

Place for content.

','Summer inspiration',1); +INSERT INTO article(id,annotation,content,title)VALUES(2,'annotation2','content2','title2'); +INSERT INTO article(id,annotation,content,title)VALUES(3,'annotation','content','title'); +INSERT INTO article(id,annotation,content,title)VALUES(4,'annotation','content','title'); +INSERT INTO article(id,annotation,content,title)VALUES(333,'annotation','content','delete article'); + +INSERT INTO article(id,book_id,annotation,title,content)VALUES(335,1,'','Зимняя поэма', +'

Измеряются грустью и чашками чая
+Безымянные, кроткие, зимние дни.
+Проживаешь мгновения, не замечая,
+Как бесследно теряются в прошлом они.
+
+Покрывает надежды обманчивый иней,
+Запотевшая память подобна окну.
+Растворяется контур реальности синей:
+Над экраном холодным и ясным усну.
+
+Утомлен безучастностью, сентиментален,
+Погружаюсь, запутавшись, в тихую тьму.
+А приснятся весенние язвы проталин
+И снега, равнодушны к концу своему.
+
+И покажется, будто неловко ступаю
+По сулящему гибель лукавому льду,
+Приближаюсь безвольно к незримому краю,
+Безучастно забвения смертного жду.
+
+Обреченность души молодой беспричинна,
+И мечты, и стремления сердца смешны.
+Неизбежно и близко маячит кончина,
+Обещая обители вечной весны.
+
+А былого сомнения ныне убоги,
+Поглотила пучина загробной реки;
+Мысли канули в тени последней дороги.
+Берега колдовской глубины далеки.
+
+Не внимая молитвам, судьба непреклонна,
+И слова, и молчание тщетны, пусты,
+Но зовут перезвоны миров Авалона
+Сквозь слепую завесу могильной черты.
+
+Промелькнуло знамение в зыби туманной,
+Воплощение обетованной земли.
+Кровоточит сознание призрачной раной.
+Рассеченную веру, Харон, исцели!
+
+На морозе растрескалась черная лодка,
+Перевозчик не тронет стремнину веслом.
+Изменилась от боли и страха походка:
+Не устанешь терзаться, скорбеть о былом.
+
+Разрывается грудь от печали железной,
+Утянуло уныние душу на дно.
+Беспредельная пропасть зияющей бездной
+Констатирует факт: умереть суждено.
+
+Принимаю жестокую истину эту.
+Под ногами ни твердь, но крошащийся наст.
+Пересечь пожелавший замерзшую Лету
+Неизбежно бесценное стражу отдаст.
+
+Проседает поверхность, ломаясь и тая,
+Увлекаясь течением мертвой воды,
+И тасует агония льды, как живая,
+И надежды безлики и сердцу чужды.
+
+На сминаемой глади нельзя схорониться,
+Настигает невидимый ужас. Фантом
+Беспощаден. Аида владений граница
+Искаверкала личность в тумане густом.
+
+Просыпаюсь, но кровь под висками грохочет,
+Что увидел в зловещем, пророческом сне.
+И дрожу, возвратившийся первопроходчик,
+Почему темнота ухмыляется мне?
+
+Поднимусь, в запыленное зеркало гляну,
+Не поверю, что нет в волосах седины.
+Удивленный открытому в духе изъяну,
+Осознаю, подобные обречены.
+
+Не бывает беспечна дорога поэта,
+И печаль неотступная бдит за спиной.
+Угнетает суровая истина эта.
+Почему безучастность любезна со мной?
+
+Очарован иллюзией, горечью болен,
+За словами под землю бреду в глубину.
+В лабиринтах блуждаю заброшенных штолен
+И ищу драгоценную правду одну.
+
+И в прорубленных поиском чьим-то пещерах
+Затеряюсь: обманка, сомнения, ложь.
+А в рассветных, неискренних сумерках серых
+На умершего в зеркале старом похож.
+
+Бесполезен желанием я. Одноразов
+И порыв, и поэзии выкрик немой.
+Принимаю стекляшки за груды алмазов.
+Одиссей никогда не вернется домой.
+
+Помню Данте и вечный огонь Одиссею,
+И правдивый, бесплотный, печальный язык.
+Опереться на слабую веру не смею,
+К безысходности мрачной за годы привык.
+
+Не боюсь очевидной угрозы обвала,
+Не боюсь нависающей тяжести гор.
+Равнодушная память, чадя, истлевала.
+Неужели конец однозначен и скор?
+
+Умываюсь, покинув недвижность постели,
+Отвечает насмешкой безмолвие глаз.
+А секунды, минуты, недели летели.
+Поклянусь измениться единственный раз.
+
+И слезами обет приношу перед Богом,
+Не терять ни мгновения, миг не терять.
+На пути разрушения скользком, пологом
+Не могу, покатившись, подняться опять.
+
+Поклонюсь утешающим, строгим иконам,
+И молитва развеет гнетущую тьму.
+И прощенный, служивший делам беззаконным,
+Не надеюсь тщеславно спастись самому.
+
+На восходе сугроб переливчат и розов,
+И затейлив узор приукрасил окно.
+Неужели с уходом кристальных морозов
+Полотно белотканное обречено?
+
+Опалив чистоту, молодое светило
+Прожигает сияющий кров белизны,
+А снежинки в бесплодную грязь превратило.
+Обтекают деревья угрюмы, темны.
+
+Почему разрушения ярость воспета?
+Сожаления вязки, сугубы, остры.
+Разлилась половодьем забвения Лета,
+Разделяя потоком зеркальным миры.
+
+А тлетворная оттепель жизни хлопочет,
+Умирает в смирении зимний покой.
+Вырываются листья из лопнувших почек,
+И весна торжествует в капели слепой.
'); \ No newline at end of file diff --git a/src/main/resources/data/book.sql b/src/main/resources/data/book.sql new file mode 100644 index 0000000..49fd526 --- /dev/null +++ b/src/main/resources/data/book.sql @@ -0,0 +1,11 @@ +INSERT INTO book(id,title,description, size, state,author_id)VALUES(1,'Arabella','Artic monkeys', 1,2,1); +INSERT INTO user_books_as_author (user_id,books_as_author_id)VALUES(1,1); +INSERT INTO book_subauthors (user_id,book_id)VALUES(2,1); +INSERT INTO book(title,description, size, state)VALUES('Old yellow bricks','Artic monkeys', 1,2); +INSERT INTO book(title,description, size, state)VALUES('End of me','Apocalyptica', 0,1); +INSERT INTO book(title,description, size, state)VALUES('Отблеск разочарований','Безнадежности печать...', 0,1); + + + + + diff --git a/src/main/resources/data/book_article.sql b/src/main/resources/data/book_article.sql new file mode 100644 index 0000000..1c61683 --- /dev/null +++ b/src/main/resources/data/book_article.sql @@ -0,0 +1,2 @@ +INSERT INTO book_articles(book_id,articles_id)VALUES(1,335); +INSERT INTO book_articles(book_id,articles_id)VALUES(1,1); \ No newline at end of file diff --git a/src/main/resources/data/user.sql b/src/main/resources/data/user.sql new file mode 100644 index 0000000..deb354d --- /dev/null +++ b/src/main/resources/data/user.sql @@ -0,0 +1,2 @@ +INSERT INTO user(id,about, information, username)VALUES(1,'I am author',' zaraza-takaja@mail.ru','Zaraza takaja'); +INSERT INTO user(id,about, information, username)VALUES(2,'I am author, too',' zaraza-takaja@mail.ru','@uthor'); \ No newline at end of file From dd61e47710ff31fb40d85b33c92e1234cf7d99c7 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:33:00 +0300 Subject: [PATCH 05/14] Add repositories --- .../writer/domain/repository/ActorRepository.java | 7 +++++++ .../domain/repository/ActorStateRepository.java | 15 +++++++++++++++ .../domain/repository/ArticleRepository.java | 10 ++++++++++ .../writer/domain/repository/BookRepository.java | 7 +++++++ .../writer/domain/repository/GenreRepository.java | 7 +++++++ .../writer/domain/repository/UserRepository.java | 10 ++++++++++ 6 files changed, 56 insertions(+) create mode 100644 src/main/java/fic/writer/domain/repository/ActorRepository.java create mode 100644 src/main/java/fic/writer/domain/repository/ActorStateRepository.java create mode 100644 src/main/java/fic/writer/domain/repository/ArticleRepository.java create mode 100644 src/main/java/fic/writer/domain/repository/BookRepository.java create mode 100644 src/main/java/fic/writer/domain/repository/GenreRepository.java create mode 100644 src/main/java/fic/writer/domain/repository/UserRepository.java diff --git a/src/main/java/fic/writer/domain/repository/ActorRepository.java b/src/main/java/fic/writer/domain/repository/ActorRepository.java new file mode 100644 index 0000000..3eb5d55 --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/ActorRepository.java @@ -0,0 +1,7 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.Actor; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActorRepository extends JpaRepository { +} diff --git a/src/main/java/fic/writer/domain/repository/ActorStateRepository.java b/src/main/java/fic/writer/domain/repository/ActorStateRepository.java new file mode 100644 index 0000000..10cb175 --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/ActorStateRepository.java @@ -0,0 +1,15 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface ActorStateRepository extends JpaRepository { + Page findAllByIdActorId(Long actorId, Pageable pageable); + + Optional findAByIdActorIdAndIdArticleId(Long actorId, Long articleId); +} diff --git a/src/main/java/fic/writer/domain/repository/ArticleRepository.java b/src/main/java/fic/writer/domain/repository/ArticleRepository.java new file mode 100644 index 0000000..0456388 --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/ArticleRepository.java @@ -0,0 +1,10 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.Article; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface ArticleRepository extends JpaRepository { + List
findAllByBookId(Long bookId); +} diff --git a/src/main/java/fic/writer/domain/repository/BookRepository.java b/src/main/java/fic/writer/domain/repository/BookRepository.java new file mode 100644 index 0000000..a769dfb --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/BookRepository.java @@ -0,0 +1,7 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.Book; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface BookRepository extends JpaRepository { +} diff --git a/src/main/java/fic/writer/domain/repository/GenreRepository.java b/src/main/java/fic/writer/domain/repository/GenreRepository.java new file mode 100644 index 0000000..fddb9e1 --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/GenreRepository.java @@ -0,0 +1,7 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.Genre; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface GenreRepository extends JpaRepository { +} diff --git a/src/main/java/fic/writer/domain/repository/UserRepository.java b/src/main/java/fic/writer/domain/repository/UserRepository.java new file mode 100644 index 0000000..361fabf --- /dev/null +++ b/src/main/java/fic/writer/domain/repository/UserRepository.java @@ -0,0 +1,10 @@ +package fic.writer.domain.repository; + +import fic.writer.domain.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); +} From 04727574963505a1c7ab3347c0b9c1a04c8b7b84 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:35:26 +0300 Subject: [PATCH 06/14] Add config for tests --- src/test/resources/application-db-h2.yml | 13 +++++++++++++ src/test/resources/application-db-mysql.yml | 12 ++++++++++++ src/test/resources/application.yml | 15 +++++++++++++++ src/test/resources/data/actor.sql | 10 ++++++++++ src/test/resources/data/actor_state.sql | 7 +++++++ src/test/resources/data/article.sql | 12 ++++++++++++ src/test/resources/data/book.sql | 15 +++++++++++++++ src/test/resources/data/book_article.sql | 5 +++++ src/test/resources/data/user.sql | 4 ++++ 9 files changed, 93 insertions(+) create mode 100644 src/test/resources/application-db-h2.yml create mode 100644 src/test/resources/application-db-mysql.yml create mode 100644 src/test/resources/application.yml create mode 100644 src/test/resources/data/actor.sql create mode 100644 src/test/resources/data/actor_state.sql create mode 100644 src/test/resources/data/article.sql create mode 100644 src/test/resources/data/book.sql create mode 100644 src/test/resources/data/book_article.sql create mode 100644 src/test/resources/data/user.sql diff --git a/src/test/resources/application-db-h2.yml b/src/test/resources/application-db-h2.yml new file mode 100644 index 0000000..ced131b --- /dev/null +++ b/src/test/resources/application-db-h2.yml @@ -0,0 +1,13 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + jpa: + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + h2: + console: + enabled: true \ No newline at end of file diff --git a/src/test/resources/application-db-mysql.yml b/src/test/resources/application-db-mysql.yml new file mode 100644 index 0000000..68c4a70 --- /dev/null +++ b/src/test/resources/application-db-mysql.yml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/ficwriter-test + username: dl + password: p@ssword + initialization-mode: always + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + dialect: org.hibernate.dialect.MySQL5InnoDBDialect \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..5828953 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,15 @@ +spring: + profiles: + active: db-mysql + jpa: + hibernate: + ddl-auto: create-drop + properties: + hibernate: + hbm2ddl: + import_files: data/user.sql, data/actor.sql, data/book.sql, data/article.sql, data/book_article.sql, data/actor_state.sql +logging: + level: + org: + springframework: INFO + diff --git a/src/test/resources/data/actor.sql b/src/test/resources/data/actor.sql new file mode 100644 index 0000000..bb9597c --- /dev/null +++ b/src/test/resources/data/actor.sql @@ -0,0 +1,10 @@ +INSERT INTO actor(id,description,name)VALUES(1,'description1','name1'); +INSERT INTO actor(id,description,name)VALUES(2,'description2','name2'); +INSERT INTO actor(id,description,name)VALUES(3,'description3','name3'); +INSERT INTO actor(id,description,name)VALUES(987,'description','name'); +INSERT INTO actor(id,description,name)VALUES(333,'description','name'); +INSERT INTO actor(id,description,name)VALUES(334,'description','name'); + + + + diff --git a/src/test/resources/data/actor_state.sql b/src/test/resources/data/actor_state.sql new file mode 100644 index 0000000..6e99aca --- /dev/null +++ b/src/test/resources/data/actor_state.sql @@ -0,0 +1,7 @@ +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content1','title1',1,1); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content2','title2',1,2); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content3','title3',2,1); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',2,2); + +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',333,3); +INSERT INTO actor_state(content,title,actor_id,article_id)VALUES('content4','title4',334,3); \ No newline at end of file diff --git a/src/test/resources/data/article.sql b/src/test/resources/data/article.sql new file mode 100644 index 0000000..3ca4a6e --- /dev/null +++ b/src/test/resources/data/article.sql @@ -0,0 +1,12 @@ +INSERT INTO article(id,annotation,content,title)VALUES(1,'annotation1','content1','title1'); +INSERT INTO article(id,annotation,content,title)VALUES(2,'annotation2','content2','title2'); +INSERT INTO article(id,annotation,content,title)VALUES(3,'annotation','content','title'); +INSERT INTO article(id,annotation,content,title)VALUES(4,'annotation','content','title'); +INSERT INTO article(id,annotation,content,title)VALUES(333,'annotation','content','delete article'); +INSERT INTO article(id,annotation,content,title)VALUES(334,'annotation','content','delete article'); + +INSERT INTO article(id,annotation,content,title,book_id)VALUES(5,'Place for annotation','

Place for content.

','Summer inspiration',335); +INSERT INTO article(id,annotation,content,title,book_id)VALUES(6,'Place for annotation','

Place for content.

','Summer inspiration#2',335); +INSERT INTO article(id,annotation,content,title,book_id)VALUES(7,'Place for annotation','

Place for content.

','Summer #3',335); +INSERT INTO article(id,annotation,content,title,book_id)VALUES(8,'Place for annotation','

Place for content.

','Winrte',335); + diff --git a/src/test/resources/data/book.sql b/src/test/resources/data/book.sql new file mode 100644 index 0000000..b3adcdc --- /dev/null +++ b/src/test/resources/data/book.sql @@ -0,0 +1,15 @@ +INSERT INTO book(id,title)VALUES(1,'book title'); +INSERT INTO book(id,title,description)VALUES(2,'book title','description'); +INSERT INTO book(id,title,size)VALUES(3,'book title',1); +INSERT INTO book(id,title,state)VALUES(4,'book title',1); +INSERT INTO book(id,title)VALUES(5,'book title'); +INSERT INTO book(id,title)VALUES(333,'delete book'); +INSERT INTO book(id,title)VALUES(334,'Книга для удаления'); + +INSERT INTO book(id,title,description, size, state,author_id)VALUES(335,'Arabella','Artic monkeys', 1,2,3); +INSERT INTO user_books_as_author (user_id,books_as_author_id)VALUES(3,335); +INSERT INTO book_subauthors (user_id,book_id)VALUES(4,335); + + + + diff --git a/src/test/resources/data/book_article.sql b/src/test/resources/data/book_article.sql new file mode 100644 index 0000000..7cf2f23 --- /dev/null +++ b/src/test/resources/data/book_article.sql @@ -0,0 +1,5 @@ +INSERT INTO book_articles(book_id,articles_id)VALUES(334,334); +INSERT INTO book_articles(book_id,articles_id)VALUES(335,5); +INSERT INTO book_articles(book_id,articles_id)VALUES(335,6); +INSERT INTO book_articles(book_id,articles_id)VALUES(335,7); +INSERT INTO book_articles(book_id,articles_id)VALUES(335,8); \ No newline at end of file diff --git a/src/test/resources/data/user.sql b/src/test/resources/data/user.sql new file mode 100644 index 0000000..dd5d16b --- /dev/null +++ b/src/test/resources/data/user.sql @@ -0,0 +1,4 @@ +INSERT INTO user(id,username)VALUES(123,'delete user'); +INSERT INTO user(id,username)VALUES(1,'test user'); +INSERT INTO user(id,username)VALUES(3,'Bella'); +INSERT INTO user(id,username)VALUES(4,'Bella junior'); \ No newline at end of file From 2e4c135b23c3331d3c33f3b4d928bec368965f45 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:37:06 +0300 Subject: [PATCH 07/14] Create abstract services --- .../writer/domain/service/ActorService.java | 25 ++++++++++++++ .../domain/service/ActorStateService.java | 25 ++++++++++++++ .../writer/domain/service/ArticleService.java | 25 ++++++++++++++ .../writer/domain/service/BookService.java | 34 +++++++++++++++++++ .../writer/domain/service/CrudService.java | 21 ++++++++++++ .../writer/domain/service/GenreService.java | 7 ++++ .../writer/domain/service/UserService.java | 27 +++++++++++++++ 7 files changed, 164 insertions(+) create mode 100644 src/main/java/fic/writer/domain/service/ActorService.java create mode 100644 src/main/java/fic/writer/domain/service/ActorStateService.java create mode 100644 src/main/java/fic/writer/domain/service/ArticleService.java create mode 100644 src/main/java/fic/writer/domain/service/BookService.java create mode 100644 src/main/java/fic/writer/domain/service/CrudService.java create mode 100644 src/main/java/fic/writer/domain/service/GenreService.java create mode 100644 src/main/java/fic/writer/domain/service/UserService.java diff --git a/src/main/java/fic/writer/domain/service/ActorService.java b/src/main/java/fic/writer/domain/service/ActorService.java new file mode 100644 index 0000000..e39043f --- /dev/null +++ b/src/main/java/fic/writer/domain/service/ActorService.java @@ -0,0 +1,25 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.dto.ActorDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface ActorService { + Actor getOne(Long id); + + Actor create(ActorDto actor); + + List findAll(); + + Page findPage(Pageable pageable); + + Optional findById(Long id); + + Actor update(Long id, ActorDto actor); + + void deleteById(Long aLong); +} diff --git a/src/main/java/fic/writer/domain/service/ActorStateService.java b/src/main/java/fic/writer/domain/service/ActorStateService.java new file mode 100644 index 0000000..f3988bf --- /dev/null +++ b/src/main/java/fic/writer/domain/service/ActorStateService.java @@ -0,0 +1,25 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface ActorStateService { + Page findAllByActor(Long id, Pageable pageable); + + Optional findForActorByArticle(Long actorId, Long articleId); + + List findAll(); + + Page findPage(Pageable pageable); + + Optional findById(ActorStateId actorStateId); + + void delete(ActorState actorState); + + void deleteById(ActorStateId actorStateId); +} diff --git a/src/main/java/fic/writer/domain/service/ArticleService.java b/src/main/java/fic/writer/domain/service/ArticleService.java new file mode 100644 index 0000000..8129e9a --- /dev/null +++ b/src/main/java/fic/writer/domain/service/ArticleService.java @@ -0,0 +1,25 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.dto.ArticleDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface ArticleService { + List

findAll(); + + List
findAllForBook(Long bookId); + + Page
findPage(Pageable pageable); + + Article update(Long id, ArticleDto articleDto); + + Optional
findById(Long aLong); + + void delete(Article article); + + void deleteById(Long aLong); +} diff --git a/src/main/java/fic/writer/domain/service/BookService.java b/src/main/java/fic/writer/domain/service/BookService.java new file mode 100644 index 0000000..49a027d --- /dev/null +++ b/src/main/java/fic/writer/domain/service/BookService.java @@ -0,0 +1,34 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.dto.ArticleDto; +import fic.writer.domain.entity.dto.BookDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +public interface BookService { + + List findAll(); + + Page findPage(Pageable pageable); + + Optional findById(Long aLong); + + Book create(BookDto bookDto); + + Book update(Long id, BookDto bookDto); + + Book addArticle(Long bookId, ArticleDto articleDto); + + Book removeArticle(Long bookId, Long articleId); + + void delete(Book book); + + void deleteById(Long aLong); + + byte[] getBookAsByteArray(Long bookId) throws IOException; +} diff --git a/src/main/java/fic/writer/domain/service/CrudService.java b/src/main/java/fic/writer/domain/service/CrudService.java new file mode 100644 index 0000000..7e5739f --- /dev/null +++ b/src/main/java/fic/writer/domain/service/CrudService.java @@ -0,0 +1,21 @@ +package fic.writer.domain.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface CrudService { + List findAll(); + + Page findPage(Pageable pageable); + + Optional findById(ID id); + + T save(T t); + + void delete(T t); + + void deleteById(ID id); +} diff --git a/src/main/java/fic/writer/domain/service/GenreService.java b/src/main/java/fic/writer/domain/service/GenreService.java new file mode 100644 index 0000000..9292154 --- /dev/null +++ b/src/main/java/fic/writer/domain/service/GenreService.java @@ -0,0 +1,7 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Genre; + +public interface GenreService extends CrudService { + +} diff --git a/src/main/java/fic/writer/domain/service/UserService.java b/src/main/java/fic/writer/domain/service/UserService.java new file mode 100644 index 0000000..5df0850 --- /dev/null +++ b/src/main/java/fic/writer/domain/service/UserService.java @@ -0,0 +1,27 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.User; +import fic.writer.domain.entity.dto.UserDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.Optional; + +public interface UserService { + List findAll(); + + Page findPage(Pageable pageable); + + Optional findById(Long aLong); + + Optional findByUsername(String username); + + User create(UserDto user); + + User update(Long userId, UserDto user); + + void delete(User user); + + void deleteById(Long aLong); +} From 39d68b0061decc9fce13fd201f702bd9ff6283d5 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:38:41 +0300 Subject: [PATCH 08/14] Create tests for services --- .../ActorAndActorStateServicesTest.java | 105 +++++++++++++++ .../domain/service/ActorServiceTest.java | 59 +++++++++ .../domain/service/ActorStateServiceTest.java | 125 ++++++++++++++++++ .../domain/service/ArticleServiceTest.java | 54 ++++++++ .../service/BookAndArticleServicesTest.java | 56 ++++++++ .../domain/service/BookServiceTest.java | 69 ++++++++++ .../domain/service/UserServiceTest.java | 42 ++++++ 7 files changed, 510 insertions(+) create mode 100644 src/test/java/fic/writer/domain/service/ActorAndActorStateServicesTest.java create mode 100644 src/test/java/fic/writer/domain/service/ActorServiceTest.java create mode 100644 src/test/java/fic/writer/domain/service/ActorStateServiceTest.java create mode 100644 src/test/java/fic/writer/domain/service/ArticleServiceTest.java create mode 100644 src/test/java/fic/writer/domain/service/BookAndArticleServicesTest.java create mode 100644 src/test/java/fic/writer/domain/service/BookServiceTest.java create mode 100644 src/test/java/fic/writer/domain/service/UserServiceTest.java diff --git a/src/test/java/fic/writer/domain/service/ActorAndActorStateServicesTest.java b/src/test/java/fic/writer/domain/service/ActorAndActorStateServicesTest.java new file mode 100644 index 0000000..4c36603 --- /dev/null +++ b/src/test/java/fic/writer/domain/service/ActorAndActorStateServicesTest.java @@ -0,0 +1,105 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.dto.ActorDto; +import org.hibernate.Session; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.util.Set; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Transactional +public class ActorAndActorStateServicesTest { + @Autowired + private ActorService actorService; + @Autowired + private ActorStateService actorStateService; + @Autowired + private EntityManager entityManager; + + @Test + public void updateActorWithActorStates_whenCorrect_shouldUpdateInActorState() { + final Long ACTOR_ID = 333L; + final Long ARTICLE_ID = 3L; + final String NEW_ACTOR_NAME = "new name"; + Actor actor = actorService.findById(ACTOR_ID).get(); + ActorStateId actorStateId = ActorStateId.builder() + .actorId(ACTOR_ID) + .articleId(ARTICLE_ID) + .build(); + actor.setName(NEW_ACTOR_NAME); + actorService.update(ACTOR_ID, ActorDto.of(actor)); + + ActorState actorState = actorStateService.findById(actorStateId).get(); + + assertEquals(NEW_ACTOR_NAME, actorState.getActor().getName()); + } + + private Session getSession() { + return entityManager.unwrap(Session.class); + } + + @Test + public void deleteActorState_whenCorrect_shouldCleanInActor() { + final Long ACTOR_ID = 333L; + final Long ARTICLE_ID = 3L; + Session session = getSession(); + ActorStateId actorStateId = ActorStateId.builder() + .actorId(ACTOR_ID) + .articleId(ARTICLE_ID) + .build(); + + actorStateService.deleteById(actorStateId); + session.flush(); + Actor actor = actorService.findById(ACTOR_ID).get(); + + Boolean isStateInActor = actor.getActorStates().stream().anyMatch(as -> as.getId().equals(actorStateId)); + assertFalse(isStateInActor); + } + + + @Test + public void deleteActor_whenCorrect_shouldCleanActorStates() { + final Long ACTOR_ID = 334L; + + Actor actor = actorService.findById(ACTOR_ID).get(); + + actorService.deleteById(ACTOR_ID); + + assertFalse(actorStateService.findAll().stream().anyMatch(as -> as.getActor().equals(actor))); + } + + @Test + public void createActorState_whenCorrect_shouldCascadeAddInActor() { + final Long ACTOR_ID = 333L; + final Long ARTICLE_ID = 1L; + final int EXPECTED_SIZE = 2; + Article article = Article.builder().id(ARTICLE_ID).build(); + Actor actor = actorService.findById(ACTOR_ID).get(); + ActorState actorState = ActorState.builder() + .id(ActorStateId.builder().actorId(ACTOR_ID).articleId(ARTICLE_ID).build()) + .actor(actor) + .article(article) + .title("title") + .build(); + Set actorStates = actor.getActorStates(); + actorStates.add(actorState); + actor.setActorStates(actorStates); + actorService.update(ACTOR_ID, ActorDto.of(actor)); + int changedSize = actorService.findById(ACTOR_ID).get().getActorStates().size(); + assertEquals(EXPECTED_SIZE, changedSize); + } +} diff --git a/src/test/java/fic/writer/domain/service/ActorServiceTest.java b/src/test/java/fic/writer/domain/service/ActorServiceTest.java new file mode 100644 index 0000000..85f6754 --- /dev/null +++ b/src/test/java/fic/writer/domain/service/ActorServiceTest.java @@ -0,0 +1,59 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.dto.ActorDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Transactional +public class ActorServiceTest { + @Autowired + private ActorService actorService; + + @Test + public void createActor_whenCorrect_shouldFindWithId() { + Actor actor = Actor.builder().build(); + actor = actorService.create(ActorDto.of(actor)); + + assertTrue(actorService.findById(actor.getId()).isPresent()); + } + + @Test + public void createActor_whenIdExists_shouldFindUpdate() { + final Long ACTOR_ID = 1L; + final String NEW_NAME = "new name"; + actorService.update(ACTOR_ID, ActorDto.builder().name(NEW_NAME).build()); + Optional updatedActor = actorService.findById(ACTOR_ID); + assertTrue(updatedActor.isPresent()); + assertEquals(NEW_NAME, updatedActor.get().getName()); + } + + @Test + public void deleteActor_whenExists_shouldNotFound() { + final Long AUTHOR_ID = 987L; + assertTrue(actorService.findById(AUTHOR_ID).isPresent()); + actorService.deleteById(AUTHOR_ID); + assertFalse(actorService.findById(AUTHOR_ID).isPresent()); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void deleteActor_whenNotExists_shouldThrowException() { + final Long AUTHOR_ID = -1L; + + assertFalse(actorService.findById(AUTHOR_ID).isPresent()); + actorService.deleteById(AUTHOR_ID); + } + + +} \ No newline at end of file diff --git a/src/test/java/fic/writer/domain/service/ActorStateServiceTest.java b/src/test/java/fic/writer/domain/service/ActorStateServiceTest.java new file mode 100644 index 0000000..419ba1a --- /dev/null +++ b/src/test/java/fic/writer/domain/service/ActorStateServiceTest.java @@ -0,0 +1,125 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.dto.ActorDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Transactional +public class ActorStateServiceTest { + @Autowired + private ActorStateService actorStateService; + @Autowired + private ActorService actorService; + + @Test + public void createActorState_whenCorrect_shouldFindWithId() { + final Long ARTICLE_ID = 3L; + final Long ACTOR_ID = 3L; + Actor actor = actorService.findById(ACTOR_ID).get(); + ActorState actorState = buildActorState(actor, ARTICLE_ID); + actor.getActorStates().add(actorState); + + actor = actorService.update(ACTOR_ID, ActorDto.of(actor)); + assertTrue(actorStateService.findById(actorState.getId()).isPresent()); + } + + private ActorState buildActorState(Actor actor, Long articleId) { + Article article = Article.builder().id(articleId).build(); + ActorStateId actorStateId = ActorStateId.builder().articleId(articleId).actorId(actor.getId()).build(); + return ActorState.builder() + .id(actorStateId) + .actor(actor) + .article(article) + .build(); + } + + private ActorState buildActorState(Long actorId, Long articleId) { + Article article = Article.builder().id(articleId).build(); + Actor actor = actorService.findById(actorId).get(); + ActorStateId actorStateId = ActorStateId.builder().articleId(articleId).actorId(actor.getId()).build(); + return ActorState.builder() + .id(actorStateId) + .actor(actor) + .article(article) + .build(); + } + + @Test + public void findActorStateByArticle_whenCorrect_shouldExist() { + assertTrue(actorStateService.findForActorByArticle(1L, 1L).isPresent()); + } + + @Test + public void findActorStateByActor_whenCorrect_shouldExist() { + Pageable pageable = new PageRequest(0, 10); + assertNotEquals(0, actorStateService.findAllByActor(1L, pageable).getTotalElements()); + } + + @Test + public void findActorStateByActor_whenActorNotExist_shouldReturnEmptyPage() { + Pageable pageable = new PageRequest(0, 10); + assertEquals(0, actorStateService.findAllByActor(-1L, pageable).getTotalElements()); + } + + @Test + public void findActorStateByArticle_whenArticleNotExists_shouldExist() { + assertFalse(actorStateService.findForActorByArticle(1L, -1L).isPresent()); + } + + @Test + public void createActorState_whenAlreadyExist_shouldCreateId() { + final Long ARTICLE_ID = 1L; + final Long ACTOR_ID = 1L; + + Actor actor = actorService.findById(ACTOR_ID).get(); + ActorState actorState = buildActorState(ACTOR_ID, ARTICLE_ID); + actor.getActorStates().add(actorState); + actorService.update(ACTOR_ID, ActorDto.of(actor)); + + assertNotNull(actorStateService.findForActorByArticle(ACTOR_ID, ARTICLE_ID)); + } + + @Test + public void deleteActorState_whenCorrect_shouldNotFoundAfterDelete() { + final Long ARTICLE_ID = 2L; + final Long ACTOR_ID = 2L; + ActorStateId actorStateId = ActorStateId.builder().actorId(ACTOR_ID).articleId(ARTICLE_ID).build(); + + Optional actorState = actorStateService.findById(actorStateId); + assertTrue(actorState.isPresent()); + + actorStateService.delete(actorState.get()); + + assertFalse(actorStateService.findById(actorStateId).isPresent()); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void deleteActorState_whenIdNotExist_should() { + final Long ARTICLE_ID = 2L; + final Long ACTOR_ID = -2L; + + ActorStateId actorStateId = ActorStateId.builder() + .actorId(ACTOR_ID) + .articleId(ARTICLE_ID) + .build(); + + actorStateService.deleteById(actorStateId); + } +} \ No newline at end of file diff --git a/src/test/java/fic/writer/domain/service/ArticleServiceTest.java b/src/test/java/fic/writer/domain/service/ArticleServiceTest.java new file mode 100644 index 0000000..9c52f6f --- /dev/null +++ b/src/test/java/fic/writer/domain/service/ArticleServiceTest.java @@ -0,0 +1,54 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.dto.ArticleDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Date; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@EnableJpaAuditing +public class ArticleServiceTest { + @Autowired + private ArticleService articleService; + + + @Test + public void updateArticle_whenUpdateTitle_shouldChangeTitle() { + final Long ARTICLE_ID = 3L; + final String NEW_TITLE = "new title"; + + ArticleDto articleDto = ArticleDto.builder().title(NEW_TITLE).build(); + articleService.update(ARTICLE_ID, articleDto); + Article article = articleService.findById(ARTICLE_ID).get(); + assertEquals(NEW_TITLE, article.getTitle()); + } + + @Test + public void updateArticle_whenUpdateTitle_shouldChangeUpdateDate() { + final Long ARTICLE_ID = 4L; + final String NEW_TITLE = "new title"; + Date prevUpdateDate = articleService.findById(ARTICLE_ID).get().getLastModify(); + + ArticleDto articleDto = ArticleDto.builder().title(NEW_TITLE).build(); + articleService.update(ARTICLE_ID, articleDto); + Article article = articleService.findById(ARTICLE_ID).get(); + assertNotEquals(prevUpdateDate, article.getLastModify()); + } + + @Test + public void deleteArticle_whenExist_shouldNotFoundById() { + final Long ARTICLE_ID = 333L; + articleService.deleteById(ARTICLE_ID); + assertFalse(articleService.findById(ARTICLE_ID).isPresent()); + } + +} \ No newline at end of file diff --git a/src/test/java/fic/writer/domain/service/BookAndArticleServicesTest.java b/src/test/java/fic/writer/domain/service/BookAndArticleServicesTest.java new file mode 100644 index 0000000..3b2b564 --- /dev/null +++ b/src/test/java/fic/writer/domain/service/BookAndArticleServicesTest.java @@ -0,0 +1,56 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.dto.ArticleDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@EnableJpaAuditing +@Transactional +public class BookAndArticleServicesTest { + @Autowired + private ArticleService articleService; + @Autowired + private BookService bookService; + + @Test + public void createArticle_shouldFindByGeneratedId() { + final Long BOOK_ID = 1L; + Article article = Article.builder().build(); + bookService.addArticle(BOOK_ID, ArticleDto.of(article)); + + Book book = bookService.findById(BOOK_ID).get(); + book.getArticles().forEach(art -> assertTrue(articleService.findById(art.getId()).isPresent())); + } + + @Test + public void createArticle_shouldGenerateCreatedDate() { + final Long BOOK_ID = 1L; + Article article = Article.builder().build(); + bookService.addArticle(BOOK_ID, ArticleDto.of(article)); + + Book book = bookService.findById(BOOK_ID).get(); + book.getArticles().forEach(art -> assertNotNull(art.getCreated())); + } + + @Test + public void removeArticle_shouldGenerateCreatedDate() { + final Long BOOK_ID = 334L; + Book book = bookService.findById(BOOK_ID).get(); + + Long articleId = book.getArticles().stream().findFirst().get().getId(); + book = bookService.removeArticle(BOOK_ID, articleId); + + book.getArticles().forEach(art -> assertNotEquals(articleId, art.getId())); + } +} diff --git a/src/test/java/fic/writer/domain/service/BookServiceTest.java b/src/test/java/fic/writer/domain/service/BookServiceTest.java new file mode 100644 index 0000000..279574c --- /dev/null +++ b/src/test/java/fic/writer/domain/service/BookServiceTest.java @@ -0,0 +1,69 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.dto.BookDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class BookServiceTest { + @Autowired + private BookService bookService; + + @Test + public void createBook_shouldChangeCount() { + final int SIZE_BEFORE = bookService.findAll().size(); + Book emptyBook = Book.builder().build(); + bookService.create(BookDto.of(emptyBook)); + + assertNotEquals(SIZE_BEFORE, bookService.findAll().size()); + } + + @Test + public void createBook_whenTitleIsCyrillic_shouldFindByGeneratedId() { + final String TITLE = "Заголовок"; + final long CURRENT_COUNT = 1L; + Book emptyBook = Book.builder().title(TITLE).build(); + bookService.create(BookDto.of(emptyBook)); + + assertEquals(CURRENT_COUNT, bookService.findAll().stream().filter(book -> book.getTitle().equals(TITLE)).count()); + } + + @Test + public void findBook_whenContainSizeEnum_shouldConvertToNotNullSize() { + final Long BOOK_ID = 3L; + Book book = bookService.findById(BOOK_ID).get(); + + assertNotNull(book.getSize()); + } + + @Test + public void findBook_whenContainStateEnum_shouldConvertToNotNullState() { + final Long BOOK_ID = 4L; + Book book = bookService.findById(BOOK_ID).get(); + + assertNotNull(book.getState()); + } + + @Test + public void deletedBook_whenExist_shouldNotFound() { + final Long DELETE_BOOK_ID = 333L; + bookService.deleteById(DELETE_BOOK_ID); + + assertNotNull(bookService.findById(DELETE_BOOK_ID)); + } + + @Test(expected = EmptyResultDataAccessException.class) + public void deletedBook_whenNotExist_shouldThorwException() { + final Long DELETE_BOOK_ID = -1L; + bookService.deleteById(DELETE_BOOK_ID); + } + +} \ No newline at end of file diff --git a/src/test/java/fic/writer/domain/service/UserServiceTest.java b/src/test/java/fic/writer/domain/service/UserServiceTest.java new file mode 100644 index 0000000..2dc5473 --- /dev/null +++ b/src/test/java/fic/writer/domain/service/UserServiceTest.java @@ -0,0 +1,42 @@ +package fic.writer.domain.service; + +import fic.writer.domain.entity.dto.UserDto; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.junit.Assert.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class UserServiceTest { + @Autowired + private UserService userService; + + @Test + public void createUser() { + final String USERNAME = "createTestUser"; + UserDto user = UserDto.builder().username(USERNAME).build(); + userService.create(user); + assertTrue(userService.findAll().stream().anyMatch(u -> u.getUsername().equals(USERNAME))); + } + + @Test + public void deleteUser() { + final Long USER_ID = 123L; + assertTrue(userService.findById(USER_ID).isPresent()); + userService.deleteById(USER_ID); + assertFalse(userService.findById(USER_ID).isPresent()); + } + + @Test + public void updateUser_whenUpdateAbout_shouldChangeAbout() { + final Long USER_ID = 1L; + final String NEW_ABOUT = "new about"; + UserDto userDto = UserDto.builder().about(NEW_ABOUT).build(); + userService.update(USER_ID, userDto); + assertEquals(NEW_ABOUT, userService.findById(USER_ID).get().getAbout()); + } +} \ No newline at end of file From 4ead6e56a7d159b8c10181e1164f6ec43e47fd82 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:40:12 +0300 Subject: [PATCH 09/14] Implements services --- .../domain/service/impl/ActorServiceImpl.java | 75 +++++++++ .../service/impl/ActorStateServiceImpl.java | 70 ++++++++ .../service/impl/ArticleServiceImpl.java | 74 +++++++++ .../domain/service/impl/BookServiceImpl.java | 154 ++++++++++++++++++ .../domain/service/impl/GenreServiceImpl.java | 52 ++++++ .../domain/service/impl/UserServiceImpl.java | 82 ++++++++++ 6 files changed, 507 insertions(+) create mode 100644 src/main/java/fic/writer/domain/service/impl/ActorServiceImpl.java create mode 100644 src/main/java/fic/writer/domain/service/impl/ActorStateServiceImpl.java create mode 100644 src/main/java/fic/writer/domain/service/impl/ArticleServiceImpl.java create mode 100644 src/main/java/fic/writer/domain/service/impl/BookServiceImpl.java create mode 100644 src/main/java/fic/writer/domain/service/impl/GenreServiceImpl.java create mode 100644 src/main/java/fic/writer/domain/service/impl/UserServiceImpl.java diff --git a/src/main/java/fic/writer/domain/service/impl/ActorServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/ActorServiceImpl.java new file mode 100644 index 0000000..c67d57d --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/ActorServiceImpl.java @@ -0,0 +1,75 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.dto.ActorDto; +import fic.writer.domain.repository.ActorRepository; +import fic.writer.domain.service.ActorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityNotFoundException; +import java.util.List; +import java.util.Optional; + +@Service +public class ActorServiceImpl implements ActorService { + private ActorRepository actorRepository; + + @Autowired + public ActorServiceImpl(ActorRepository actorRepository) { + this.actorRepository = actorRepository; + } + + @Override + public List findAll() { + return actorRepository.findAll(); + } + + @Override + public Page findPage(Pageable pageable) { + return actorRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return actorRepository.findById(id); + } + + @Override + public Actor update(Long id, ActorDto actorDto) { + Actor actor = actorRepository.findById(id).orElseThrow(EntityNotFoundException::new); + flushArticleDtoToArticle(actor, actorDto); + return actorRepository.save(actor); + } + + private void flushArticleDtoToArticle(Actor actor, ActorDto actorDto) { + if (actorDto.getName() != null) { + actor.setName(actorDto.getName()); + } + if (actorDto.getActorStates() != null) { + actor.setActorStates(actorDto.getActorStates()); + } + if (actorDto.getDescription() != null) { + actor.setDescription(actorDto.getDescription()); + } + } + + @Override + public Actor getOne(Long id) { + return actorRepository.getOne(id); + } + + @Override + public void deleteById(Long id) { + actorRepository.deleteById(id); + } + + @Override + public Actor create(ActorDto actorDto) { + Actor actor = Actor.builder().build(); + flushArticleDtoToArticle(actor, actorDto); + return actorRepository.save(actor); + } +} diff --git a/src/main/java/fic/writer/domain/service/impl/ActorStateServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/ActorStateServiceImpl.java new file mode 100644 index 0000000..3749d67 --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/ActorStateServiceImpl.java @@ -0,0 +1,70 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.ActorState; +import fic.writer.domain.entity.ActorStateId; +import fic.writer.domain.entity.dto.ActorStateDto; +import fic.writer.domain.repository.ActorRepository; +import fic.writer.domain.repository.ActorStateRepository; +import fic.writer.domain.service.ActorStateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class ActorStateServiceImpl implements ActorStateService { + private ActorStateRepository actorStateRepository; + private ActorRepository actorRepository; + + @Autowired + public ActorStateServiceImpl(ActorStateRepository actorStateRepository) { + this.actorStateRepository = actorStateRepository; + } + + @Override + public List findAll() { + return actorStateRepository.findAll(); + } + + + @Override + public Page findPage(Pageable pageable) { + return actorStateRepository.findAll(pageable); + } + + @Override + public Optional findById(ActorStateId id) { + return actorStateRepository.findById(id); + } + + @Override + public void delete(ActorState actorState) { + actorStateRepository.delete(actorState); + } + + @Override + public void deleteById(ActorStateId id) { + actorStateRepository.deleteById(id); + } + + @Override + public Page findAllByActor(Long id, Pageable pageable) { + return actorStateRepository.findAllByIdActorId(id, pageable); + } + + @Override + public Optional findForActorByArticle(Long actorId, Long articleId) { + return actorStateRepository.findAByIdActorIdAndIdArticleId(actorId, articleId); + } + + private ActorState actorStateDtoForActorState(ActorStateDto actorState) { + return ActorState.builder() + .id(actorState.getId()) + .title(actorState.getTitle()) + .content(actorState.getContent()) + .build(); + } +} diff --git a/src/main/java/fic/writer/domain/service/impl/ArticleServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/ArticleServiceImpl.java new file mode 100644 index 0000000..3ad1f61 --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/ArticleServiceImpl.java @@ -0,0 +1,74 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.dto.ArticleDto; +import fic.writer.domain.repository.ArticleRepository; +import fic.writer.domain.service.ArticleService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityExistsException; +import java.util.List; +import java.util.Optional; + +@Service +public class ArticleServiceImpl implements ArticleService { + private ArticleRepository articleRepository; + + @Autowired + public ArticleServiceImpl(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + @Override + public List
findAll() { + return articleRepository.findAll(); + } + + @Override + public List
findAllForBook(Long bookId) { + return articleRepository.findAllByBookId(bookId); + } + + @Override + public Page
findPage(Pageable pageable) { + return articleRepository.findAll(pageable); + } + + @Override + public Article update(Long id, ArticleDto articleDto) { + Article article = articleRepository.findById(id).orElseThrow(EntityExistsException::new); + flushArticleDtoToArticle(article, articleDto); + return articleRepository.save(article); + } + + @Override + public Optional
findById(Long id) { + return articleRepository.findById(id); + } + + @Override + public void delete(Article article) { + articleRepository.delete(article); + } + + @Override + public void deleteById(Long id) { + articleRepository.deleteById(id); + } + + + private void flushArticleDtoToArticle(Article article, ArticleDto articleDto) { + if (articleDto.getTitle() != null) { + article.setTitle(articleDto.getTitle()); + } + if (articleDto.getAnnotation() != null) { + article.setAnnotation(articleDto.getAnnotation()); + } + if (articleDto.getContent() != null) { + article.setContent(articleDto.getContent()); + } + } +} diff --git a/src/main/java/fic/writer/domain/service/impl/BookServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/BookServiceImpl.java new file mode 100644 index 0000000..8d850e8 --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/BookServiceImpl.java @@ -0,0 +1,154 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.Article; +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.User; +import fic.writer.domain.entity.dto.ArticleDto; +import fic.writer.domain.entity.dto.BookDto; +import fic.writer.domain.repository.BookRepository; +import fic.writer.domain.service.BookService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityListeners; +import javax.persistence.EntityNotFoundException; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@Service +@EntityListeners(AuditingEntityListener.class) +@Transactional +public class BookServiceImpl implements BookService { + private BookRepository bookRepository; + + @Autowired + + public BookServiceImpl(BookRepository bookRepository) { + this.bookRepository = bookRepository; + } + + @Override + public List findAll() { + return bookRepository.findAll(); + } + + + @Override + public Page findPage(Pageable pageable) { + return bookRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return bookRepository.findById(id); + } + + @Override + public Book create(BookDto bookDto) { + Book book = Book.builder().build(); + flushBookDtoToBook(book, bookDto); + return bookRepository.save(book); + } + + @Override + public Book update(Long id, BookDto bookDto) { + Book book = bookRepository.getOne(id); + flushBookDtoToBook(book, bookDto); + return bookRepository.save(book); + } + + @Override + public Book addArticle(Long bookId, ArticleDto articleDto) { + Book book = bookRepository.getOne(bookId); + Article article = Article.builder().build(); + flushArticleDtoToArticle(article, articleDto); + article.setBook(Book.builder().id(bookId).build()); + book.getArticles().add(article); + bookRepository.save(book); + return book; + } + + @Override + public Book removeArticle(Long bookId, Long articleId) { + Book book = bookRepository.getOne(bookId); + book.getArticles().removeIf(article -> article.getId().equals(articleId)); + bookRepository.save(book); + return book; + } + + private void flushArticleDtoToArticle(Article article, ArticleDto articleDto) { + if (articleDto.getTitle() != null) { + article.setTitle(articleDto.getTitle()); + } + if (articleDto.getAnnotation() != null) { + article.setAnnotation(articleDto.getAnnotation()); + } + if (articleDto.getContent() != null) { + article.setContent(articleDto.getContent()); + } + } + + @Override + public void delete(Book book) { + bookRepository.delete(book); + } + + @Override + public void deleteById(Long id) { + bookRepository.deleteById(id); + } + + @Override + public byte[] getBookAsByteArray(Long bookId) throws IOException { + Book book = bookRepository.findById(bookId).orElseThrow(EntityNotFoundException::new); + String output = ""; + output += "Title:" + book.getTitle(); + output += " \nDescription:" + book.getDescription(); + if (book.getAuthor() != null) { + output += " \nAuthor:" + book.getAuthor().getUsername(); + } + String coauthors = book.getSubAuthors().stream().map(User::getUsername).reduce((b, a) -> b + "," + a).orElse(""); + output += " \nCoauthor:" + coauthors; + if (book.getSize() != null) { + output += " \nSize:" + book.getSize().name(); + } + if (book.getSize() != null) { + output += " \nState:" + book.getState().name(); + } + int counter = 0; + String content = " \nContent: \n"; + Set
articles = book.getArticles(); + for (Article article : articles) { + content += " \n" + ++counter + ". " + article.getTitle(); + } + output += content; + + for (Article article : articles) { + output += " \n" + article.getTitle(); + output += " \nAnnotation:" + article.getAnnotation(); + output += " \n" + article.getContent(); + } + return output.getBytes(); + } + + private void flushBookDtoToBook(Book book, BookDto bookDto) { + if (bookDto.getTitle() != null) { + book.setTitle(bookDto.getTitle()); + } + if (bookDto.getDescription() != null) { + book.setDescription(bookDto.getDescription()); + } + if (bookDto.getSize() != null) { + book.setSize(bookDto.getSize()); + } + if (bookDto.getState() != null) { + book.setState(bookDto.getState()); + } + } +} diff --git a/src/main/java/fic/writer/domain/service/impl/GenreServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/GenreServiceImpl.java new file mode 100644 index 0000000..9ff80dd --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/GenreServiceImpl.java @@ -0,0 +1,52 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.Genre; +import fic.writer.domain.repository.GenreRepository; +import fic.writer.domain.service.GenreService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; + +@Service +public class GenreServiceImpl implements GenreService { + private GenreRepository genreRepository; + + @Autowired + public GenreServiceImpl(GenreRepository genreRepository) { + this.genreRepository = genreRepository; + } + + @Override + public List findAll() { + return genreRepository.findAll(); + } + + @Override + public Page findPage(Pageable pageable) { + return genreRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return genreRepository.findById(id); + } + + @Override + public Genre save(Genre genre) { + return genreRepository.save(genre); + } + + @Override + public void delete(Genre genre) { + genreRepository.delete(genre); + } + + @Override + public void deleteById(Long id) { + genreRepository.deleteById(id); + } +} \ No newline at end of file diff --git a/src/main/java/fic/writer/domain/service/impl/UserServiceImpl.java b/src/main/java/fic/writer/domain/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..30dba9e --- /dev/null +++ b/src/main/java/fic/writer/domain/service/impl/UserServiceImpl.java @@ -0,0 +1,82 @@ +package fic.writer.domain.service.impl; + +import fic.writer.domain.entity.User; +import fic.writer.domain.entity.dto.UserDto; +import fic.writer.domain.repository.UserRepository; +import fic.writer.domain.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import javax.persistence.EntityNotFoundException; +import java.util.List; +import java.util.Optional; + +@Service +public class UserServiceImpl implements UserService { + private UserRepository userRepository; + + @Autowired + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public List findAll() { + return userRepository.findAll(); + } + + @Override + public Page findPage(Pageable pageable) { + return userRepository.findAll(pageable); + } + + @Override + public Optional findById(Long id) { + return userRepository.findById(id); + } + + @Override + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + @Override + public User create(UserDto userDto) { + User user = User.builder().build(); + flushUserDtoToUser(user, userDto); + return userRepository.save(user); + + } + + @Override + public User update(Long userId, UserDto userDto) { + User user = userRepository.findById(userId).orElseThrow(EntityNotFoundException::new); + flushUserDtoToUser(user, userDto); + userRepository.save(user); + return user; + } + + private void flushUserDtoToUser(User user, UserDto userDto) { + if (userDto.getUsername() != null) { + user.setUsername(userDto.getUsername()); + } + if (userDto.getAbout() != null) { + user.setAbout(userDto.getAbout()); + } + if (userDto.getInformation() != null) { + user.setInformation(userDto.getInformation()); + } + } + + @Override + public void delete(User user) { + userRepository.delete(user); + } + + @Override + public void deleteById(Long id) { + userRepository.deleteById(id); + } +} From c6543353f9c9cbe202cffa8767dd7a47572f525a Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:41:40 +0300 Subject: [PATCH 10/14] Create responses for entities --- .../writer/web/response/ActorResponse.java | 39 ++++++++++ .../web/response/ActorStateResponse.java | 12 +++ .../writer/web/response/ArticleResponse.java | 37 ++++++++++ .../fic/writer/web/response/BookResponse.java | 74 +++++++++++++++++++ .../fic/writer/web/response/PageResponse.java | 52 +++++++++++++ .../fic/writer/web/response/UserResponse.java | 45 +++++++++++ 6 files changed, 259 insertions(+) create mode 100644 src/main/java/fic/writer/web/response/ActorResponse.java create mode 100644 src/main/java/fic/writer/web/response/ActorStateResponse.java create mode 100644 src/main/java/fic/writer/web/response/ArticleResponse.java create mode 100644 src/main/java/fic/writer/web/response/BookResponse.java create mode 100644 src/main/java/fic/writer/web/response/PageResponse.java create mode 100644 src/main/java/fic/writer/web/response/UserResponse.java diff --git a/src/main/java/fic/writer/web/response/ActorResponse.java b/src/main/java/fic/writer/web/response/ActorResponse.java new file mode 100644 index 0000000..b86ea74 --- /dev/null +++ b/src/main/java/fic/writer/web/response/ActorResponse.java @@ -0,0 +1,39 @@ +package fic.writer.web.response; + +import fic.writer.domain.entity.Actor; +import fic.writer.web.controller.ActorController; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.hateoas.ResourceSupport; + +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class ActorResponse extends ResourceSupport { + private Long actorId; + private String name; + private String description; + private Set books; + private Set actorStates; + + public ActorResponse(Actor actor) { + actorId = actor.getId(); + name = actor.getName(); + description = actor.getDescription(); + books = actor.getBooks().stream().map(BookResponse::new).collect(Collectors.toSet()); + actorStates = actor.getActorStates().stream().map(ActorStateResponse::new).collect(Collectors.toSet()); + addSelfLink(actorId); + } + + + private void addSelfLink(Long id) { + add(linkTo(methodOn(ActorController.class, id).getActorById(id)).withSelfRel()); + } +} diff --git a/src/main/java/fic/writer/web/response/ActorStateResponse.java b/src/main/java/fic/writer/web/response/ActorStateResponse.java new file mode 100644 index 0000000..67a6718 --- /dev/null +++ b/src/main/java/fic/writer/web/response/ActorStateResponse.java @@ -0,0 +1,12 @@ +package fic.writer.web.response; + +import fic.writer.domain.entity.ActorState; +import org.springframework.hateoas.ResourceSupport; + +public class ActorStateResponse extends ResourceSupport { + private ActorState actorState; + + public ActorStateResponse(ActorState actorState) { + this.actorState = actorState; + } +} diff --git a/src/main/java/fic/writer/web/response/ArticleResponse.java b/src/main/java/fic/writer/web/response/ArticleResponse.java new file mode 100644 index 0000000..ca96f1e --- /dev/null +++ b/src/main/java/fic/writer/web/response/ArticleResponse.java @@ -0,0 +1,37 @@ +package fic.writer.web.response; + +import fic.writer.domain.entity.Article; +import fic.writer.web.controller.BookController; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.hateoas.ResourceSupport; + +import java.util.Date; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class ArticleResponse extends ResourceSupport { + private Long articleId; + private String title; + private Date created; + private String content; + private String annotation; + + public ArticleResponse(Article article) { + articleId = article.getId(); + title = article.getTitle(); + created = article.getCreated(); + content = article.getContent(); + annotation = article.getAnnotation(); + addSelfLink(articleId); + } + + private void addSelfLink(Long id) { + add(linkTo(methodOn(BookController.class, id).getBookById(id)).withSelfRel()); + } +} diff --git a/src/main/java/fic/writer/web/response/BookResponse.java b/src/main/java/fic/writer/web/response/BookResponse.java new file mode 100644 index 0000000..5baaa2c --- /dev/null +++ b/src/main/java/fic/writer/web/response/BookResponse.java @@ -0,0 +1,74 @@ +package fic.writer.web.response; + +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.Genre; +import fic.writer.domain.entity.enums.Size; +import fic.writer.domain.entity.enums.State; +import fic.writer.web.controller.ArticleController; +import fic.writer.web.controller.BookController; +import fic.writer.web.controller.UserController; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.ResourceSupport; + +import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class BookResponse extends ResourceSupport { + private Long bookId; + private String title; + private Link author; + private Set subAuthors; + private Set source; + private String description; + private Size size; + private State state; + private Set genres; + private Set actors; + private Link articles; + + public BookResponse(Book book) { + this.bookId = book.getId(); + title = book.getTitle(); + if (book.getAuthor() != null) { + Long authorId = book.getAuthor().getId(); + author = linkTo(methodOn(UserController.class, authorId).getUserById(authorId)).withRel("author"); + } + subAuthors = book.getSubAuthors().stream().map(author -> + linkTo(methodOn(UserController.class, author.getId()).getUserById(author.getId())) + .withRel("subauthor")).collect(Collectors.toSet()); + source = book.getSource().stream().map(BookResponse::new).map(ResourceSupport::getId).collect(Collectors.toSet()); + description = book.getDescription(); + size = book.getSize(); + state = book.getState(); + genres = book.getGenres(); + actors = book.getActors().stream().map(ActorResponse::new).map(ResourceSupport::getId).collect(Collectors.toSet()); + articles = linkTo(methodOn(ArticleController.class, bookId).getAllArticles(bookId)).withRel("articles"); + addSelfLink(bookId); + addDownloadLink((bookId)); + } + + private void addSelfLink(Long id) { + add(linkTo(methodOn(BookController.class, id).getBookById(id)).withSelfRel()); + } + + private void addDownloadLink(Long id) { + + try { + add(linkTo(methodOn(BookController.class, id).downloadBook(id)).withRel("download")); + } catch (IOException e) { + e.printStackTrace(); + } + } + + +} diff --git a/src/main/java/fic/writer/web/response/PageResponse.java b/src/main/java/fic/writer/web/response/PageResponse.java new file mode 100644 index 0000000..3b6e4ce --- /dev/null +++ b/src/main/java/fic/writer/web/response/PageResponse.java @@ -0,0 +1,52 @@ +package fic.writer.web.response; + +import org.springframework.data.domain.Page; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.PagedResources; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +public class PageResponse extends PagedResources { + private Page page; + + public PageResponse(Page page) { + super(page.getContent(), new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages())); + this.page = page; + addSelfLink(); + addPageableLinks(); + } + + private void addPageableLinks() { + int firstPageNumber = 0; + int lastPageNumber = page.getTotalPages() <= 0 ? 0 : page.getTotalPages() - 1; + if (page.getTotalElements() != 0) { + addPageNumberLinkWithRel(firstPageNumber, Link.REL_FIRST); + addPageNumberLinkWithRel(lastPageNumber, Link.REL_LAST); + } + + if (page.hasNext()) { + int nextPageNumber = page.nextPageable().getPageNumber(); + addPageNumberLinkWithRel(nextPageNumber, Link.REL_NEXT); + } + + if (page.hasPrevious()) { + int previousPageNumber = page.previousPageable().getPageNumber(); + addPageNumberLinkWithRel(previousPageNumber, Link.REL_PREVIOUS); + } + } + + private void addSelfLink() { + String path = getCurrentRequestUriBuilder().build().toString(); + Link link = new Link(path, Link.REL_SELF); + add(link); + } + + private void addPageNumberLinkWithRel(Integer number, String rel) { + String path = getCurrentRequestUriBuilder().replaceQueryParam("page", number).build().toString(); + Link link = new Link(path, rel); + add(link); + } + + private ServletUriComponentsBuilder getCurrentRequestUriBuilder() { + return ServletUriComponentsBuilder.fromCurrentRequest(); + } +} diff --git a/src/main/java/fic/writer/web/response/UserResponse.java b/src/main/java/fic/writer/web/response/UserResponse.java new file mode 100644 index 0000000..bc6d942 --- /dev/null +++ b/src/main/java/fic/writer/web/response/UserResponse.java @@ -0,0 +1,45 @@ +package fic.writer.web.response; + +import fic.writer.domain.entity.User; +import fic.writer.web.controller.UserController; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.hateoas.ResourceSupport; + +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; + +@AllArgsConstructor +@NoArgsConstructor +@Getter +public class UserResponse extends ResourceSupport { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long userId; + private String username; + private String about; + private String information; + private Set booksAsSubAuthor; + private Set booksAsAuthor; + + public UserResponse(User user) { + this.userId = user.getId(); + username = user.getUsername(); + about = user.getAbout(); + information = user.getInformation(); + booksAsSubAuthor = user.getBooksAsSubAuthor().stream().map(BookResponse::new).collect(Collectors.toSet()); + booksAsAuthor = user.getBooksAsAuthor().stream().map(BookResponse::new).collect(Collectors.toSet()); + addSelfLink(userId); + } + + private void addSelfLink(Long id) { + add(linkTo(methodOn(UserController.class, id).getUserById(id)).withSelfRel()); + } +} From 8cccb073c5ea6a9e81d1febbc2109ddb19377916 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:42:45 +0300 Subject: [PATCH 11/14] Create rest controllers --- .../web/controller/ActorController.java | 59 ++++++++ .../web/controller/ArticleController.java | 61 ++++++++ .../writer/web/controller/BookController.java | 82 +++++++++++ .../writer/web/controller/UserController.java | 61 ++++++++ .../web/controller/UserControllerTest.java | 133 ++++++++++++++++++ 5 files changed, 396 insertions(+) create mode 100644 src/main/java/fic/writer/web/controller/ActorController.java create mode 100644 src/main/java/fic/writer/web/controller/ArticleController.java create mode 100644 src/main/java/fic/writer/web/controller/BookController.java create mode 100644 src/main/java/fic/writer/web/controller/UserController.java create mode 100644 src/test/java/fic/writer/web/controller/UserControllerTest.java diff --git a/src/main/java/fic/writer/web/controller/ActorController.java b/src/main/java/fic/writer/web/controller/ActorController.java new file mode 100644 index 0000000..0c254b7 --- /dev/null +++ b/src/main/java/fic/writer/web/controller/ActorController.java @@ -0,0 +1,59 @@ +package fic.writer.web.controller; + +import fic.writer.domain.entity.Actor; +import fic.writer.domain.entity.dto.ActorDto; +import fic.writer.domain.service.ActorService; +import fic.writer.web.response.ActorResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.persistence.EntityNotFoundException; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/actors") +public class ActorController { + private static final String ID_TEMPLATE_PATH = "/{actorId}"; + private static final String ID_TEMPLATE = "actorId"; + + private ActorService actorService; + + @Autowired + public ActorController(ActorService actorService) { + this.actorService = actorService; + } + + @GetMapping + public List getAllActors() { + return actorService.findAll().stream() + .map(ActorResponse::new) + .collect(Collectors.toList()); + } + + @GetMapping(ID_TEMPLATE_PATH) + public ActorResponse getActorById(@PathVariable(ID_TEMPLATE) Long id) { + return actorService.findById(id) + .map(ActorResponse::new) + .orElseThrow(EntityNotFoundException::new); + } + + @PostMapping + public ActorResponse createActor(ActorDto actor) { + Actor savedActor = actorService.create(actor); + return new ActorResponse(savedActor); + } + + @PutMapping(ID_TEMPLATE_PATH) + public ActorResponse updateActor(Long id, ActorDto actor) { + Actor savedActor = actorService.update(id, actor); + return new ActorResponse(savedActor); + } + + @DeleteMapping(ID_TEMPLATE_PATH) + public HttpStatus deleteActor(Long id) { + actorService.deleteById(id); + return HttpStatus.NO_CONTENT; + } +} diff --git a/src/main/java/fic/writer/web/controller/ArticleController.java b/src/main/java/fic/writer/web/controller/ArticleController.java new file mode 100644 index 0000000..4016fbb --- /dev/null +++ b/src/main/java/fic/writer/web/controller/ArticleController.java @@ -0,0 +1,61 @@ +package fic.writer.web.controller; + +import fic.writer.domain.entity.dto.ArticleDto; +import fic.writer.domain.service.ArticleService; +import fic.writer.domain.service.BookService; +import fic.writer.web.response.ArticleResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; + +import javax.persistence.EntityNotFoundException; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping(value = "/books/{bookId}/articles", produces = MediaType.APPLICATION_JSON_VALUE) +@CrossOrigin(origins = "http://localhost:3000") +public class ArticleController { + private static final String ID_TEMPLATE_PATH = "/{articleId}"; + private static final String ID_TEMPLATE = "articleId"; + private static final String BOOK_ID_TEMPLATE_PATH = "/{bookId}"; + private static final String BOOK_ID_TEMPLATE = "bookId"; + @Autowired + private ArticleService articleService; + @Autowired + private BookService bookService; + + @GetMapping + public List getAllArticles(@PathVariable(BOOK_ID_TEMPLATE) Long bookId) { + List list = articleService.findAllForBook(bookId).stream().map(ArticleResponse::new).collect(Collectors.toList()); + return list; + } + + @GetMapping(ID_TEMPLATE_PATH) + public ArticleResponse getOneArticle(@PathVariable(BOOK_ID_TEMPLATE) Long bookId, @PathVariable(ID_TEMPLATE) Long articleId) { + return articleService.findAllForBook(bookId).stream(). + filter(article -> article.getId().equals(articleId)) + .map(ArticleResponse::new) + .findFirst() + .orElseThrow(EntityNotFoundException::new); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public void createArticle(@PathVariable(BOOK_ID_TEMPLATE) Long bookId, @RequestBody ArticleDto articleDto) { + bookService.addArticle(bookId, articleDto); + } + + @DeleteMapping(ID_TEMPLATE_PATH) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteArticle(@PathVariable(BOOK_ID_TEMPLATE) Long bookId, @PathVariable(ID_TEMPLATE) Long articleId) { + bookService.removeArticle(bookId, articleId); + } + + @PutMapping(ID_TEMPLATE_PATH) + @ResponseStatus(HttpStatus.OK) + public void updateArticle(@PathVariable(ID_TEMPLATE) Long articleId, @RequestBody ArticleDto articleDto) { + articleService.update(articleId, articleDto); + } +} \ No newline at end of file diff --git a/src/main/java/fic/writer/web/controller/BookController.java b/src/main/java/fic/writer/web/controller/BookController.java new file mode 100644 index 0000000..1969ea6 --- /dev/null +++ b/src/main/java/fic/writer/web/controller/BookController.java @@ -0,0 +1,82 @@ +package fic.writer.web.controller; + +import fic.writer.domain.entity.Book; +import fic.writer.domain.entity.dto.BookDto; +import fic.writer.domain.service.BookService; +import fic.writer.web.response.BookResponse; +import fic.writer.web.response.PageResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.persistence.EntityNotFoundException; +import java.io.IOException; + +@RestController +@RequestMapping(value = "/books", produces = MediaType.APPLICATION_JSON_VALUE) +@CrossOrigin(origins = "http://localhost:3000") +public class BookController { + private static final String ID_TEMPLATE_PATH = "/{bookId}"; + private static final String ID_TEMPLATE = "bookId"; + + private BookService bookService; + + @Autowired + public BookController(BookService bookService) { + this.bookService = bookService; + } + + @GetMapping + public PageResponse getAllBooks(Pageable pageable) { + Page resourcePage = bookService.findPage(pageable).map(BookResponse::new); + return new PageResponse<>(resourcePage); + } + + @GetMapping(ID_TEMPLATE_PATH) + public BookResponse getBookById(@PathVariable(ID_TEMPLATE) Long id) { + return bookService.findById(id) + .map(BookResponse::new) + .orElseThrow(EntityNotFoundException::new); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public BookResponse createBook(@RequestBody BookDto book) { + Book savedBook = bookService.create(book); + return new BookResponse(savedBook); + } + + @PutMapping(ID_TEMPLATE_PATH) + public BookResponse updateBook(@PathVariable(ID_TEMPLATE) Long id, @RequestBody BookDto book) { + Book savedBook = bookService.update(id, book); + return new BookResponse(savedBook); + } + + @DeleteMapping(ID_TEMPLATE_PATH) + public HttpStatus deleteBook(Long id) { + bookService.deleteById(id); + return HttpStatus.NO_CONTENT; + } + + @GetMapping(ID_TEMPLATE_PATH + "/download") + public ResponseEntity downloadBook(@PathVariable(ID_TEMPLATE) Long id) throws IOException { + byte[] bytes = bookService.getBookAsByteArray(id); + Book book = bookService.findById(id).get(); + + ByteArrayResource resource = new ByteArrayResource(bytes); + MediaType mediaType = MediaType.TEXT_PLAIN; + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + book.getTitle() + ".txt") + .contentType(mediaType) + .contentLength(bytes.length) + .body(resource); + } + + +} diff --git a/src/main/java/fic/writer/web/controller/UserController.java b/src/main/java/fic/writer/web/controller/UserController.java new file mode 100644 index 0000000..dd1f123 --- /dev/null +++ b/src/main/java/fic/writer/web/controller/UserController.java @@ -0,0 +1,61 @@ +package fic.writer.web.controller; + +import fic.writer.domain.entity.User; +import fic.writer.domain.entity.dto.UserDto; +import fic.writer.domain.service.UserService; +import fic.writer.web.response.UserResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import javax.persistence.EntityNotFoundException; +import java.util.List; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/users") +@CrossOrigin(origins = "http://localhost:3000") +public class UserController { + private static final String ID_TEMPLATE_PATH = "/{userId}"; + private static final String ID_TEMPLATE = "userId"; + + private UserService userService; + + @Autowired + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public List getAllUsers() { + return userService.findAll().stream() + .map(UserResponse::new) + .collect(Collectors.toList()); + } + + @GetMapping(ID_TEMPLATE_PATH) + public UserResponse getUserById(@PathVariable(ID_TEMPLATE) Long id) { + return userService.findById(id) + .map(UserResponse::new) + .orElseThrow(EntityNotFoundException::new); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UserResponse createUser(@RequestBody UserDto user) { + User savedUser = userService.create(user); + return new UserResponse(savedUser); + } + + @PutMapping(ID_TEMPLATE_PATH) + public UserResponse updateUser(@PathVariable(ID_TEMPLATE) Long id, @RequestBody UserDto userDto) { + User savedUser = userService.update(id, userDto); + return new UserResponse(savedUser); + } + + @DeleteMapping(ID_TEMPLATE_PATH) + @ResponseStatus(HttpStatus.NO_CONTENT) + public void deleteUser(Long id) { + userService.deleteById(id); + } +} diff --git a/src/test/java/fic/writer/web/controller/UserControllerTest.java b/src/test/java/fic/writer/web/controller/UserControllerTest.java new file mode 100644 index 0000000..3c2eb61 --- /dev/null +++ b/src/test/java/fic/writer/web/controller/UserControllerTest.java @@ -0,0 +1,133 @@ +package fic.writer.web.controller; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import fic.writer.domain.entity.User; +import fic.writer.domain.entity.dto.UserDto; +import fic.writer.domain.service.UserService; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@WebMvcTest(value = UserController.class, secure = false) +public class UserControllerTest { + private static final String USERS_PATH = "/users"; + private static final String USER_ID_PATH_TEMPLATE = USERS_PATH + "/{id}"; + @Autowired + private UserController userController; + @Autowired + private MockMvc mockMvc; + @MockBean + private UserService userService; + + + @Test + public void getUsers_whenDtoIsEmpty_shouldReturnOk() throws Exception { + List userList = new ArrayList<>(); + User user = new User(); + user.setId(1L); + user.setUsername("testUsername"); + userList.add(user); + Mockito.when(userService.findAll()).thenReturn(userList); + + mockMvc.perform(get(USERS_PATH)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.[0].username").value(user.getUsername())) + .andExpect(jsonPath("$.[0].links.[0].rel").value("self")); + } + + @Test + public void getUserById_whenUserExists_shouldReturnOk() throws Exception { + final long ID = 1L; + User user = new User(); + user.setId(ID); + user.setUsername("testUsername"); + + Mockito.when(userService.findById(1L)).thenReturn(Optional.of(user)); + + mockMvc.perform(get(USER_ID_PATH_TEMPLATE, ID)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value(user.getUsername())) + .andExpect(jsonPath("$._links.self").hasJsonPath()); + } + + @Test + public void createUser() throws Exception { + final Long USER_ID = 1L; + final String username = "testUsername", about = "about", information = "inform"; + UserDto dto = new UserDto(username, about, information); + ObjectMapper mapper = new ObjectMapper(); + + User user = User.builder() + .id(USER_ID) + .username(username) + .about(about) + .information(information) + .booksAsAuthor(new HashSet<>()) + .booksAsSubAuthor(new HashSet<>()) + .build(); + + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + String body = mapper.writeValueAsString(dto); + Mockito.when(userService.create(any(UserDto.class))).thenReturn(user); + + mockMvc.perform(post(USERS_PATH) + .content(body) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.username").value(user.getUsername())) + .andExpect(jsonPath("$._links.self").hasJsonPath()); + } + + @Test + public void updateUser() throws Exception { + final Long USER_ID = 1L; + final String NEW_USERNAME = "testUsername"; + User updatedUser = User.builder() + .id(USER_ID) + .username(NEW_USERNAME) + .booksAsAuthor(new HashSet<>()) + .booksAsSubAuthor(new HashSet<>()) + .build(); + UserDto dto = new UserDto(NEW_USERNAME, null, null); + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + String body = mapper.writeValueAsString(dto); + + Mockito.when(userService.update(anyLong(), any(UserDto.class))).thenReturn(updatedUser); + + mockMvc.perform(put(USER_ID_PATH_TEMPLATE, USER_ID) + .content(body) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.username").value(updatedUser.getUsername())) + .andExpect(jsonPath("$._links.self").hasJsonPath()); + + } + + @Test + public void deleteUser() throws Exception { + final long ID = 1L; + + mockMvc.perform(delete(USER_ID_PATH_TEMPLATE, ID)) + .andExpect(status().isNoContent()); + } +} \ No newline at end of file From 8ed06e4a2da1611370d7af604c4fb7e14e985e89 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Thu, 11 Apr 2019 18:43:55 +0300 Subject: [PATCH 12/14] Create react client --- almanac-web/package.json | 47 ++++++ almanac-web/src/App.js | 93 +++++++++++ almanac-web/src/App.test.js | 9 ++ almanac-web/src/Header.js | 22 +++ almanac-web/src/Navbar.js | 26 +++ almanac-web/src/agent.js | 96 ++++++++++++ almanac-web/src/bootstrap.min.css | 7 + almanac-web/src/components/HeaderList.js | 57 +++++++ almanac-web/src/components/ListPagination.js | 55 +++++++ almanac-web/src/constants/actionTypes.js | 45 ++++++ almanac-web/src/constants/commonConstants.js | 1 + .../src/containers/Article/ArticleCreate.js | 66 ++++++++ .../src/containers/Article/ArticleEdit.js | 114 ++++++++++++++ .../src/containers/Article/ArticleList.js | 56 +++++++ .../src/containers/Article/ArticlePreview.js | 23 +++ almanac-web/src/containers/Article/index.js | 76 +++++++++ .../containers/Authentication/ListErrors.js | 26 +++ .../src/containers/Authentication/Login.js | 97 ++++++++++++ .../src/containers/Authentication/Register.js | 113 +++++++++++++ almanac-web/src/containers/Book/BookCreate.js | 90 +++++++++++ almanac-web/src/containers/Book/BookEdit.js | 148 ++++++++++++++++++ almanac-web/src/containers/Book/BookList.js | 38 +++++ .../src/containers/Book/BookPreview.js | 47 ++++++ almanac-web/src/containers/Book/index.js | 110 +++++++++++++ almanac-web/src/containers/Home/Banner.js | 20 +++ almanac-web/src/containers/Home/MainView.js | 30 ++++ almanac-web/src/containers/Home/Tags.js | 36 +++++ almanac-web/src/containers/Home/index.js | 58 +++++++ .../src/containers/Profile/ProfilePreview.js | 43 +++++ almanac-web/src/containers/Profile/index.js | 100 ++++++++++++ almanac-web/src/index.js | 21 +++ almanac-web/src/logo.svg | 7 + almanac-web/src/middleware.js | 68 ++++++++ almanac-web/src/reducer.js | 20 +++ almanac-web/src/reducers/article.js | 37 +++++ almanac-web/src/reducers/auth.js | 34 ++++ almanac-web/src/reducers/book.js | 40 +++++ almanac-web/src/reducers/bookList.js | 33 ++++ almanac-web/src/reducers/common.js | 70 +++++++++ almanac-web/src/reducers/home.js | 14 ++ almanac-web/src/reducers/profile.js | 19 +++ almanac-web/src/store.js | 29 ++++ 42 files changed, 2141 insertions(+) create mode 100644 almanac-web/package.json create mode 100644 almanac-web/src/App.js create mode 100644 almanac-web/src/App.test.js create mode 100644 almanac-web/src/Header.js create mode 100644 almanac-web/src/Navbar.js create mode 100644 almanac-web/src/agent.js create mode 100644 almanac-web/src/bootstrap.min.css create mode 100644 almanac-web/src/components/HeaderList.js create mode 100644 almanac-web/src/components/ListPagination.js create mode 100644 almanac-web/src/constants/actionTypes.js create mode 100644 almanac-web/src/constants/commonConstants.js create mode 100644 almanac-web/src/containers/Article/ArticleCreate.js create mode 100644 almanac-web/src/containers/Article/ArticleEdit.js create mode 100644 almanac-web/src/containers/Article/ArticleList.js create mode 100644 almanac-web/src/containers/Article/ArticlePreview.js create mode 100644 almanac-web/src/containers/Article/index.js create mode 100644 almanac-web/src/containers/Authentication/ListErrors.js create mode 100644 almanac-web/src/containers/Authentication/Login.js create mode 100644 almanac-web/src/containers/Authentication/Register.js create mode 100644 almanac-web/src/containers/Book/BookCreate.js create mode 100644 almanac-web/src/containers/Book/BookEdit.js create mode 100644 almanac-web/src/containers/Book/BookList.js create mode 100644 almanac-web/src/containers/Book/BookPreview.js create mode 100644 almanac-web/src/containers/Book/index.js create mode 100644 almanac-web/src/containers/Home/Banner.js create mode 100644 almanac-web/src/containers/Home/MainView.js create mode 100644 almanac-web/src/containers/Home/Tags.js create mode 100644 almanac-web/src/containers/Home/index.js create mode 100644 almanac-web/src/containers/Profile/ProfilePreview.js create mode 100644 almanac-web/src/containers/Profile/index.js create mode 100644 almanac-web/src/index.js create mode 100644 almanac-web/src/logo.svg create mode 100644 almanac-web/src/middleware.js create mode 100644 almanac-web/src/reducer.js create mode 100644 almanac-web/src/reducers/article.js create mode 100644 almanac-web/src/reducers/auth.js create mode 100644 almanac-web/src/reducers/book.js create mode 100644 almanac-web/src/reducers/bookList.js create mode 100644 almanac-web/src/reducers/common.js create mode 100644 almanac-web/src/reducers/home.js create mode 100644 almanac-web/src/reducers/profile.js create mode 100644 almanac-web/src/store.js diff --git a/almanac-web/package.json b/almanac-web/package.json new file mode 100644 index 0000000..42f6cbd --- /dev/null +++ b/almanac-web/package.json @@ -0,0 +1,47 @@ +{ + "name": "almanac-web", + "version": "0.1.0", + "private": true, + "dependencies": { + "@tinymce/tinymce-react": "^3.0.1", + "axios": "^0.18.0", + "history": "^4.6.3", + "moment": "^2.24.0", + "moment-timezone": "^0.5.23", + "prismic-reactjs": "^0.3.2", + "react": "^16.4.2", + "react-datepicker": "^2.0.0", + "react-dom": "^16.4.2", + "react-dropdown": "^1.6.4", + "react-intl": "^2.8.0", + "react-moment": "^0.8.4", + "react-paginate": "^6.2.1", + "react-redux": "^5.0.7", + "react-router-dom": "^4.3.1", + "react-router-redux": "^5.0.0-alpha.6", + "react-rte": "^0.16.1", + "react-scripts": "1.1.4", + "redux": "^4.0.0", + "redux-devtools-extension": "^2.13.5", + "redux-logger": "^3.0.1", + "redux-sequence-action": "^0.2.1", + "redux-thunk": "^2.3.0", + "superagent": "^3.8.2", + "superagent-promise": "^1.1.0" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": [ + ">0.2%", + "not dead", + "not ie <= 11", + "not op_mini all" + ] +} diff --git a/almanac-web/src/App.js b/almanac-web/src/App.js new file mode 100644 index 0000000..fadf377 --- /dev/null +++ b/almanac-web/src/App.js @@ -0,0 +1,93 @@ +import agent from './agent'; +import Header from './Header'; +import React from 'react'; +import { connect } from 'react-redux'; +import { APP_LOAD, REDIRECT } from './constants/actionTypes'; +import { Route, Switch } from 'react-router-dom'; +import Home from './containers/Home'; +import { store } from './store'; +import { push } from 'react-router-redux'; +import Book from './containers/Book'; +import Article from './containers/Article'; +import './bootstrap.min.css' +import Profile from './containers/Profile'; +import BookCreate from './containers/Book/BookCreate'; +import BookList from './containers/Book/BookList'; +import ArticleCreate from './containers/Article/ArticleCreate'; +import BookEdit from './containers/Book/BookEdit'; +import { IntlProvider } from 'react-intl'; +import ArticleEdit from './containers/Article/ArticleEdit'; +import Login from './containers/Authentication/Login'; +import Register from './containers/Authentication/Register'; + +const mapStateToProps = state => { + return { + appLoaded: state.common.appLoaded, + appName: state.common.appName, + currentUser: state.common.currentUser, + redirectTo: state.common.redirectTo + } +}; + +const mapDispatchToProps = dispatch => ({ + onLoad: (payload, token) => + dispatch({ type: APP_LOAD, payload, token, skipTracking: true }), + onRedirect: () => + dispatch({ type: REDIRECT }) +}); + +class App extends React.Component { + componentWillReceiveProps(nextProps) { + if (nextProps.redirectTo) { + // this.context.router.replace(nextProps.redirectTo); + store.dispatch(push(nextProps.redirectTo)); + this.props.onRedirect(); + } + } + + componentWillMount() { + const token = window.localStorage.getItem('jwt'); + if (token) { + agent.setToken(token); + } + + this.props.onLoad(token ? agent.Auth.current() : null, token); + } + render() { + if (this.props.appLoaded) { + return ( +
+
+ + + + + + + + + + + + + +
+ ); + } + return ( +
+
+
+ ); + } +} + +// App.contextTypes = { +// router: PropTypes.object.isRequired +// }; + +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/almanac-web/src/App.test.js b/almanac-web/src/App.test.js new file mode 100644 index 0000000..a754b20 --- /dev/null +++ b/almanac-web/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); + ReactDOM.unmountComponentAtNode(div); +}); diff --git a/almanac-web/src/Header.js b/almanac-web/src/Header.js new file mode 100644 index 0000000..68a6fba --- /dev/null +++ b/almanac-web/src/Header.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import HeaderList from './components/HeaderList' + +class Header extends React.Component { + render() { + return ( + + ); + } +} + +export default Header; diff --git a/almanac-web/src/Navbar.js b/almanac-web/src/Navbar.js new file mode 100644 index 0000000..9cdeb9b --- /dev/null +++ b/almanac-web/src/Navbar.js @@ -0,0 +1,26 @@ +import React from 'react'; +import Banner from './containers/Home/Banner'; + +const Navbar = (props) => { + return ( + + + ); +}; +export default Navbar \ No newline at end of file diff --git a/almanac-web/src/agent.js b/almanac-web/src/agent.js new file mode 100644 index 0000000..d1ad137 --- /dev/null +++ b/almanac-web/src/agent.js @@ -0,0 +1,96 @@ +import superagentPromise from 'superagent-promise'; +import _superagent from 'superagent'; +import { PAGE_SIZE } from './constants/commonConstants' + +const superagent = superagentPromise(_superagent, global.Promise); + +const API_ROOT = 'http://localhost:8080'; + +const encode = encodeURIComponent; +const responseBody = res => res.body; + +let token = null; +const tokenPlugin = req => { + if (token) { + req.set('authorization', `Token ${token}`); + } +} + +const requests = { + del: url => + superagent.del(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + get: url => + superagent.get(`${API_ROOT}${url}`).use(tokenPlugin).then(responseBody), + put: (url, body) => + superagent.put(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody), + post: (url, body) => + superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody) +}; +const directRequest = { + del: url => + superagent.del(`${url}`).use(tokenPlugin).then(responseBody), + get: url => + superagent.get(`${url}`).use(tokenPlugin).then(responseBody), + put: (url, body) => + superagent.put(`${url}`, body).use(tokenPlugin).then(responseBody), + post: (url, body) => + superagent.post(`${url}`, body).use(tokenPlugin).then(responseBody) +}; + +const Auth = { + current: () => + requests.get('/user'), + login: (email, password) => + requests.post('/users/login', { user: { email, password } }), + register: (username, email, password) => + requests.post('/users', { username, email, password }), + save: user => + requests.put('/user', { user }) +}; + +const Tags = { + getAll: () => requests.get('/tags') +}; + +const limit = (size, page) => `size=${size}&page=${page ? page : 0}`; +const omitSlug = article => Object.assign({}, article, { slug: undefined }) +const Books = { + all: page => + requests.get(`/books?${limit(PAGE_SIZE, page)}`), + del: slug => + requests.del(`/books/${slug}`), + get: slug => + requests.get(`/books/${slug}`), + update: (id, book) => + requests.put(`/books/${id}`, { ...book }), + create: book => + requests.post('/books', { ...book }) +}; +const Articles = { + del: (bookId, articleId) => + requests.del(`/books/${bookId}/articles/${articleId}`), + get: (bookId, articleId) => + requests.get(`/books/${bookId}/articles/${articleId}`), + update: (bookId, articleId, article) => + requests.put(`/books/${bookId}/articles/${articleId}`, { ...article }), + create: (bookId, article) => + requests.post(`/books/${bookId}/articles`, { ...article }) +}; + +const Profile = { + follow: username => + requests.post(`/profiles/${username}/follow`), + get: id => + requests.get(`/users/${id}`), + unfollow: username => + requests.del(`/profiles/${username}/follow`) +}; + +export default { + Books, + Auth, + Profile, + directRequest, + Articles, + setToken: _token => { token = _token; } +}; diff --git a/almanac-web/src/bootstrap.min.css b/almanac-web/src/bootstrap.min.css new file mode 100644 index 0000000..7aebd0f --- /dev/null +++ b/almanac-web/src/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.1.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue:#007bff;--indigo:#6610f2;--purple:#6f42c1;--pink:#e83e8c;--red:#dc3545;--orange:#fd7e14;--yellow:#ffc107;--green:#28a745;--teal:#20c997;--cyan:#17a2b8;--white:#fff;--gray:#6c757d;--gray-dark:#343a40;--primary:#007bff;--secondary:#6c757d;--success:#28a745;--info:#17a2b8;--warning:#ffc107;--danger:#dc3545;--light:#f8f9fa;--dark:#343a40;--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.2;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014 \00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-break:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}.container-fluid{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-auto,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-auto,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-auto,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-auto,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-auto{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-ms-flex-order:-1;order:-1}.order-last{-ms-flex-order:13;order:13}.order-0{-ms-flex-order:0;order:0}.order-1{-ms-flex-order:1;order:1}.order-2{-ms-flex-order:2;order:2}.order-3{-ms-flex-order:3;order:3}.order-4{-ms-flex-order:4;order:4}.order-5{-ms-flex-order:5;order:5}.order-6{-ms-flex-order:6;order:6}.order-7{-ms-flex-order:7;order:7}.order-8{-ms-flex-order:8;order:8}.order-9{-ms-flex-order:9;order:9}.order-10{-ms-flex-order:10;order:10}.order-11{-ms-flex-order:11;order:11}.order-12{-ms-flex-order:12;order:12}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-sm-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-ms-flex-order:-1;order:-1}.order-sm-last{-ms-flex-order:13;order:13}.order-sm-0{-ms-flex-order:0;order:0}.order-sm-1{-ms-flex-order:1;order:1}.order-sm-2{-ms-flex-order:2;order:2}.order-sm-3{-ms-flex-order:3;order:3}.order-sm-4{-ms-flex-order:4;order:4}.order-sm-5{-ms-flex-order:5;order:5}.order-sm-6{-ms-flex-order:6;order:6}.order-sm-7{-ms-flex-order:7;order:7}.order-sm-8{-ms-flex-order:8;order:8}.order-sm-9{-ms-flex-order:9;order:9}.order-sm-10{-ms-flex-order:10;order:10}.order-sm-11{-ms-flex-order:11;order:11}.order-sm-12{-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-md-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-ms-flex-order:-1;order:-1}.order-md-last{-ms-flex-order:13;order:13}.order-md-0{-ms-flex-order:0;order:0}.order-md-1{-ms-flex-order:1;order:1}.order-md-2{-ms-flex-order:2;order:2}.order-md-3{-ms-flex-order:3;order:3}.order-md-4{-ms-flex-order:4;order:4}.order-md-5{-ms-flex-order:5;order:5}.order-md-6{-ms-flex-order:6;order:6}.order-md-7{-ms-flex-order:7;order:7}.order-md-8{-ms-flex-order:8;order:8}.order-md-9{-ms-flex-order:9;order:9}.order-md-10{-ms-flex-order:10;order:10}.order-md-11{-ms-flex-order:11;order:11}.order-md-12{-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-lg-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-ms-flex-order:-1;order:-1}.order-lg-last{-ms-flex-order:13;order:13}.order-lg-0{-ms-flex-order:0;order:0}.order-lg-1{-ms-flex-order:1;order:1}.order-lg-2{-ms-flex-order:2;order:2}.order-lg-3{-ms-flex-order:3;order:3}.order-lg-4{-ms-flex-order:4;order:4}.order-lg-5{-ms-flex-order:5;order:5}.order-lg-6{-ms-flex-order:6;order:6}.order-lg-7{-ms-flex-order:7;order:7}.order-lg-8{-ms-flex-order:8;order:8}.order-lg-9{-ms-flex-order:9;order:9}.order-lg-10{-ms-flex-order:10;order:10}.order-lg-11{-ms-flex-order:11;order:11}.order-lg-12{-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:none}.col-xl-1{-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-ms-flex-order:-1;order:-1}.order-xl-last{-ms-flex-order:13;order:13}.order-xl-0{-ms-flex-order:0;order:0}.order-xl-1{-ms-flex-order:1;order:1}.order-xl-2{-ms-flex-order:2;order:2}.order-xl-3{-ms-flex-order:3;order:3}.order-xl-4{-ms-flex-order:4;order:4}.order-xl-5{-ms-flex-order:5;order:5}.order-xl-6{-ms-flex-order:6;order:6}.order-xl-7{-ms-flex-order:7;order:7}.order-xl-8{-ms-flex-order:8;order:8}.order-xl-9{-ms-flex-order:9;order:9}.order-xl-10{-ms-flex-order:10;order:10}.order-xl-11{-ms-flex-order:11;order:11}.order-xl-12{-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary>td,.table-primary>th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#9fcdff}.table-secondary,.table-secondary>td,.table-secondary>th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#c8cbcf}.table-success,.table-success>td,.table-success>th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#b1dfbb}.table-info,.table-info>td,.table-info>th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#abdde5}.table-warning,.table-warning>td,.table-warning>th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#ffe8a1}.table-danger,.table-danger>td,.table-danger>th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f1b0b7}.table-light,.table-light>td,.table-light>th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ececf6}.table-dark,.table-dark>td,.table-dark>th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b9bbbe}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm>.table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-md>.table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-lg>.table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding-top:.375rem;padding-bottom:.375rem;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm,.input-group-lg>.form-control-plaintext.form-control,.input-group-lg>.input-group-append>.form-control-plaintext.btn,.input-group-lg>.input-group-append>.form-control-plaintext.input-group-text,.input-group-lg>.input-group-prepend>.form-control-plaintext.btn,.input-group-lg>.input-group-prepend>.form-control-plaintext.input-group-text,.input-group-sm>.form-control-plaintext.form-control,.input-group-sm>.input-group-append>.form-control-plaintext.btn,.input-group-sm>.input-group-append>.form-control-plaintext.input-group-text,.input-group-sm>.input-group-prepend>.form-control-plaintext.btn,.input-group-sm>.input-group-prepend>.form-control-plaintext.input-group-text{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-append>.btn,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-prepend>.input-group-text{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-sm>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-sm>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:calc(1.8125rem + 2px)}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-append>.btn,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-prepend>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-lg>.input-group-append>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-append>select.input-group-text:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.btn:not([size]):not([multiple]),.input-group-lg>.input-group-prepend>select.input-group-text:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:calc(2.875rem + 2px)}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input:disabled~.form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#28a745}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(40,167,69,.8);border-radius:.2rem}.custom-select.is-valid,.form-control.is-valid,.was-validated .custom-select:valid,.was-validated .form-control:valid{border-color:#28a745}.custom-select.is-valid:focus,.form-control.is-valid:focus,.was-validated .custom-select:valid:focus,.was-validated .form-control:valid:focus{border-color:#28a745;box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.custom-select.is-valid~.valid-feedback,.custom-select.is-valid~.valid-tooltip,.form-control.is-valid~.valid-feedback,.form-control.is-valid~.valid-tooltip,.was-validated .custom-select:valid~.valid-feedback,.was-validated .custom-select:valid~.valid-tooltip,.was-validated .form-control:valid~.valid-feedback,.was-validated .form-control:valid~.valid-tooltip{display:block}.form-control-file.is-valid~.valid-feedback,.form-control-file.is-valid~.valid-tooltip,.was-validated .form-control-file:valid~.valid-feedback,.was-validated .form-control-file:valid~.valid-tooltip{display:block}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#28a745}.form-check-input.is-valid~.valid-feedback,.form-check-input.is-valid~.valid-tooltip,.was-validated .form-check-input:valid~.valid-feedback,.was-validated .form-check-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid~.custom-control-label,.was-validated .custom-control-input:valid~.custom-control-label{color:#28a745}.custom-control-input.is-valid~.custom-control-label::before,.was-validated .custom-control-input:valid~.custom-control-label::before{background-color:#71dd8a}.custom-control-input.is-valid~.valid-feedback,.custom-control-input.is-valid~.valid-tooltip,.was-validated .custom-control-input:valid~.valid-feedback,.was-validated .custom-control-input:valid~.valid-tooltip{display:block}.custom-control-input.is-valid:checked~.custom-control-label::before,.was-validated .custom-control-input:valid:checked~.custom-control-label::before{background-color:#34ce57}.custom-control-input.is-valid:focus~.custom-control-label::before,.was-validated .custom-control-input:valid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(40,167,69,.25)}.custom-file-input.is-valid~.custom-file-label,.was-validated .custom-file-input:valid~.custom-file-label{border-color:#28a745}.custom-file-input.is-valid~.custom-file-label::before,.was-validated .custom-file-input:valid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-valid~.valid-feedback,.custom-file-input.is-valid~.valid-tooltip,.was-validated .custom-file-input:valid~.valid-feedback,.was-validated .custom-file-input:valid~.valid-tooltip{display:block}.custom-file-input.is-valid:focus~.custom-file-label,.was-validated .custom-file-input:valid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(40,167,69,.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.5rem;margin-top:.1rem;font-size:.875rem;line-height:1;color:#fff;background-color:rgba(220,53,69,.8);border-radius:.2rem}.custom-select.is-invalid,.form-control.is-invalid,.was-validated .custom-select:invalid,.was-validated .form-control:invalid{border-color:#dc3545}.custom-select.is-invalid:focus,.form-control.is-invalid:focus,.was-validated .custom-select:invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.custom-select.is-invalid~.invalid-feedback,.custom-select.is-invalid~.invalid-tooltip,.form-control.is-invalid~.invalid-feedback,.form-control.is-invalid~.invalid-tooltip,.was-validated .custom-select:invalid~.invalid-feedback,.was-validated .custom-select:invalid~.invalid-tooltip,.was-validated .form-control:invalid~.invalid-feedback,.was-validated .form-control:invalid~.invalid-tooltip{display:block}.form-control-file.is-invalid~.invalid-feedback,.form-control-file.is-invalid~.invalid-tooltip,.was-validated .form-control-file:invalid~.invalid-feedback,.was-validated .form-control-file:invalid~.invalid-tooltip{display:block}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-input.is-invalid~.invalid-feedback,.form-check-input.is-invalid~.invalid-tooltip,.was-validated .form-check-input:invalid~.invalid-feedback,.was-validated .form-check-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid~.custom-control-label,.was-validated .custom-control-input:invalid~.custom-control-label{color:#dc3545}.custom-control-input.is-invalid~.custom-control-label::before,.was-validated .custom-control-input:invalid~.custom-control-label::before{background-color:#efa2a9}.custom-control-input.is-invalid~.invalid-feedback,.custom-control-input.is-invalid~.invalid-tooltip,.was-validated .custom-control-input:invalid~.invalid-feedback,.was-validated .custom-control-input:invalid~.invalid-tooltip{display:block}.custom-control-input.is-invalid:checked~.custom-control-label::before,.was-validated .custom-control-input:invalid:checked~.custom-control-label::before{background-color:#e4606d}.custom-control-input.is-invalid:focus~.custom-control-label::before,.was-validated .custom-control-input:invalid:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(220,53,69,.25)}.custom-file-input.is-invalid~.custom-file-label,.was-validated .custom-file-input:invalid~.custom-file-label{border-color:#dc3545}.custom-file-input.is-invalid~.custom-file-label::before,.was-validated .custom-file-input:invalid~.custom-file-label::before{border-color:inherit}.custom-file-input.is-invalid~.invalid-feedback,.custom-file-input.is-invalid~.invalid-tooltip,.was-validated .custom-file-input:invalid~.invalid-feedback,.was-validated .custom-file-input:invalid~.invalid-tooltip{display:block}.custom-file-input.is-invalid:focus~.custom-file-label,.was-validated .custom-file-input:invalid:focus~.custom-file-label{box-shadow:0 0 0 .2rem rgba(220,53,69,.25)}.form-inline{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-ms-flexbox;display:flex;-ms-flex:0 0 auto;flex:0 0 auto;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .custom-select,.form-inline .input-group{width:auto}.form-inline .form-check{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media screen and (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}.btn:not(:disabled):not(.disabled).active,.btn:not(:disabled):not(.disabled):active{background-image:none}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:hover{color:#fff;background-color:#0069d9;border-color:#0062cc}.btn-primary.focus,.btn-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#007bff;border-color:#007bff}.btn-primary:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0062cc;border-color:#005cbf}.btn-primary:not(:disabled):not(.disabled).active:focus,.btn-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5a6268;border-color:#545b62}.btn-secondary.focus,.btn-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:not(:disabled):not(.disabled).active,.btn-secondary:not(:disabled):not(.disabled):active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#545b62;border-color:#4e555b}.btn-secondary:not(:disabled):not(.disabled).active:focus,.btn-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-success{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:hover{color:#fff;background-color:#218838;border-color:#1e7e34}.btn-success.focus,.btn-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#28a745;border-color:#28a745}.btn-success:not(:disabled):not(.disabled).active,.btn-success:not(:disabled):not(.disabled):active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#1e7e34;border-color:#1c7430}.btn-success:not(:disabled):not(.disabled).active:focus,.btn-success:not(:disabled):not(.disabled):active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-info{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:hover{color:#fff;background-color:#138496;border-color:#117a8b}.btn-info.focus,.btn-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-info:not(:disabled):not(.disabled).active,.btn-info:not(:disabled):not(.disabled):active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#117a8b;border-color:#10707f}.btn-info:not(:disabled):not(.disabled).active:focus,.btn-info:not(:disabled):not(.disabled):active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-warning{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#212529;background-color:#e0a800;border-color:#d39e00}.btn-warning.focus,.btn-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-warning:not(:disabled):not(.disabled).active,.btn-warning:not(:disabled):not(.disabled):active,.show>.btn-warning.dropdown-toggle{color:#212529;background-color:#d39e00;border-color:#c69500}.btn-warning:not(:disabled):not(.disabled).active:focus,.btn-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#c82333;border-color:#bd2130}.btn-danger.focus,.btn-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:not(:disabled):not(.disabled).active,.btn-danger:not(:disabled):not(.disabled):active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#bd2130;border-color:#b21f2d}.btn-danger:not(:disabled):not(.disabled).active:focus,.btn-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-light{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#212529;background-color:#e2e6ea;border-color:#dae0e5}.btn-light.focus,.btn-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:not(:disabled):not(.disabled).active,.btn-light:not(:disabled):not(.disabled):active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#dae0e5;border-color:#d3d9df}.btn-light:not(:disabled):not(.disabled).active:focus,.btn-light:not(:disabled):not(.disabled):active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-dark{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:hover{color:#fff;background-color:#23272b;border-color:#1d2124}.btn-dark.focus,.btn-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#343a40;border-color:#343a40}.btn-dark:not(:disabled):not(.disabled).active,.btn-dark:not(:disabled):not(.disabled):active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1d2124;border-color:#171a1d}.btn-dark:not(:disabled):not(.disabled).active:focus,.btn-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-primary{color:#007bff;background-color:transparent;background-image:none;border-color:#007bff}.btn-outline-primary:hover{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary.focus,.btn-outline-primary:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#007bff;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled).active,.btn-outline-primary:not(:disabled):not(.disabled):active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#007bff;border-color:#007bff}.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(0,123,255,.5)}.btn-outline-secondary{color:#6c757d;background-color:transparent;background-image:none;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary.focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled).active,.btn-outline-secondary:not(:disabled):not(.disabled):active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(108,117,125,.5)}.btn-outline-success{color:#28a745;background-color:transparent;background-image:none;border-color:#28a745}.btn-outline-success:hover{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success.focus,.btn-outline-success:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#28a745;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled).active,.btn-outline-success:not(:disabled):not(.disabled):active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#28a745;border-color:#28a745}.btn-outline-success:not(:disabled):not(.disabled).active:focus,.btn-outline-success:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(40,167,69,.5)}.btn-outline-info{color:#17a2b8;background-color:transparent;background-image:none;border-color:#17a2b8}.btn-outline-info:hover{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info.focus,.btn-outline-info:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#17a2b8;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled).active,.btn-outline-info:not(:disabled):not(.disabled):active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#17a2b8;border-color:#17a2b8}.btn-outline-info:not(:disabled):not(.disabled).active:focus,.btn-outline-info:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(23,162,184,.5)}.btn-outline-warning{color:#ffc107;background-color:transparent;background-image:none;border-color:#ffc107}.btn-outline-warning:hover{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning.focus,.btn-outline-warning:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled).active,.btn-outline-warning:not(:disabled):not(.disabled):active,.show>.btn-outline-warning.dropdown-toggle{color:#212529;background-color:#ffc107;border-color:#ffc107}.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(255,193,7,.5)}.btn-outline-danger{color:#dc3545;background-color:transparent;background-image:none;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger.focus,.btn-outline-danger:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled).active,.btn-outline-danger:not(:disabled):not(.disabled):active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(220,53,69,.5)}.btn-outline-light{color:#f8f9fa;background-color:transparent;background-image:none;border-color:#f8f9fa}.btn-outline-light:hover{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light.focus,.btn-outline-light:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled).active,.btn-outline-light:not(:disabled):not(.disabled):active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:not(:disabled):not(.disabled).active:focus,.btn-outline-light:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(248,249,250,.5)}.btn-outline-dark{color:#343a40;background-color:transparent;background-image:none;border-color:#343a40}.btn-outline-dark:hover{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark.focus,.btn-outline-dark:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#343a40;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled).active,.btn-outline-dark:not(:disabled):not(.disabled):active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#343a40;border-color:#343a40}.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(52,58,64,.5)}.btn-link{font-weight:400;color:#007bff;background-color:transparent}.btn-link:hover{color:#0056b3;text-decoration:underline;background-color:transparent;border-color:transparent}.btn-link.focus,.btn-link:focus{text-decoration:underline;border-color:transparent;box-shadow:none}.btn-link.disabled,.btn-link:disabled{color:#6c757d;pointer-events:none}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{transition:opacity .15s linear}@media screen and (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height .35s ease}@media screen and (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropleft,.dropright,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu-right{right:0;left:auto}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;width:0;height:0;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^=bottom],.dropdown-menu[x-placement^=left],.dropdown-menu[x-placement^=right],.dropdown-menu[x-placement^=top]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#007bff}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:1}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:1}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:not(:first-child){border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type=checkbox],.btn-group-toggle>.btn input[type=radio],.btn-group-toggle>.btn-group>.btn input[type=checkbox],.btn-group-toggle>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.custom-file,.input-group>.custom-select,.input-group>.form-control{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group>.custom-file:focus,.input-group>.custom-select:focus,.input-group>.form-control:focus{z-index:3}.input-group>.custom-file+.custom-file,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.form-control,.input-group>.custom-select+.custom-file,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.form-control,.input-group>.form-control+.custom-file,.input-group>.form-control+.custom-select,.input-group>.form-control+.form-control{margin-left:-1px}.input-group>.custom-select:not(:last-child),.input-group>.form-control:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-select:not(:first-child),.input-group>.form-control:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-append,.input-group-prepend{display:-ms-flexbox;display:flex}.input-group-append .btn,.input-group-prepend .btn{position:relative;z-index:2}.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.btn,.input-group-append .input-group-text+.input-group-text,.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-prepend .input-group-text+.input-group-text{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type=checkbox],.input-group-text input[type=radio]{margin-top:0}.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child),.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child),.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text{border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-label::before{color:#fff;background-color:#007bff}.custom-control-input:focus~.custom-control-label::before{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-control-input:active~.custom-control-label::before{color:#fff;background-color:#b3d7ff}.custom-control-input:disabled~.custom-control-label{color:#6c757d}.custom-control-input:disabled~.custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#dee2e6}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background-repeat:no-repeat;background-position:center center;background-size:50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::before{background-color:#007bff}.custom-checkbox .custom-control-input:indeterminate~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-checkbox .custom-control-input:disabled:indeterminate~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-label::before{background-color:#007bff}.custom-radio .custom-control-input:checked~.custom-control-label::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-radio .custom-control-input:disabled:checked~.custom-control-label::before{background-color:rgba(0,123,255,.5)}.custom-select{display:inline-block;width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;background-size:8px 10px;border:1px solid #ced4da;border-radius:.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#80bdff;outline:0;box-shadow:inset 0 1px 2px rgba(0,0,0,.075),0 0 5px rgba(128,189,255,.5)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{height:calc(1.8125rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-select-lg{height:calc(2.875rem + 2px);padding-top:.375rem;padding-bottom:.375rem;font-size:125%}.custom-file{position:relative;display:inline-block;width:100%;height:calc(2.25rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(2.25rem + 2px);margin:0;opacity:0}.custom-file-input:focus~.custom-file-label{border-color:#80bdff;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.custom-file-input:focus~.custom-file-label::after{border-color:#80bdff}.custom-file-input:lang(en)~.custom-file-label::after{content:"Browse"}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(2.25rem + 2px);padding:.375rem .75rem;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:2.25rem;padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:1px solid #ced4da;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;padding-left:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:0}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#007bff;border:0;border-radius:1rem;-webkit-appearance:none;appearance:none}.custom-range::-webkit-slider-thumb:focus{outline:0;box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-webkit-slider-thumb:active{background-color:#b3d7ff}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;-moz-appearance:none;appearance:none}.custom-range::-moz-range-thumb:focus{outline:0;box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-moz-range-thumb:active{background-color:#b3d7ff}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;background-color:#007bff;border:0;border-radius:1rem;appearance:none}.custom-range::-ms-thumb:focus{outline:0;box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(0,123,255,.25)}.custom-range::-ms-thumb:active{background-color:#b3d7ff}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.nav{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#6c757d}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#007bff}.nav-fill .nav-item{-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:.5rem 1rem}.navbar>.container,.navbar>.container-fluid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-ms-flex-positive:1;flex-grow:1;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler:not(:disabled):not(.disabled){cursor:pointer}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width:575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-expand-sm{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width:767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-expand-md{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width:991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-expand-lg{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width:1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-expand-xl{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-ms-flex-flow:row nowrap;flex-flow:row nowrap;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-ms-flexbox!important;display:flex!important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.5);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,.9)}.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.5);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 30 30' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E")}.navbar-dark .navbar-text{color:rgba(255,255,255,.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group:first-child .list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-body{-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img{width:100%;border-radius:calc(.25rem - 1px)}.card-img-top{width:100%;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img-bottom{width:100%;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-deck .card{margin-bottom:15px}@media (min-width:576px){.card-deck{-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{display:-ms-flexbox;display:flex;-ms-flex:1 0 0%;flex:1 0 0%;-ms-flex-direction:column;flex-direction:column;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.card-group>.card{margin-bottom:15px}@media (min-width:576px){.card-group{-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:first-child .card-header,.card-group>.card:first-child .card-img-top{border-top-right-radius:0}.card-group>.card:first-child .card-footer,.card-group>.card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:last-child .card-header,.card-group>.card:last-child .card-img-top{border-top-left-radius:0}.card-group>.card:last-child .card-footer,.card-group>.card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group>.card:only-child{border-radius:.25rem}.card-group>.card:only-child .card-header,.card-group>.card:only-child .card-img-top{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.card-group>.card:only-child .card-footer,.card-group>.card:only-child .card-img-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-group>.card:not(:first-child):not(:last-child):not(:only-child){border-radius:0}.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-footer,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-header,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-bottom,.card-group>.card:not(:first-child):not(:last-child):not(:only-child) .card-img-top{border-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion .card:not(:first-of-type):not(:last-of-type){border-bottom:0;border-radius:0}.accordion .card:not(:first-of-type) .card-header:first-child{border-radius:0}.accordion .card:first-of-type{border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion .card:last-of-type{border-top-left-radius:0;border-top-right-radius:0}.breadcrumb{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#007bff;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#0056b3;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:2;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.page-link:not(:disabled):not(.disabled){cursor:pointer}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:1;color:#fff;background-color:#007bff;border-color:#007bff}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#007bff}.badge-primary[href]:focus,.badge-primary[href]:hover{color:#fff;text-decoration:none;background-color:#0062cc}.badge-secondary{color:#fff;background-color:#6c757d}.badge-secondary[href]:focus,.badge-secondary[href]:hover{color:#fff;text-decoration:none;background-color:#545b62}.badge-success{color:#fff;background-color:#28a745}.badge-success[href]:focus,.badge-success[href]:hover{color:#fff;text-decoration:none;background-color:#1e7e34}.badge-info{color:#fff;background-color:#17a2b8}.badge-info[href]:focus,.badge-info[href]:hover{color:#fff;text-decoration:none;background-color:#117a8b}.badge-warning{color:#212529;background-color:#ffc107}.badge-warning[href]:focus,.badge-warning[href]:hover{color:#212529;text-decoration:none;background-color:#d39e00}.badge-danger{color:#fff;background-color:#dc3545}.badge-danger[href]:focus,.badge-danger[href]:hover{color:#fff;text-decoration:none;background-color:#bd2130}.badge-light{color:#212529;background-color:#f8f9fa}.badge-light[href]:focus,.badge-light[href]:hover{color:#212529;text-decoration:none;background-color:#dae0e5}.badge-dark{color:#fff;background-color:#343a40}.badge-dark[href]:focus,.badge-dark[href]:hover{color:#fff;text-decoration:none;background-color:#1d2124}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#004085;background-color:#cce5ff;border-color:#b8daff}.alert-primary hr{border-top-color:#9fcdff}.alert-primary .alert-link{color:#002752}.alert-secondary{color:#383d41;background-color:#e2e3e5;border-color:#d6d8db}.alert-secondary hr{border-top-color:#c8cbcf}.alert-secondary .alert-link{color:#202326}.alert-success{color:#155724;background-color:#d4edda;border-color:#c3e6cb}.alert-success hr{border-top-color:#b1dfbb}.alert-success .alert-link{color:#0b2e13}.alert-info{color:#0c5460;background-color:#d1ecf1;border-color:#bee5eb}.alert-info hr{border-top-color:#abdde5}.alert-info .alert-link{color:#062c33}.alert-warning{color:#856404;background-color:#fff3cd;border-color:#ffeeba}.alert-warning hr{border-top-color:#ffe8a1}.alert-warning .alert-link{color:#533f03}.alert-danger{color:#721c24;background-color:#f8d7da;border-color:#f5c6cb}.alert-danger hr{border-top-color:#f1b0b7}.alert-danger .alert-link{color:#491217}.alert-light{color:#818182;background-color:#fefefe;border-color:#fdfdfe}.alert-light hr{border-top-color:#ececf6}.alert-light .alert-link{color:#686868}.alert-dark{color:#1b1e21;background-color:#d6d8d9;border-color:#c6c8ca}.alert-dark hr{border-top-color:#b9bbbe}.alert-dark .alert-link{color:#040505}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex-pack:center;justify-content:center;color:#fff;text-align:center;white-space:nowrap;background-color:#007bff;transition:width .6s ease}@media screen and (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start}.media-body{-ms-flex:1;flex:1}.list-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{z-index:1;text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#007bff;border-color:#007bff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-primary{color:#004085;background-color:#b8daff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#004085;background-color:#9fcdff}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#004085;border-color:#004085}.list-group-item-secondary{color:#383d41;background-color:#d6d8db}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#383d41;background-color:#c8cbcf}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#383d41;border-color:#383d41}.list-group-item-success{color:#155724;background-color:#c3e6cb}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#155724;background-color:#b1dfbb}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#155724;border-color:#155724}.list-group-item-info{color:#0c5460;background-color:#bee5eb}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#0c5460;background-color:#abdde5}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#0c5460;border-color:#0c5460}.list-group-item-warning{color:#856404;background-color:#ffeeba}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#856404;background-color:#ffe8a1}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#856404;border-color:#856404}.list-group-item-danger{color:#721c24;background-color:#f5c6cb}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#721c24;background-color:#f1b0b7}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#721c24;border-color:#721c24}.list-group-item-light{color:#818182;background-color:#fdfdfe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#818182;background-color:#ececf6}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#818182;border-color:#818182}.list-group-item-dark{color:#1b1e21;background-color:#c6c8ca}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#1b1e21;background-color:#b9bbbe}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#1b1e21;border-color:#1b1e21}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;opacity:.75}.close:not(:disabled):not(.disabled){cursor:pointer}button.close{padding:0;background-color:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out;-webkit-transform:translate(0,-25%);transform:translate(0,-25%)}@media screen and (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{-webkit-transform:translate(0,0);transform:translate(0,0)}.modal-dialog-centered{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;min-height:calc(100% - (.5rem * 2))}.modal-content{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-ms-flexbox;display:flex;-ms-flex-align:start;align-items:flex-start;-ms-flex-pack:justify;justify-content:space-between;padding:1rem;border-bottom:1px solid #e9ecef;border-top-left-radius:.3rem;border-top-right-radius:.3rem}.modal-header .close{padding:1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:end;justify-content:flex-end;padding:1rem;border-top:1px solid #e9ecef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-centered{min-height:calc(100% - (1.75rem * 2))}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[x-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[x-placement^=top] .arrow,.bs-tooltip-top .arrow{bottom:0}.bs-tooltip-auto[x-placement^=top] .arrow::before,.bs-tooltip-top .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[x-placement^=right],.bs-tooltip-right{padding:0 .4rem}.bs-tooltip-auto[x-placement^=right] .arrow,.bs-tooltip-right .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=right] .arrow::before,.bs-tooltip-right .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[x-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[x-placement^=bottom] .arrow,.bs-tooltip-bottom .arrow{top:0}.bs-tooltip-auto[x-placement^=bottom] .arrow::before,.bs-tooltip-bottom .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[x-placement^=left],.bs-tooltip-left{padding:0 .4rem}.bs-tooltip-auto[x-placement^=left] .arrow,.bs-tooltip-left .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[x-placement^=left] .arrow::before,.bs-tooltip-left .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::after,.popover .arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[x-placement^=top],.bs-popover-top{margin-bottom:.5rem}.bs-popover-auto[x-placement^=top] .arrow,.bs-popover-top .arrow{bottom:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::after,.bs-popover-top .arrow::before{border-width:.5rem .5rem 0}.bs-popover-auto[x-placement^=top] .arrow::before,.bs-popover-top .arrow::before{bottom:0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=top] .arrow::after,.bs-popover-top .arrow::after{bottom:1px;border-top-color:#fff}.bs-popover-auto[x-placement^=right],.bs-popover-right{margin-left:.5rem}.bs-popover-auto[x-placement^=right] .arrow,.bs-popover-right .arrow{left:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::after,.bs-popover-right .arrow::before{border-width:.5rem .5rem .5rem 0}.bs-popover-auto[x-placement^=right] .arrow::before,.bs-popover-right .arrow::before{left:0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=right] .arrow::after,.bs-popover-right .arrow::after{left:1px;border-right-color:#fff}.bs-popover-auto[x-placement^=bottom],.bs-popover-bottom{margin-top:.5rem}.bs-popover-auto[x-placement^=bottom] .arrow,.bs-popover-bottom .arrow{top:calc((.5rem + 1px) * -1)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::after,.bs-popover-bottom .arrow::before{border-width:0 .5rem .5rem .5rem}.bs-popover-auto[x-placement^=bottom] .arrow::before,.bs-popover-bottom .arrow::before{top:0;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=bottom] .arrow::after,.bs-popover-bottom .arrow::after{top:1px;border-bottom-color:#fff}.bs-popover-auto[x-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-auto[x-placement^=left],.bs-popover-left{margin-right:.5rem}.bs-popover-auto[x-placement^=left] .arrow,.bs-popover-left .arrow{right:calc((.5rem + 1px) * -1);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::after,.bs-popover-left .arrow::before{border-width:.5rem 0 .5rem .5rem}.bs-popover-auto[x-placement^=left] .arrow::before,.bs-popover-left .arrow::before{right:0;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[x-placement^=left] .arrow::after,.bs-popover-left .arrow::after{right:1px;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;color:inherit;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;-ms-flex-align:center;align-items:center;width:100%;transition:-webkit-transform .6s ease;transition:transform .6s ease;transition:transform .6s ease,-webkit-transform .6s ease;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}@media screen and (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translateX(100%);transform:translateX(100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translateX(-100%);transform:translateX(-100%)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-fade .carousel-item{opacity:0;transition-duration:.6s;transition-property:opacity}.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right,.carousel-fade .carousel-item.active{opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{opacity:0}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translateX(0);transform:translateX(0)}@supports ((-webkit-transform-style:preserve-3d) or (transform-style:preserve-3d)){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-prev,.carousel-fade .carousel-item-next,.carousel-fade .carousel-item-prev,.carousel-fade .carousel-item.active{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M5.25 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M2.75 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-primary{background-color:#007bff!important}a.bg-primary:focus,a.bg-primary:hover,button.bg-primary:focus,button.bg-primary:hover{background-color:#0062cc!important}.bg-secondary{background-color:#6c757d!important}a.bg-secondary:focus,a.bg-secondary:hover,button.bg-secondary:focus,button.bg-secondary:hover{background-color:#545b62!important}.bg-success{background-color:#28a745!important}a.bg-success:focus,a.bg-success:hover,button.bg-success:focus,button.bg-success:hover{background-color:#1e7e34!important}.bg-info{background-color:#17a2b8!important}a.bg-info:focus,a.bg-info:hover,button.bg-info:focus,button.bg-info:hover{background-color:#117a8b!important}.bg-warning{background-color:#ffc107!important}a.bg-warning:focus,a.bg-warning:hover,button.bg-warning:focus,button.bg-warning:hover{background-color:#d39e00!important}.bg-danger{background-color:#dc3545!important}a.bg-danger:focus,a.bg-danger:hover,button.bg-danger:focus,button.bg-danger:hover{background-color:#bd2130!important}.bg-light{background-color:#f8f9fa!important}a.bg-light:focus,a.bg-light:hover,button.bg-light:focus,button.bg-light:hover{background-color:#dae0e5!important}.bg-dark{background-color:#343a40!important}a.bg-dark:focus,a.bg-dark:hover,button.bg-dark:focus,button.bg-dark:hover{background-color:#1d2124!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.border{border:1px solid #dee2e6!important}.border-top{border-top:1px solid #dee2e6!important}.border-right{border-right:1px solid #dee2e6!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-left{border-left:1px solid #dee2e6!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.border-primary{border-color:#007bff!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#28a745!important}.border-info{border-color:#17a2b8!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#343a40!important}.border-white{border-color:#fff!important}.rounded{border-radius:.25rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-right{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-left{border-top-left-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-circle{border-radius:50%!important}.rounded-0{border-radius:0!important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}@media print{.d-print-none{display:none!important}.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:-ms-flexbox!important;display:flex!important}.d-print-inline-flex{display:-ms-inline-flexbox!important;display:inline-flex!important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-sm-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-sm-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-sm-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-sm-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-sm-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-sm-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-sm-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-sm-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-md-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-md-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-md-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-md-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-md-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-md-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-md-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-md-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-lg-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-lg-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-lg-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-lg-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-lg-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-lg-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-lg-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-lg-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-row{-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.flex-xl-fill{-ms-flex:1 1 auto!important;flex:1 1 auto!important}.flex-xl-grow-0{-ms-flex-positive:0!important;flex-grow:0!important}.flex-xl-grow-1{-ms-flex-positive:1!important;flex-grow:1!important}.flex-xl-shrink-0{-ms-flex-negative:0!important;flex-shrink:0!important}.flex-xl-shrink-1{-ms-flex-negative:1!important;flex-shrink:1!important}.justify-content-xl-start{-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-ms-flex-item-align:auto!important;align-self:auto!important}.align-self-xl-start{-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-ms-flex-item-align:center!important;align-self:center!important}.align-self-xl-baseline{-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-ms-flex-item-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports ((position:-webkit-sticky) or (position:sticky)){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0!important}.mt-0,.my-0{margin-top:0!important}.mr-0,.mx-0{margin-right:0!important}.mb-0,.my-0{margin-bottom:0!important}.ml-0,.mx-0{margin-left:0!important}.m-1{margin:.25rem!important}.mt-1,.my-1{margin-top:.25rem!important}.mr-1,.mx-1{margin-right:.25rem!important}.mb-1,.my-1{margin-bottom:.25rem!important}.ml-1,.mx-1{margin-left:.25rem!important}.m-2{margin:.5rem!important}.mt-2,.my-2{margin-top:.5rem!important}.mr-2,.mx-2{margin-right:.5rem!important}.mb-2,.my-2{margin-bottom:.5rem!important}.ml-2,.mx-2{margin-left:.5rem!important}.m-3{margin:1rem!important}.mt-3,.my-3{margin-top:1rem!important}.mr-3,.mx-3{margin-right:1rem!important}.mb-3,.my-3{margin-bottom:1rem!important}.ml-3,.mx-3{margin-left:1rem!important}.m-4{margin:1.5rem!important}.mt-4,.my-4{margin-top:1.5rem!important}.mr-4,.mx-4{margin-right:1.5rem!important}.mb-4,.my-4{margin-bottom:1.5rem!important}.ml-4,.mx-4{margin-left:1.5rem!important}.m-5{margin:3rem!important}.mt-5,.my-5{margin-top:3rem!important}.mr-5,.mx-5{margin-right:3rem!important}.mb-5,.my-5{margin-bottom:3rem!important}.ml-5,.mx-5{margin-left:3rem!important}.p-0{padding:0!important}.pt-0,.py-0{padding-top:0!important}.pr-0,.px-0{padding-right:0!important}.pb-0,.py-0{padding-bottom:0!important}.pl-0,.px-0{padding-left:0!important}.p-1{padding:.25rem!important}.pt-1,.py-1{padding-top:.25rem!important}.pr-1,.px-1{padding-right:.25rem!important}.pb-1,.py-1{padding-bottom:.25rem!important}.pl-1,.px-1{padding-left:.25rem!important}.p-2{padding:.5rem!important}.pt-2,.py-2{padding-top:.5rem!important}.pr-2,.px-2{padding-right:.5rem!important}.pb-2,.py-2{padding-bottom:.5rem!important}.pl-2,.px-2{padding-left:.5rem!important}.p-3{padding:1rem!important}.pt-3,.py-3{padding-top:1rem!important}.pr-3,.px-3{padding-right:1rem!important}.pb-3,.py-3{padding-bottom:1rem!important}.pl-3,.px-3{padding-left:1rem!important}.p-4{padding:1.5rem!important}.pt-4,.py-4{padding-top:1.5rem!important}.pr-4,.px-4{padding-right:1.5rem!important}.pb-4,.py-4{padding-bottom:1.5rem!important}.pl-4,.px-4{padding-left:1.5rem!important}.p-5{padding:3rem!important}.pt-5,.py-5{padding-top:3rem!important}.pr-5,.px-5{padding-right:3rem!important}.pb-5,.py-5{padding-bottom:3rem!important}.pl-5,.px-5{padding-left:3rem!important}.m-auto{margin:auto!important}.mt-auto,.my-auto{margin-top:auto!important}.mr-auto,.mx-auto{margin-right:auto!important}.mb-auto,.my-auto{margin-bottom:auto!important}.ml-auto,.mx-auto{margin-left:auto!important}@media (min-width:576px){.m-sm-0{margin:0!important}.mt-sm-0,.my-sm-0{margin-top:0!important}.mr-sm-0,.mx-sm-0{margin-right:0!important}.mb-sm-0,.my-sm-0{margin-bottom:0!important}.ml-sm-0,.mx-sm-0{margin-left:0!important}.m-sm-1{margin:.25rem!important}.mt-sm-1,.my-sm-1{margin-top:.25rem!important}.mr-sm-1,.mx-sm-1{margin-right:.25rem!important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem!important}.ml-sm-1,.mx-sm-1{margin-left:.25rem!important}.m-sm-2{margin:.5rem!important}.mt-sm-2,.my-sm-2{margin-top:.5rem!important}.mr-sm-2,.mx-sm-2{margin-right:.5rem!important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem!important}.ml-sm-2,.mx-sm-2{margin-left:.5rem!important}.m-sm-3{margin:1rem!important}.mt-sm-3,.my-sm-3{margin-top:1rem!important}.mr-sm-3,.mx-sm-3{margin-right:1rem!important}.mb-sm-3,.my-sm-3{margin-bottom:1rem!important}.ml-sm-3,.mx-sm-3{margin-left:1rem!important}.m-sm-4{margin:1.5rem!important}.mt-sm-4,.my-sm-4{margin-top:1.5rem!important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem!important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem!important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem!important}.m-sm-5{margin:3rem!important}.mt-sm-5,.my-sm-5{margin-top:3rem!important}.mr-sm-5,.mx-sm-5{margin-right:3rem!important}.mb-sm-5,.my-sm-5{margin-bottom:3rem!important}.ml-sm-5,.mx-sm-5{margin-left:3rem!important}.p-sm-0{padding:0!important}.pt-sm-0,.py-sm-0{padding-top:0!important}.pr-sm-0,.px-sm-0{padding-right:0!important}.pb-sm-0,.py-sm-0{padding-bottom:0!important}.pl-sm-0,.px-sm-0{padding-left:0!important}.p-sm-1{padding:.25rem!important}.pt-sm-1,.py-sm-1{padding-top:.25rem!important}.pr-sm-1,.px-sm-1{padding-right:.25rem!important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem!important}.pl-sm-1,.px-sm-1{padding-left:.25rem!important}.p-sm-2{padding:.5rem!important}.pt-sm-2,.py-sm-2{padding-top:.5rem!important}.pr-sm-2,.px-sm-2{padding-right:.5rem!important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem!important}.pl-sm-2,.px-sm-2{padding-left:.5rem!important}.p-sm-3{padding:1rem!important}.pt-sm-3,.py-sm-3{padding-top:1rem!important}.pr-sm-3,.px-sm-3{padding-right:1rem!important}.pb-sm-3,.py-sm-3{padding-bottom:1rem!important}.pl-sm-3,.px-sm-3{padding-left:1rem!important}.p-sm-4{padding:1.5rem!important}.pt-sm-4,.py-sm-4{padding-top:1.5rem!important}.pr-sm-4,.px-sm-4{padding-right:1.5rem!important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem!important}.pl-sm-4,.px-sm-4{padding-left:1.5rem!important}.p-sm-5{padding:3rem!important}.pt-sm-5,.py-sm-5{padding-top:3rem!important}.pr-sm-5,.px-sm-5{padding-right:3rem!important}.pb-sm-5,.py-sm-5{padding-bottom:3rem!important}.pl-sm-5,.px-sm-5{padding-left:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto,.my-sm-auto{margin-top:auto!important}.mr-sm-auto,.mx-sm-auto{margin-right:auto!important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto!important}.ml-sm-auto,.mx-sm-auto{margin-left:auto!important}}@media (min-width:768px){.m-md-0{margin:0!important}.mt-md-0,.my-md-0{margin-top:0!important}.mr-md-0,.mx-md-0{margin-right:0!important}.mb-md-0,.my-md-0{margin-bottom:0!important}.ml-md-0,.mx-md-0{margin-left:0!important}.m-md-1{margin:.25rem!important}.mt-md-1,.my-md-1{margin-top:.25rem!important}.mr-md-1,.mx-md-1{margin-right:.25rem!important}.mb-md-1,.my-md-1{margin-bottom:.25rem!important}.ml-md-1,.mx-md-1{margin-left:.25rem!important}.m-md-2{margin:.5rem!important}.mt-md-2,.my-md-2{margin-top:.5rem!important}.mr-md-2,.mx-md-2{margin-right:.5rem!important}.mb-md-2,.my-md-2{margin-bottom:.5rem!important}.ml-md-2,.mx-md-2{margin-left:.5rem!important}.m-md-3{margin:1rem!important}.mt-md-3,.my-md-3{margin-top:1rem!important}.mr-md-3,.mx-md-3{margin-right:1rem!important}.mb-md-3,.my-md-3{margin-bottom:1rem!important}.ml-md-3,.mx-md-3{margin-left:1rem!important}.m-md-4{margin:1.5rem!important}.mt-md-4,.my-md-4{margin-top:1.5rem!important}.mr-md-4,.mx-md-4{margin-right:1.5rem!important}.mb-md-4,.my-md-4{margin-bottom:1.5rem!important}.ml-md-4,.mx-md-4{margin-left:1.5rem!important}.m-md-5{margin:3rem!important}.mt-md-5,.my-md-5{margin-top:3rem!important}.mr-md-5,.mx-md-5{margin-right:3rem!important}.mb-md-5,.my-md-5{margin-bottom:3rem!important}.ml-md-5,.mx-md-5{margin-left:3rem!important}.p-md-0{padding:0!important}.pt-md-0,.py-md-0{padding-top:0!important}.pr-md-0,.px-md-0{padding-right:0!important}.pb-md-0,.py-md-0{padding-bottom:0!important}.pl-md-0,.px-md-0{padding-left:0!important}.p-md-1{padding:.25rem!important}.pt-md-1,.py-md-1{padding-top:.25rem!important}.pr-md-1,.px-md-1{padding-right:.25rem!important}.pb-md-1,.py-md-1{padding-bottom:.25rem!important}.pl-md-1,.px-md-1{padding-left:.25rem!important}.p-md-2{padding:.5rem!important}.pt-md-2,.py-md-2{padding-top:.5rem!important}.pr-md-2,.px-md-2{padding-right:.5rem!important}.pb-md-2,.py-md-2{padding-bottom:.5rem!important}.pl-md-2,.px-md-2{padding-left:.5rem!important}.p-md-3{padding:1rem!important}.pt-md-3,.py-md-3{padding-top:1rem!important}.pr-md-3,.px-md-3{padding-right:1rem!important}.pb-md-3,.py-md-3{padding-bottom:1rem!important}.pl-md-3,.px-md-3{padding-left:1rem!important}.p-md-4{padding:1.5rem!important}.pt-md-4,.py-md-4{padding-top:1.5rem!important}.pr-md-4,.px-md-4{padding-right:1.5rem!important}.pb-md-4,.py-md-4{padding-bottom:1.5rem!important}.pl-md-4,.px-md-4{padding-left:1.5rem!important}.p-md-5{padding:3rem!important}.pt-md-5,.py-md-5{padding-top:3rem!important}.pr-md-5,.px-md-5{padding-right:3rem!important}.pb-md-5,.py-md-5{padding-bottom:3rem!important}.pl-md-5,.px-md-5{padding-left:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto,.my-md-auto{margin-top:auto!important}.mr-md-auto,.mx-md-auto{margin-right:auto!important}.mb-md-auto,.my-md-auto{margin-bottom:auto!important}.ml-md-auto,.mx-md-auto{margin-left:auto!important}}@media (min-width:992px){.m-lg-0{margin:0!important}.mt-lg-0,.my-lg-0{margin-top:0!important}.mr-lg-0,.mx-lg-0{margin-right:0!important}.mb-lg-0,.my-lg-0{margin-bottom:0!important}.ml-lg-0,.mx-lg-0{margin-left:0!important}.m-lg-1{margin:.25rem!important}.mt-lg-1,.my-lg-1{margin-top:.25rem!important}.mr-lg-1,.mx-lg-1{margin-right:.25rem!important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem!important}.ml-lg-1,.mx-lg-1{margin-left:.25rem!important}.m-lg-2{margin:.5rem!important}.mt-lg-2,.my-lg-2{margin-top:.5rem!important}.mr-lg-2,.mx-lg-2{margin-right:.5rem!important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem!important}.ml-lg-2,.mx-lg-2{margin-left:.5rem!important}.m-lg-3{margin:1rem!important}.mt-lg-3,.my-lg-3{margin-top:1rem!important}.mr-lg-3,.mx-lg-3{margin-right:1rem!important}.mb-lg-3,.my-lg-3{margin-bottom:1rem!important}.ml-lg-3,.mx-lg-3{margin-left:1rem!important}.m-lg-4{margin:1.5rem!important}.mt-lg-4,.my-lg-4{margin-top:1.5rem!important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem!important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem!important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem!important}.m-lg-5{margin:3rem!important}.mt-lg-5,.my-lg-5{margin-top:3rem!important}.mr-lg-5,.mx-lg-5{margin-right:3rem!important}.mb-lg-5,.my-lg-5{margin-bottom:3rem!important}.ml-lg-5,.mx-lg-5{margin-left:3rem!important}.p-lg-0{padding:0!important}.pt-lg-0,.py-lg-0{padding-top:0!important}.pr-lg-0,.px-lg-0{padding-right:0!important}.pb-lg-0,.py-lg-0{padding-bottom:0!important}.pl-lg-0,.px-lg-0{padding-left:0!important}.p-lg-1{padding:.25rem!important}.pt-lg-1,.py-lg-1{padding-top:.25rem!important}.pr-lg-1,.px-lg-1{padding-right:.25rem!important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem!important}.pl-lg-1,.px-lg-1{padding-left:.25rem!important}.p-lg-2{padding:.5rem!important}.pt-lg-2,.py-lg-2{padding-top:.5rem!important}.pr-lg-2,.px-lg-2{padding-right:.5rem!important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem!important}.pl-lg-2,.px-lg-2{padding-left:.5rem!important}.p-lg-3{padding:1rem!important}.pt-lg-3,.py-lg-3{padding-top:1rem!important}.pr-lg-3,.px-lg-3{padding-right:1rem!important}.pb-lg-3,.py-lg-3{padding-bottom:1rem!important}.pl-lg-3,.px-lg-3{padding-left:1rem!important}.p-lg-4{padding:1.5rem!important}.pt-lg-4,.py-lg-4{padding-top:1.5rem!important}.pr-lg-4,.px-lg-4{padding-right:1.5rem!important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem!important}.pl-lg-4,.px-lg-4{padding-left:1.5rem!important}.p-lg-5{padding:3rem!important}.pt-lg-5,.py-lg-5{padding-top:3rem!important}.pr-lg-5,.px-lg-5{padding-right:3rem!important}.pb-lg-5,.py-lg-5{padding-bottom:3rem!important}.pl-lg-5,.px-lg-5{padding-left:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto,.my-lg-auto{margin-top:auto!important}.mr-lg-auto,.mx-lg-auto{margin-right:auto!important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto!important}.ml-lg-auto,.mx-lg-auto{margin-left:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0!important}.mt-xl-0,.my-xl-0{margin-top:0!important}.mr-xl-0,.mx-xl-0{margin-right:0!important}.mb-xl-0,.my-xl-0{margin-bottom:0!important}.ml-xl-0,.mx-xl-0{margin-left:0!important}.m-xl-1{margin:.25rem!important}.mt-xl-1,.my-xl-1{margin-top:.25rem!important}.mr-xl-1,.mx-xl-1{margin-right:.25rem!important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem!important}.ml-xl-1,.mx-xl-1{margin-left:.25rem!important}.m-xl-2{margin:.5rem!important}.mt-xl-2,.my-xl-2{margin-top:.5rem!important}.mr-xl-2,.mx-xl-2{margin-right:.5rem!important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem!important}.ml-xl-2,.mx-xl-2{margin-left:.5rem!important}.m-xl-3{margin:1rem!important}.mt-xl-3,.my-xl-3{margin-top:1rem!important}.mr-xl-3,.mx-xl-3{margin-right:1rem!important}.mb-xl-3,.my-xl-3{margin-bottom:1rem!important}.ml-xl-3,.mx-xl-3{margin-left:1rem!important}.m-xl-4{margin:1.5rem!important}.mt-xl-4,.my-xl-4{margin-top:1.5rem!important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem!important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem!important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem!important}.m-xl-5{margin:3rem!important}.mt-xl-5,.my-xl-5{margin-top:3rem!important}.mr-xl-5,.mx-xl-5{margin-right:3rem!important}.mb-xl-5,.my-xl-5{margin-bottom:3rem!important}.ml-xl-5,.mx-xl-5{margin-left:3rem!important}.p-xl-0{padding:0!important}.pt-xl-0,.py-xl-0{padding-top:0!important}.pr-xl-0,.px-xl-0{padding-right:0!important}.pb-xl-0,.py-xl-0{padding-bottom:0!important}.pl-xl-0,.px-xl-0{padding-left:0!important}.p-xl-1{padding:.25rem!important}.pt-xl-1,.py-xl-1{padding-top:.25rem!important}.pr-xl-1,.px-xl-1{padding-right:.25rem!important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem!important}.pl-xl-1,.px-xl-1{padding-left:.25rem!important}.p-xl-2{padding:.5rem!important}.pt-xl-2,.py-xl-2{padding-top:.5rem!important}.pr-xl-2,.px-xl-2{padding-right:.5rem!important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem!important}.pl-xl-2,.px-xl-2{padding-left:.5rem!important}.p-xl-3{padding:1rem!important}.pt-xl-3,.py-xl-3{padding-top:1rem!important}.pr-xl-3,.px-xl-3{padding-right:1rem!important}.pb-xl-3,.py-xl-3{padding-bottom:1rem!important}.pl-xl-3,.px-xl-3{padding-left:1rem!important}.p-xl-4{padding:1.5rem!important}.pt-xl-4,.py-xl-4{padding-top:1.5rem!important}.pr-xl-4,.px-xl-4{padding-right:1.5rem!important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem!important}.pl-xl-4,.px-xl-4{padding-left:1.5rem!important}.p-xl-5{padding:3rem!important}.pt-xl-5,.py-xl-5{padding-top:3rem!important}.pr-xl-5,.px-xl-5{padding-right:3rem!important}.pb-xl-5,.py-xl-5{padding-bottom:3rem!important}.pl-xl-5,.px-xl-5{padding-left:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto,.my-xl-auto{margin-top:auto!important}.mr-xl-auto,.mx-xl-auto{margin-right:auto!important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto!important}.ml-xl-auto,.mx-xl-auto{margin-left:auto!important}}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-light{font-weight:300!important}.font-weight-normal{font-weight:400!important}.font-weight-bold{font-weight:700!important}.font-italic{font-style:italic!important}.text-white{color:#fff!important}.text-primary{color:#007bff!important}a.text-primary:focus,a.text-primary:hover{color:#0062cc!important}.text-secondary{color:#6c757d!important}a.text-secondary:focus,a.text-secondary:hover{color:#545b62!important}.text-success{color:#28a745!important}a.text-success:focus,a.text-success:hover{color:#1e7e34!important}.text-info{color:#17a2b8!important}a.text-info:focus,a.text-info:hover{color:#117a8b!important}.text-warning{color:#ffc107!important}a.text-warning:focus,a.text-warning:hover{color:#d39e00!important}.text-danger{color:#dc3545!important}a.text-danger:focus,a.text-danger:hover{color:#bd2130!important}.text-light{color:#f8f9fa!important}a.text-light:focus,a.text-light:hover{color:#dae0e5!important}.text-dark{color:#343a40!important}a.text-dark:focus,a.text-dark:hover{color:#1d2124!important}.text-body{color:#212529!important}.text-muted{color:#6c757d!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media print{*,::after,::before{text-shadow:none!important;box-shadow:none!important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px!important}.container{min-width:992px!important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #dee2e6!important}.table-dark{color:inherit}.table-dark tbody+tbody,.table-dark td,.table-dark th,.table-dark thead th{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} +/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/almanac-web/src/components/HeaderList.js b/almanac-web/src/components/HeaderList.js new file mode 100644 index 0000000..f6d0023 --- /dev/null +++ b/almanac-web/src/components/HeaderList.js @@ -0,0 +1,57 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + + const HeaderList = props => { + return ( +
    + +
  • + + Home + +
  • + + {!props.currentUser && +
  • + + Sign in + +
  • } + + {!props.currentUser && +
  • + + Sign up + +
  • + } + + {props.currentUser && +
  • + +  New Post + +
  • + } + {props.currentUser && +
  • + +  Settings + +
  • + } + + {props.currentUser && +
  • + + {props.currentUser.username} + {props.currentUser.username} + +
  • + } +
+ ); +} +export default HeaderList; diff --git a/almanac-web/src/components/ListPagination.js b/almanac-web/src/components/ListPagination.js new file mode 100644 index 0000000..4296588 --- /dev/null +++ b/almanac-web/src/components/ListPagination.js @@ -0,0 +1,55 @@ +import React from 'react'; +import agent from '../agent'; +import { connect } from 'react-redux'; +import { SET_PAGE } from '../constants/actionTypes'; +import { PAGE_SIZE } from '../constants/commonConstants'; + +const mapDispatchToProps = dispatch => ({ + onSetPage: (page, payload) => + dispatch({ type: SET_PAGE, page, payload }) +}); + +const ListPagination = props => { + if (props.booksCount <= PAGE_SIZE) { + return null; + } + + const range = []; + for (let i = 0; i < props.pager.totalPages; ++i) { + range.push(i); + } + + const setPage = page => { + props.onSetPage(page, agent.Books.all(page)) + }; + + return ( + + ); +}; + +export default connect(() => ({}), mapDispatchToProps)(ListPagination); diff --git a/almanac-web/src/constants/actionTypes.js b/almanac-web/src/constants/actionTypes.js new file mode 100644 index 0000000..f8cbc0a --- /dev/null +++ b/almanac-web/src/constants/actionTypes.js @@ -0,0 +1,45 @@ +export const APP_LOAD = 'APP_LOAD'; +export const REDIRECT = 'REDIRECT'; +export const ARTICLE_SUBMITTED = 'ARTICLE_SUBMITTED'; +export const SETTINGS_SAVED = 'SETTINGS_SAVED'; +export const SETTINGS_PAGE_UNLOADED = 'SETTINGS_PAGE_UNLOADED'; +export const HOME_PAGE_LOADED = 'HOME_PAGE_LOADED'; +export const HOME_PAGE_UNLOADED = 'HOME_PAGE_UNLOADED'; +export const BOOK_PAGE_LOADED = 'BOOK_PAGE_LOADED'; +export const BOOK_PAGE_UNLOADED = 'BOOK_PAGE_UNLOADED'; +export const BOOK_ARTICLES_LOADED = 'BOOK_PAGE_LOADED'; +export const ADD_COMMENT = 'ADD_COMMENT'; +export const DELETE_COMMENT = 'DELETE_COMMENT'; +export const ARTICLE_FAVORITED = 'ARTICLE_FAVORITED'; +export const ARTICLE_UNFAVORITED = 'ARTICLE_UNFAVORITED'; +export const SET_PAGE = 'SET_PAGE'; +export const APPLY_TAG_FILTER = 'APPLY_TAG_FILTER'; +export const CHANGE_TAB = 'CHANGE_TAB'; +export const PROFILE_PAGE_LOADED = 'PROFILE_PAGE_LOADED'; +export const PROFILE_PAGE_UNLOADED = 'PROFILE_PAGE_UNLOADED'; +export const LOGIN = 'LOGIN'; +export const LOGOUT = 'LOGOUT'; +export const REGISTER = 'REGISTER'; +export const LOGIN_PAGE_UNLOADED = 'LOGIN_PAGE_UNLOADED'; +export const REGISTER_PAGE_UNLOADED = 'REGISTER_PAGE_UNLOADED'; +export const ASYNC_START = 'ASYNC_START'; +export const ASYNC_END = 'ASYNC_END'; +export const EDITOR_PAGE_LOADED = 'EDITOR_PAGE_LOADED'; +export const EDITOR_PAGE_UNLOADED = 'EDITOR_PAGE_UNLOADED'; +export const ADD_TAG = 'ADD_TAG'; +export const REMOVE_TAG = 'REMOVE_TAG'; +export const UPDATE_FIELD_AUTH = 'UPDATE_FIELD_AUTH'; +export const UPDATE_FIELD_EDITOR = 'UPDATE_FIELD_EDITOR'; +export const FOLLOW_USER = 'FOLLOW_USER'; +export const UNFOLLOW_USER = 'UNFOLLOW_USER'; +export const PROFILE_FAVORITES_PAGE_UNLOADED = 'PROFILE_FAVORITES_PAGE_UNLOADED'; +export const PROFILE_FAVORITES_PAGE_LOADED = 'PROFILE_FAVORITES_PAGE_LOADED'; + +export const ARTICLE_PAGE_LOADED = 'ARTICLE_PAGE_LOADED'; +export const ARTICLE_PAGE_UNLOADED = 'ARTICLE_PAGE_UNLOADED'; +export const CREATE_BOOK = 'CREATE_BOOK'; +export const CREATE_ARTICLE = 'CREATE_ARTICLE'; +export const DELETE_ARTICLE = 'DELETE_ARTICLE'; +export const UPDATE_ARTICLE = 'UPDATE_ARTICLE'; +export const BOOK_UPDATED = 'BOOK_UPDATED'; + diff --git a/almanac-web/src/constants/commonConstants.js b/almanac-web/src/constants/commonConstants.js new file mode 100644 index 0000000..2f38546 --- /dev/null +++ b/almanac-web/src/constants/commonConstants.js @@ -0,0 +1 @@ +export const PAGE_SIZE = 2; \ No newline at end of file diff --git a/almanac-web/src/containers/Article/ArticleCreate.js b/almanac-web/src/containers/Article/ArticleCreate.js new file mode 100644 index 0000000..7040ed8 --- /dev/null +++ b/almanac-web/src/containers/Article/ArticleCreate.js @@ -0,0 +1,66 @@ +import React from 'react'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + CREATE_ARTICLE +} from '../../constants/actionTypes'; +import { Editor } from '@tinymce/tinymce-react'; + +class ArticleCreate extends React.Component { + constructor() { + super(); + this.state = { + article: {} + } + this.updateField = this.updateField.bind(this); + this.createArticle = this.createArticle.bind(this); + }; + createArticle() { + var article = this.state.article; + const payload = agent.Articles.create(this.props.match.params.bookId, + { ...article }); + this.setState({ article: {} }); + this.props.history.push(`/books/${this.props.match.params.bookId}/edit`) + }; + updateField(event) { + var article = { ...this.state.article, [event.target.name]: event.target.value }; + this.setState( + { article: article } + ); + } + render() { + var article = this.state.article; + if (this.state.redirectTo) { + } + return ( +
+
+ + +
+
+ + +
+
+ +
+
+ +
+
+ ); + } +} +export default ArticleCreate; diff --git a/almanac-web/src/containers/Article/ArticleEdit.js b/almanac-web/src/containers/Article/ArticleEdit.js new file mode 100644 index 0000000..ab4eb9e --- /dev/null +++ b/almanac-web/src/containers/Article/ArticleEdit.js @@ -0,0 +1,114 @@ +import React from 'react'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + UPDATE_ARTICLE, ARTICLE_PAGE_LOADED, + ARTICLE_PAGE_UNLOADED +} from '../../constants/actionTypes'; +import RichTextEditor from 'react-rte'; + +const mapStateToProps = state => ({ + ...state.article, + currentUser: state.common.currentUser +}); +const mapDispatchToProps = dispatch => ({ + onLoad: payload => + dispatch({ type: ARTICLE_PAGE_LOADED, payload }), + onUnload: () => + dispatch({ type: ARTICLE_PAGE_UNLOADED }), + onSubmit: payload => { dispatch({ type: UPDATE_ARTICLE, payload }) } +}); +class ArticleCreate extends React.Component { + constructor() { + super(); + this.state = { + article: { content: RichTextEditor.createEmptyValue() } + }; + this.updateArticle = this.updateArticle.bind(this); + }; + + componentWillMount() { + var getArticle = agent.Articles.get(this.props.match.params.bookId, this.props.match.params.articleId); + this.props.onLoad(Promise.all([getArticle])); + } + componentWillReceiveProps(newProps) { + var article = { ...newProps.article }; + article.content= RichTextEditor.createValueFromString(article.content,"html") + this.setState({ article: article }) + } + componentWillUnmount() { + this.props.onUnload(); + } + updateArticle() { + var article = this.state.article; + article.content = this.state.article.content.toString('html'); + const payload = agent.Articles.update(this.props.match.params.bookId, this.props.match.params.articleId, + { ...article }); + + this.setState({ article: {} }); + this.props.history.push(`/books/${this.props.match.params.bookId}`) + }; + onChangeRte = (value) => { + var article = { ...this.state.article,content: value }; + this.setState({ article: article }); + }; + updateField=(event)=> { + var article = { ...this.state.article, [event.target.name]: event.target.value }; + this.setState( + { article: article } + ); + } + render() { + var article = this.state.article; + return ( +
+
+
+
+ + +
+
+
+
+
+
+
+
+

+ Annotation: +

+

+ +

+
+
+
+ + +
+
+
+ +
+
+
+ ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(ArticleCreate); diff --git a/almanac-web/src/containers/Article/ArticleList.js b/almanac-web/src/containers/Article/ArticleList.js new file mode 100644 index 0000000..a2634e1 --- /dev/null +++ b/almanac-web/src/containers/Article/ArticleList.js @@ -0,0 +1,56 @@ +import ArticlePreview from './ArticlePreview'; +import React from 'react'; +import { connect } from 'react-redux'; +import axios from 'axios'; + + +const mapStateToProps = state => ({ + ...state, + link: state.link, + currentUser: state.common.currentUser +}); +class ArticleList extends React.Component { + constructor() { + super(); + this.state = { + articles: [] + + } + } + componentWillMount() { + if (this.props.book) { + axios.get(this.props.book.book.articles.href) + .then(response => this.setState({ ...this.state, articles: response.data })) + } + } + + + render() { + var articles = this.state.articles; + if (!articles) { + return ( +
Loading...
+ ); + } + + if (articles.length === 0) { + return ( +
+ No articles are here... yet. +
+ ); + } + return ( +
    + { + articles.map(article => { + return ( +
  1. + ); + }) + } +
+ ); + } +} +export default connect(mapStateToProps, null)(ArticleList); diff --git a/almanac-web/src/containers/Article/ArticlePreview.js b/almanac-web/src/containers/Article/ArticlePreview.js new file mode 100644 index 0000000..3294f3a --- /dev/null +++ b/almanac-web/src/containers/Article/ArticlePreview.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import Moment from 'react-moment'; +import 'moment-timezone'; + +const ArticlePreview = props => { + const article = props.article; + return ( +
+
+
+ +
{article.title}
+ + {article.created && +
} +
+
+
+ ); +} + +export default ArticlePreview; diff --git a/almanac-web/src/containers/Article/index.js b/almanac-web/src/containers/Article/index.js new file mode 100644 index 0000000..0473c67 --- /dev/null +++ b/almanac-web/src/containers/Article/index.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import agent from '../../agent'; +import { Link } from 'react-router-dom'; +import { ARTICLE_PAGE_LOADED, ARTICLE_PAGE_UNLOADED, DELETE_ARTICLE } from '../../constants/actionTypes'; + +const mapStateToProps = state => ({ + ...state.article, + currentUser: state.common.currentUser +}); + +const mapDispatchToProps = dispatch => ({ + onLoad: payload => + dispatch({ type: ARTICLE_PAGE_LOADED, payload }), + onDeleteArticle: payload => dispatch({ + type: ARTICLE_PAGE_LOADED, payload + }), + onUnload: () => + dispatch({ type: ARTICLE_PAGE_UNLOADED }) +}); + +class Book extends React.Component { + constructor() { + super(); + this.deleteArticle = this.deleteArticle.bind(this); + } + componentWillMount() { + this.props.onLoad(Promise.all([agent.Articles.get(this.props.match.params.bookId, this.props.match.params.articleId)])); + } + deleteArticle() { + var deleteArticle = agent.Articles.del(this.props.match.params.bookId, this.props.match.params.articleId); + this.props.onDeleteArticle(Promise.all([deleteArticle])); + this.props.history.push(`/books/${this.props.match.params.bookId}`) + } + + componentWillUnmount() { + this.props.onUnload(); + } + render() { + if (!this.props.article) { + return null; + } + var article = this.props.article; + return ( +
+
+
+

{article.title}

+
+
+ edit +
+ +
+
+ to book +
+
+
+

Annotation:

+

+ {article.annotation} +

+
+
+
+
+

+
+
+
+
+ ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(Book); \ No newline at end of file diff --git a/almanac-web/src/containers/Authentication/ListErrors.js b/almanac-web/src/containers/Authentication/ListErrors.js new file mode 100644 index 0000000..3b40022 --- /dev/null +++ b/almanac-web/src/containers/Authentication/ListErrors.js @@ -0,0 +1,26 @@ +import React from 'react'; + +class ListErrors extends React.Component { + render() { + const errors = this.props.errors; + if (errors) { + return ( +
    + { + Object.keys(errors).map(key => { + return ( +
  • + {key} {errors[key]} +
  • + ); + }) + } +
+ ); + } else { + return null; + } + } +} + +export default ListErrors; diff --git a/almanac-web/src/containers/Authentication/Login.js b/almanac-web/src/containers/Authentication/Login.js new file mode 100644 index 0000000..e0fa092 --- /dev/null +++ b/almanac-web/src/containers/Authentication/Login.js @@ -0,0 +1,97 @@ +import { Link } from 'react-router-dom'; +import ListErrors from './ListErrors'; +import React from 'react'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + UPDATE_FIELD_AUTH, + LOGIN, + LOGIN_PAGE_UNLOADED +} from '../../constants/actionTypes'; + +const mapStateToProps = state => ({ ...state.auth }); + +const mapDispatchToProps = dispatch => ({ + onChangeEmail: value => + dispatch({ type: UPDATE_FIELD_AUTH, key: 'email', value }), + onChangePassword: value => + dispatch({ type: UPDATE_FIELD_AUTH, key: 'password', value }), + onSubmit: (email, password) => + dispatch({ type: LOGIN, payload: agent.Auth.login(email, password) }), + onUnload: () => + dispatch({ type: LOGIN_PAGE_UNLOADED }) +}); + +class Login extends React.Component { + constructor() { + super(); + this.changeEmail = ev => this.props.onChangeEmail(ev.target.value); + this.changePassword = ev => this.props.onChangePassword(ev.target.value); + this.submitForm = (email, password) => ev => { + ev.preventDefault(); + this.props.onSubmit(email, password); + }; + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + const email = this.props.email; + const password = this.props.password; + return ( +
+
+
+ +
+

Sign In

+

+ + Need an account? + +

+ + + +
+
+ +
+ +
+ +
+ +
+ + + +
+
+
+ +
+
+
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/almanac-web/src/containers/Authentication/Register.js b/almanac-web/src/containers/Authentication/Register.js new file mode 100644 index 0000000..af063fb --- /dev/null +++ b/almanac-web/src/containers/Authentication/Register.js @@ -0,0 +1,113 @@ +import { Link } from 'react-router-dom'; +import ListErrors from './ListErrors'; +import React from 'react'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + UPDATE_FIELD_AUTH, + REGISTER, + REGISTER_PAGE_UNLOADED +} from '../../constants/actionTypes'; + +const mapStateToProps = state => ({ ...state.auth }); + +const mapDispatchToProps = dispatch => ({ + onChangeEmail: value => + dispatch({ type: UPDATE_FIELD_AUTH, key: 'email', value }), + onChangePassword: value => + dispatch({ type: UPDATE_FIELD_AUTH, key: 'password', value }), + onChangeUsername: value => + dispatch({ type: UPDATE_FIELD_AUTH, key: 'username', value }), + onSubmit: (username, email, password) => { + const payload = agent.Auth.register(username, email, password); + dispatch({ type: REGISTER, payload }) + }, + onUnload: () => + dispatch({ type: REGISTER_PAGE_UNLOADED }) +}); + +class Register extends React.Component { + constructor() { + super(); + this.changeEmail = ev => this.props.onChangeEmail(ev.target.value); + this.changePassword = ev => this.props.onChangePassword(ev.target.value); + this.changeUsername = ev => this.props.onChangeUsername(ev.target.value); + this.submitForm = (username, email, password) => ev => { + ev.preventDefault(); + this.props.onSubmit(username, email, password); + } + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + const email = this.props.email; + const password = this.props.password; + const username = this.props.username; + + return ( +
+
+
+ +
+

Sign Up

+

+ + Have an account? + +

+ + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ +
+
+
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Register); diff --git a/almanac-web/src/containers/Book/BookCreate.js b/almanac-web/src/containers/Book/BookCreate.js new file mode 100644 index 0000000..47f2039 --- /dev/null +++ b/almanac-web/src/containers/Book/BookCreate.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import axios from 'axios'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + CREATE_BOOK +} from '../../constants/actionTypes'; + +const mapStateToProps = state => ({ + ...state.book, + currentUser: state.common.currentUser +}); +const mapDispatchToProps = dispatch => ({ + onSubmit: payload => + dispatch({ type: CREATE_BOOK, payload }) +}); +class BookCreate extends React.Component { + constructor() { + super(); + this.state = { + book: {} + } + this.updateField = this.updateField.bind(this); + this.createBook = this.createBook.bind(this); + }; + createBook() { + var book = this.state.book; + book.author = this.props.currentUser; + const payload = agent.Books.create( + { ...book }); + this.setState({ book: {}}); + + this.props.history.push(`/`) + }; + updateField(event) { + var book = { ...this.state.book, [event.target.name]: event.target.value }; + this.setState( + { book: book } + ); + } + render() { + var book = this.props.book; + + return ( +
+
+ + +
+
+ +
+
+ +
+ +
+ +
+
+ + ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(BookCreate); diff --git a/almanac-web/src/containers/Book/BookEdit.js b/almanac-web/src/containers/Book/BookEdit.js new file mode 100644 index 0000000..b34f72e --- /dev/null +++ b/almanac-web/src/containers/Book/BookEdit.js @@ -0,0 +1,148 @@ +import ArticleList from '../Article/ArticleList'; +import React from 'react'; +import { connect } from 'react-redux'; +import agent from '../../agent'; +import { Link } from 'react-router-dom'; +import { + BOOK_PAGE_LOADED, + BOOK_PAGE_UNLOADED, + BOOK_UPDATED +} from '../../constants/actionTypes'; +import ProfilePreview from '../Profile/ProfilePreview'; + +const mapStateToProps = state => ({ + ...state.book, + currentUser: state.common.currentUser +}); + +const mapDispatchToProps = dispatch => ({ + onLoad: payload => + dispatch({ type: BOOK_PAGE_LOADED, payload }), + onSubmit: payload => + dispatch({ type: BOOK_UPDATED, payload }), + onUnload: () => + dispatch({ type: BOOK_PAGE_UNLOADED }) +}); + + +class BookEdit extends React.Component { + constructor() { + super(); + this.state = {} + this.updateField = this.updateField.bind(this); + this.updateBook = this.updateBook.bind(this); + }; + componentWillMount() { + var getBook = agent.Books.get(this.props.match.params.id); + this.props.onLoad(Promise.all([getBook])); + } + componentWillReceiveProps(newProps) { + this.setState({ book: newProps.book }) + } + componentWillUnmount() { + this.props.onUnload(); + } + updateField(event) { + var book = { ...this.state.book, [event.target.name]: event.target.value }; + this.setState( + { book: book } + ); + } + updateBook() { + var book = this.state.book; + book.author = this.props.currentUser; + const payload = agent.Books.update(this.props.match.params.id, + { ...book }); + this.setState({ book: {} }); + this.props.onSubmit(payload); + this.props.history.push(`/books/${this.props.match.params.id}`) + }; + render() { + if (!this.props.book) { + return null; + } + var book = this.state.book ? this.state.book : {}; + return ( +
+
+
+ + +
+
+
+ {book.author && book.author.href + &&
+ Author: +
+ } +
+
+ {book.subAuthors + && book.subAuthors.length !== 0 + &&
SubAuthors: + {book.subAuthors.map(subAuthor => +
+ +
+ )}
+ } + +
+
+
+
+
State:
+ + +
+
+
+

Description

+
+ +
+
+
+
+
+ { + + } +
+ + Create article +
+
+ +
+
+
+
+ ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(BookEdit); \ No newline at end of file diff --git a/almanac-web/src/containers/Book/BookList.js b/almanac-web/src/containers/Book/BookList.js new file mode 100644 index 0000000..75dbb1e --- /dev/null +++ b/almanac-web/src/containers/Book/BookList.js @@ -0,0 +1,38 @@ +import BookPreview from './BookPreview'; +import ListPagination from '../../components/ListPagination'; +import React from 'react'; + +const BookList = props => { + if (!props.books) { + return ( +
Loading...
+ ); + } + + if (props.books.length === 0) { + return ( +
+ No books are here... yet. +
+ ); + } + + return ( +
+ { + props.books.map(book => { + return ( + + ); + }) + } + + +
+ ); +}; + +export default BookList; diff --git a/almanac-web/src/containers/Book/BookPreview.js b/almanac-web/src/containers/Book/BookPreview.js new file mode 100644 index 0000000..785c4ae --- /dev/null +++ b/almanac-web/src/containers/Book/BookPreview.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import axios from 'axios'; +import { connect } from 'react-redux'; +import Profilepreview from '../Profile/ProfilePreview'; + +const mapStateToProps = state => ({ + ...state.book, + currentUser: state.common.currentUser +}); + +class BookPreview extends React.Component { + constructor() { + super(); + this.state = { + author: null + } + } + componentWillMount() { + if (this.props.book) { + axios.get(this.props.book.author && this.props.book.author.href) + .then(response => { + this.setState({ ...this.state, author: response.data }) + }) + } + } + + render() { + var book = this.props.book; + + return ( +
+
+
+ +

{book.title}

+ +
+

{book.description}

+
+
+
+ + ); + } +} +export default connect(mapStateToProps, null)(BookPreview); diff --git a/almanac-web/src/containers/Book/index.js b/almanac-web/src/containers/Book/index.js new file mode 100644 index 0000000..d8f3dd5 --- /dev/null +++ b/almanac-web/src/containers/Book/index.js @@ -0,0 +1,110 @@ +import ArticleList from '../Article/ArticleList'; +import React from 'react'; +import { connect } from 'react-redux'; +import agent from '../../agent'; +import { Link } from 'react-router-dom'; +import { + BOOK_PAGE_LOADED, + BOOK_PAGE_UNLOADED +} from '../../constants/actionTypes'; +import ProfilePreview from '../Profile/ProfilePreview'; +import axios from 'axios'; + +const mapStateToProps = state => ({ + ...state.book, + currentUser: state.common.currentUser +}); + +const mapDispatchToProps = dispatch => ({ + onLoad: payload => + dispatch({ type: BOOK_PAGE_LOADED, payload }), + onUnload: () => + dispatch({ type: BOOK_PAGE_UNLOADED }) +}); + + +class Book extends React.Component { + constructor() { + super(); + this.downloadBook = this.downloadBook.bind(this); + } + downloadBook() { + var links = this.props.book._links; + axios.get(links && links.download.href) + } + componentWillMount() { + var getBook = agent.Books.get(this.props.match.params.id); + this.props.onLoad(Promise.all([getBook])); + } + componentWillUnmount() { + this.props.onUnload(); + } + render() { + if (!this.props.book) { + return null; + } + var book = this.props.book; + return ( +
+
+
+

{book.title}

+
+
+ + Edit + {this.props.book._links + && this.props.book._links.download + && + download} +
+
+
+
+ {book.author && book.author.href + &&
+ Author: +
+ } +
+
+ {book.subAuthors + && book.subAuthors.length !== 0 + &&
SubAuthors: + {book.subAuthors.map(subAuthor => +
+ +
+ )}
+ } + +
+
+
+
+

Size: {book.size}

+

State: {book.state}

+ +
+
+
+

Description

+

+ {book.description} +

+
+
+
+
+ { + + } +
+
+ ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(Book); \ No newline at end of file diff --git a/almanac-web/src/containers/Home/Banner.js b/almanac-web/src/containers/Home/Banner.js new file mode 100644 index 0000000..43a9220 --- /dev/null +++ b/almanac-web/src/containers/Home/Banner.js @@ -0,0 +1,20 @@ +import React from 'react'; + +const Banner = ({ appName, token }) => { + if (token) { + return null; + } + return ( +
+
+

+ {appName.toLowerCase()} +

+

A place to share your imagination.

+
+
+ ); +}; + +export default Banner; + diff --git a/almanac-web/src/containers/Home/MainView.js b/almanac-web/src/containers/Home/MainView.js new file mode 100644 index 0000000..90898fe --- /dev/null +++ b/almanac-web/src/containers/Home/MainView.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import BookList from '../Book/BookList'; +import { CHANGE_TAB } from '../../constants/actionTypes'; + +const mapStateToProps = state => ({ + ...state.bookList, + token: state.common.token +}); + +const mapDispatchToProps = dispatch => ({ + onTabClick: (tab, pager, payload) => dispatch({ type: CHANGE_TAB, tab, pager, payload }) +}); + +const MainView = props => { + return ( +
+
+ +
+
+ ); +}; + +export default connect(mapStateToProps, mapDispatchToProps)(MainView); diff --git a/almanac-web/src/containers/Home/Tags.js b/almanac-web/src/containers/Home/Tags.js new file mode 100644 index 0000000..fb498ae --- /dev/null +++ b/almanac-web/src/containers/Home/Tags.js @@ -0,0 +1,36 @@ +import React from 'react'; +import agent from '../../agent'; + +const Tags = props => { + const tags = props.tags; + if (tags) { + return ( +
+ { + tags.map(tag => { + const handleClick = ev => { + ev.preventDefault(); + props.onClickTag(tag, page => agent.Articles.byTag(tag, page), agent.Articles.byTag(tag)); + }; + + return ( + + {tag} + + ); + }) + } +
+ ); + } else { + return ( +
Loading Tags...
+ ); + } +}; + +export default Tags; diff --git a/almanac-web/src/containers/Home/index.js b/almanac-web/src/containers/Home/index.js new file mode 100644 index 0000000..1191e0c --- /dev/null +++ b/almanac-web/src/containers/Home/index.js @@ -0,0 +1,58 @@ +import Banner from './Banner'; +import MainView from './MainView'; +import React from 'react'; +import agent from '../../agent'; +import { connect } from 'react-redux'; +import { + HOME_PAGE_LOADED, + HOME_PAGE_UNLOADED +} from '../../constants/actionTypes'; + +const Promise = global.Promise; + +const mapStateToProps = state => ({ + ...state.home, + appName: state.common.appName, + token: state.common.token, + books: state.bookList.books, + currentPage: state.bookList.currentPage, + booksCount: state.bookList.booksCount +}); + +const mapDispatchToProps = dispatch => ({ + onLoad: ( pager, payload) => + dispatch({ type: HOME_PAGE_LOADED, pager, payload }), + onUnload: () => + dispatch({ type: HOME_PAGE_UNLOADED }) +}); + +class Home extends React.Component { + componentWillMount() { + const booksPromise = agent.Books.all; + this.props.onLoad(booksPromise, Promise.all([booksPromise()])); + } + + componentWillUnmount() { + this.props.onUnload(); + } + + render() { + return ( +
+ + + +
+
+ +
+
+ +
+ ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Home); diff --git a/almanac-web/src/containers/Profile/ProfilePreview.js b/almanac-web/src/containers/Profile/ProfilePreview.js new file mode 100644 index 0000000..2dc0ff9 --- /dev/null +++ b/almanac-web/src/containers/Profile/ProfilePreview.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import axios from 'axios'; +import { connect } from 'react-redux'; +import auth from '../../reducers/auth'; + +const mapStateToProps = state => ({ + ...state.book, + currentUser: state.common.currentUser +}); + +class ProfilePreview extends React.Component { + constructor() { + super(); + this.state = { + author: null + } + } + componentWillMount() { + console.dir(this.props.link) + if (this.props.link) { + axios.get(this.props.link) + .then(response => this.setState({ ...this.state, author: response.data })) + } + } + render() { + var author = this.state.author; + if (!author) { + return ( +
Loading...
+ ); + }; + return ( + + + {author.username} + + ); + }; +} + +export default connect(mapStateToProps, null)(ProfilePreview); diff --git a/almanac-web/src/containers/Profile/index.js b/almanac-web/src/containers/Profile/index.js new file mode 100644 index 0000000..a6a50e7 --- /dev/null +++ b/almanac-web/src/containers/Profile/index.js @@ -0,0 +1,100 @@ + +import React from 'react'; +import { connect } from 'react-redux'; +import agent from '../../agent'; +import { + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED +} from '../../constants/actionTypes'; +import axios from 'axios'; +import { Link } from 'react-router-dom'; + +const mapStateToProps = state => ({ + ...state.profile, + currentUser: state.common.currentUser +}); + +const mapDispatchToProps = dispatch => ({ + onLoad: payload => + dispatch({ type: PROFILE_PAGE_LOADED, payload }), + onUnload: () => + dispatch({ type: PROFILE_PAGE_UNLOADED }) +}); + + +class Profile extends React.Component { + constructor() { + super(); + this.state = { + booksAsAuthor: [], + booksAsSubAuthor: [] + } + } + componentWillMount() { + var getProfile = agent.Profile.get(this.props.match.params.id); + this.props.onLoad(Promise.all([getProfile])); + console.dir(this.props) + + } + componentWillUnmount() { + this.props.onUnload(); + } + render() { + if (!this.props.profile) { + return null; + } + var profile = this.props.profile; + var booksAsSubAuthor = profile.booksAsSubAuthor; + var booksAsAuthor = profile.booksAsAuthor; + return ( +
+
+
+

{profile.username}

+
+
+
+
+
About:
+
+
{profile.about}
+
+
+
Information:
+
{profile.information}
+
+
Books as author:
+ {booksAsAuthor.length === 0 && +
There is no any book as author
} +
    { + + booksAsAuthor && booksAsAuthor.map(book => +
  1. + +
    {book.title}
    + +
  2. + )} +
+
+
Books as coauthor:
+ {booksAsSubAuthor.length === 0 && +
There is no any book as coauthor
+ } + +
    { + booksAsSubAuthor && booksAsSubAuthor.map(book => +
  1. + +
    {book.title}
    + +
  2. + )} +
+
+
+ + ); + } +} +export default connect(mapStateToProps, mapDispatchToProps)(Profile); \ No newline at end of file diff --git a/almanac-web/src/index.js b/almanac-web/src/index.js new file mode 100644 index 0000000..ab262ef --- /dev/null +++ b/almanac-web/src/index.js @@ -0,0 +1,21 @@ +import ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import React from 'react'; +import { store, history } from './store'; + +import { Route, Switch } from 'react-router-dom'; +import { ConnectedRouter } from 'react-router-redux'; +import Book from './containers/Book'; + +import App from './App'; + +ReactDOM.render(( + + + + + + + + +), document.getElementById('root')); diff --git a/almanac-web/src/logo.svg b/almanac-web/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/almanac-web/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/almanac-web/src/middleware.js b/almanac-web/src/middleware.js new file mode 100644 index 0000000..070ad03 --- /dev/null +++ b/almanac-web/src/middleware.js @@ -0,0 +1,68 @@ +import agent from './agent'; +import { + ASYNC_START, + ASYNC_END, + LOGIN, + LOGOUT, + REGISTER +} from './constants/actionTypes'; + +const promiseMiddleware = store => next => action => { + if (isPromise(action.payload)) { + store.dispatch({ type: ASYNC_START, subtype: action.type }); + + const currentView = store.getState().viewChangeCounter; + const skipTracking = action.skipTracking; + + action.payload.then( + res => { + const currentState = store.getState() + if (!skipTracking && currentState.viewChangeCounter !== currentView) { + return + } + console.log('RESULT', res); + action.payload = res; + store.dispatch({ type: ASYNC_END, promise: action.payload }); + store.dispatch(action); + }, + error => { + const currentState = store.getState() + if (!skipTracking && currentState.viewChangeCounter !== currentView) { + return + } + console.log('ERROR', error); + action.error = true; + action.payload = error.response !==undefined ?error.response.body:error.message; + if (!action.skipTracking) { + store.dispatch({ type: ASYNC_END, promise: action.payload }); + } + store.dispatch(action); + } + ); + + return; + } + + next(action); +}; + +const localStorageMiddleware = store => next => action => { + if (action.type === REGISTER || action.type === LOGIN) { + if (!action.error) { + window.localStorage.setItem('jwt', action.payload.user.token); + agent.setToken(action.payload.user.token); + } + } else if (action.type === LOGOUT) { + window.localStorage.setItem('jwt', ''); + agent.setToken(null); + } + + next(action); +}; + +function isPromise(v) { + return v && typeof v.then === 'function'; +} + + +export { promiseMiddleware, localStorageMiddleware } diff --git a/almanac-web/src/reducer.js b/almanac-web/src/reducer.js new file mode 100644 index 0000000..80b0477 --- /dev/null +++ b/almanac-web/src/reducer.js @@ -0,0 +1,20 @@ +import auth from './reducers/auth'; +import bookList from './reducers/bookList'; +import { combineReducers } from 'redux'; +import common from './reducers/common'; +import home from './reducers/home'; +import profile from './reducers/profile'; +import book from './reducers/book'; +import article from './reducers/article'; +import { routerReducer } from 'react-router-redux'; + +export default combineReducers({ + auth, + common, + home, + profile, + book, + article, + bookList, + router: routerReducer +}); diff --git a/almanac-web/src/reducers/article.js b/almanac-web/src/reducers/article.js new file mode 100644 index 0000000..2a922ca --- /dev/null +++ b/almanac-web/src/reducers/article.js @@ -0,0 +1,37 @@ +import { + ARTICLE_PAGE_LOADED, + ARTICLE_PAGE_UNLOADED, + CREATE_ARTICLE +} from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case ARTICLE_PAGE_LOADED: { + var article = action.payload[0]; + return { + ...state, + article: article + }; + } + case ARTICLE_PAGE_UNLOADED: + return {}; + // case ADD_COMMENT: + // return { + // ...state, + // commentErrors: action.error ? action.payload.errors : null, + // comments: action.error ? + // null : + // (state.comments || []).concat([action.payload.comment]) + // }; + // case DELETE_COMMENT: + // const commentId = action.commentId + // return { + // ...state, + // comments: state.comments.filter(comment => comment.id !== commentId) + // }; + case CREATE_ARTICLE: + return { ...state, redirectTo: '/' }; + default: + return state; + } +}; diff --git a/almanac-web/src/reducers/auth.js b/almanac-web/src/reducers/auth.js new file mode 100644 index 0000000..6e83838 --- /dev/null +++ b/almanac-web/src/reducers/auth.js @@ -0,0 +1,34 @@ +import { + LOGIN, + REGISTER, + LOGIN_PAGE_UNLOADED, + REGISTER_PAGE_UNLOADED, + ASYNC_START, + UPDATE_FIELD_AUTH +} from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case LOGIN: + case REGISTER: + return { + ...state, + inProgress: false, + errors: action.error ? action.payload.errors : null + }; + case LOGIN_PAGE_UNLOADED: + case REGISTER_PAGE_UNLOADED: + return {}; + case ASYNC_START: + if (action.subtype === LOGIN || action.subtype === REGISTER) { + return { ...state, inProgress: true }; + } + break; + case UPDATE_FIELD_AUTH: + return { ...state, [action.key]: action.value }; + default: + return state; + } + + return state; +}; diff --git a/almanac-web/src/reducers/book.js b/almanac-web/src/reducers/book.js new file mode 100644 index 0000000..a57b8e1 --- /dev/null +++ b/almanac-web/src/reducers/book.js @@ -0,0 +1,40 @@ +import { + BOOK_PAGE_LOADED, + BOOK_PAGE_UNLOADED, + BOOK_ARTICLES_LOADED, + CREATE_ARTICLE +} from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case BOOK_PAGE_LOADED: { + var book = action.payload[0]; + return { + ...state, + book: book + }; + } + case CREATE_ARTICLE: + return { + ...state, redirectTo: '/' + }; + case BOOK_PAGE_UNLOADED: + return {}; + // case ADD_COMMENT: + // return { + // ...state, + // commentErrors: action.error ? action.payload.errors : null, + // comments: action.error ? + // null : + // (state.comments || []).concat([action.payload.comment]) + // }; + // case DELETE_COMMENT: + // const commentId = action.commentId + // return { + // ...state, + // comments: state.comments.filter(comment => comment.id !== commentId) + // }; + default: + return state; + } +}; diff --git a/almanac-web/src/reducers/bookList.js b/almanac-web/src/reducers/bookList.js new file mode 100644 index 0000000..1c5e26d --- /dev/null +++ b/almanac-web/src/reducers/bookList.js @@ -0,0 +1,33 @@ +import { + SET_PAGE, + HOME_PAGE_LOADED, + HOME_PAGE_UNLOADED +} from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case SET_PAGE: + return { + ...state, + books: action.payload._embedded + ? action.payload._embedded.bookResponseList + : [], + booksCount: action.payload.page.totalElements, + currentPage: action.payload.page.number + }; + case HOME_PAGE_LOADED: + return { + ...state, + pager: action.payload[0].page, + books: action.payload[0]._embedded + ? action.payload[0]._embedded.bookResponseList + : [], + booksCount: action.payload[0].page.totalElements, + currentPage: 0 + }; + case HOME_PAGE_UNLOADED: + return {}; + default: + return state; + } +}; diff --git a/almanac-web/src/reducers/common.js b/almanac-web/src/reducers/common.js new file mode 100644 index 0000000..fb9b83d --- /dev/null +++ b/almanac-web/src/reducers/common.js @@ -0,0 +1,70 @@ +import { + APP_LOAD, + REDIRECT, + LOGOUT, + ARTICLE_SUBMITTED, + SETTINGS_SAVED, + LOGIN, + REGISTER, + DELETE_ARTICLE, + BOOK_PAGE_UNLOADED, + EDITOR_PAGE_UNLOADED, + HOME_PAGE_UNLOADED, + PROFILE_PAGE_UNLOADED, + PROFILE_FAVORITES_PAGE_UNLOADED, + SETTINGS_PAGE_UNLOADED, + LOGIN_PAGE_UNLOADED, + REGISTER_PAGE_UNLOADED +} from '../constants/actionTypes'; + +const defaultState = { + appName: 'Almanac', + token: null, + viewChangeCounter: 0 +}; + +export default (state = defaultState, action) => { + switch (action.type) { + case APP_LOAD: + return { + ...state, + token: action.token || null, + appLoaded: true, + currentUser: action.payload ? action.payload.user : null + }; + case REDIRECT: + return { ...state, redirectTo: null }; + case LOGOUT: + return { ...state, redirectTo: '/', token: null, currentUser: null }; + case ARTICLE_SUBMITTED: + const redirectUrl = `/article/${action.payload.article.slug}`; + return { ...state, redirectTo: redirectUrl }; + case SETTINGS_SAVED: + return { + ...state, + redirectTo: action.error ? null : '/', + currentUser: action.error ? null : action.payload.user + }; + case LOGIN: + case REGISTER: + return { + ...state, + redirectTo: action.error ? null : '/', + token: action.error ? null : action.payload.user.token, + currentUser: action.error ? null : action.payload.user + }; + case DELETE_ARTICLE: + return { ...state, redirectTo: '/' }; + case BOOK_PAGE_UNLOADED: + case EDITOR_PAGE_UNLOADED: + case HOME_PAGE_UNLOADED: + case PROFILE_PAGE_UNLOADED: + case PROFILE_FAVORITES_PAGE_UNLOADED: + case SETTINGS_PAGE_UNLOADED: + case LOGIN_PAGE_UNLOADED: + case REGISTER_PAGE_UNLOADED: + return { ...state, viewChangeCounter: state.viewChangeCounter + 1 }; + default: + return state; + } +}; diff --git a/almanac-web/src/reducers/home.js b/almanac-web/src/reducers/home.js new file mode 100644 index 0000000..05e900d --- /dev/null +++ b/almanac-web/src/reducers/home.js @@ -0,0 +1,14 @@ +import { HOME_PAGE_LOADED, HOME_PAGE_UNLOADED } from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case HOME_PAGE_LOADED: + return { + ...state + }; + case HOME_PAGE_UNLOADED: + return {}; + default: + return state; + } +}; diff --git a/almanac-web/src/reducers/profile.js b/almanac-web/src/reducers/profile.js new file mode 100644 index 0000000..64c23bd --- /dev/null +++ b/almanac-web/src/reducers/profile.js @@ -0,0 +1,19 @@ +import { + PROFILE_PAGE_LOADED, + PROFILE_PAGE_UNLOADED +} from '../constants/actionTypes'; + +export default (state = {}, action) => { + switch (action.type) { + case PROFILE_PAGE_LOADED: + return { + ...state, + profile: action.payload[0] + }; + case PROFILE_PAGE_UNLOADED: + return {}; + + default: + return state; + } +}; diff --git a/almanac-web/src/store.js b/almanac-web/src/store.js new file mode 100644 index 0000000..f9a087c --- /dev/null +++ b/almanac-web/src/store.js @@ -0,0 +1,29 @@ +import { applyMiddleware, createStore } from 'redux'; +import { createLogger } from 'redux-logger' +import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; +import { promiseMiddleware, localStorageMiddleware } from './middleware'; +import reducer from './reducer'; + +import { routerMiddleware } from 'react-router-redux' +import createHistory from 'history/createBrowserHistory'; + +export const history = createHistory(); + +const myRouterMiddleware = routerMiddleware(history); + +const initialState = {}; +const getMiddleware = () => { + if (process.env.NODE_ENV === 'production') { + return applyMiddleware(myRouterMiddleware, promiseMiddleware, localStorageMiddleware); + } else { + // Enable additional logging in non-production environments. + return applyMiddleware(myRouterMiddleware, promiseMiddleware, localStorageMiddleware, createLogger()) + } +}; +export const store = createStore( + reducer, + initialState, + composeWithDevTools(getMiddleware()) +); + + From fa10578ca2c855f385eb23bf2618ac6a6310fd37 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Fri, 12 Apr 2019 14:41:05 +0300 Subject: [PATCH 13/14] Add text editor in react client --- almanac-web/package.json | 1 + .../src/containers/Article/ArticleCreate.js | 18 +++++++++++++----- .../src/containers/Article/ArticleEdit.js | 10 +++++----- almanac-web/src/containers/Article/index.js | 5 +++-- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/almanac-web/package.json b/almanac-web/package.json index 42f6cbd..c6bb599 100644 --- a/almanac-web/package.json +++ b/almanac-web/package.json @@ -6,6 +6,7 @@ "@tinymce/tinymce-react": "^3.0.1", "axios": "^0.18.0", "history": "^4.6.3", + "marked": "^0.6.2", "moment": "^2.24.0", "moment-timezone": "^0.5.23", "prismic-reactjs": "^0.3.2", diff --git a/almanac-web/src/containers/Article/ArticleCreate.js b/almanac-web/src/containers/Article/ArticleCreate.js index 7040ed8..e606670 100644 --- a/almanac-web/src/containers/Article/ArticleCreate.js +++ b/almanac-web/src/containers/Article/ArticleCreate.js @@ -1,25 +1,26 @@ import React from 'react'; import agent from '../../agent'; -import { connect } from 'react-redux'; import { CREATE_ARTICLE } from '../../constants/actionTypes'; import { Editor } from '@tinymce/tinymce-react'; +import RichTextEditor from 'react-rte'; class ArticleCreate extends React.Component { constructor() { super(); this.state = { - article: {} + article: { content: RichTextEditor.createEmptyValue() } } this.updateField = this.updateField.bind(this); this.createArticle = this.createArticle.bind(this); }; createArticle() { var article = this.state.article; + article.content = this.state.article.content.toString('markdown'); const payload = agent.Articles.create(this.props.match.params.bookId, { ...article }); - this.setState({ article: {} }); + this.setState({ article: { content: RichTextEditor.createEmptyValue() } }); this.props.history.push(`/books/${this.props.match.params.bookId}/edit`) }; updateField(event) { @@ -27,7 +28,11 @@ class ArticleCreate extends React.Component { this.setState( { article: article } ); - } + } + onChangeRte = (value) => { + var article = { ...this.state.article,content: value }; + this.setState({ article: article }); + }; render() { var article = this.state.article; if (this.state.redirectTo) { @@ -51,7 +56,10 @@ class ArticleCreate extends React.Component {
- +
- edit + edit
@@ -65,7 +66,7 @@ class Book extends React.Component {
-

+

From 173325d00e2c4ffe4fd662b70610f6f463f5f299 Mon Sep 17 00:00:00 2001 From: "Uladzislau.Barkou" Date: Wed, 24 Apr 2019 12:15:29 +0300 Subject: [PATCH 14/14] Add public folder. Allow users to sign in. Allow users to download book. Allow users to create book from file. --- almanac-web/public/favicon.ico | Bin 0 -> 3870 bytes almanac-web/public/index.html | 18 +++ almanac-web/public/manifest.json | 15 ++ almanac-web/src/App.js | 33 ++-- almanac-web/src/Header.js | 3 +- almanac-web/src/agent.js | 26 ++-- almanac-web/src/components/HeaderList.js | 87 +++++------ almanac-web/src/components/ListPagination.js | 12 +- almanac-web/src/constants/actionTypes.js | 2 + almanac-web/src/constants/commonConstants.js | 2 +- almanac-web/src/constants/config.js | 9 ++ .../src/containers/Article/ArticleCreate.js | 99 +++++++----- .../src/containers/Article/ArticleEdit.js | 1 + .../src/containers/Article/ArticleList.js | 19 ++- .../src/containers/Article/ArticlePreview.js | 2 +- almanac-web/src/containers/Article/index.js | 21 +-- .../src/containers/Authentication/Login.js | 21 ++- .../src/containers/Authentication/OAuth2.js | 41 +++++ .../Authentication/OAuthProvider.js | 20 +++ .../src/containers/Authentication/Popup.js | 88 +++++++++++ almanac-web/src/containers/Book/BookCreate.js | 145 +++++++++++++----- almanac-web/src/containers/Book/BookList.js | 16 +- .../src/containers/Book/BookPreview.js | 22 ++- almanac-web/src/containers/Book/index.js | 19 ++- .../src/containers/Profile/ProfilePreview.js | 10 +- almanac-web/src/containers/Profile/index.js | 9 +- almanac-web/src/middleware.js | 10 +- almanac-web/src/reducers/auth.js | 3 +- almanac-web/src/reducers/book.js | 20 +-- almanac-web/src/reducers/bookList.js | 20 +-- almanac-web/src/reducers/common.js | 13 +- 31 files changed, 582 insertions(+), 224 deletions(-) create mode 100644 almanac-web/public/favicon.ico create mode 100644 almanac-web/public/index.html create mode 100644 almanac-web/public/manifest.json create mode 100644 almanac-web/src/constants/config.js create mode 100644 almanac-web/src/containers/Authentication/OAuth2.js create mode 100644 almanac-web/src/containers/Authentication/OAuthProvider.js create mode 100644 almanac-web/src/containers/Authentication/Popup.js diff --git a/almanac-web/public/favicon.ico b/almanac-web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a11777cc471a4344702741ab1c8a588998b1311a GIT binary patch literal 3870 zcma);c{J4h9>;%nil|2-o+rCuEF-(I%-F}ijC~o(k~HKAkr0)!FCj~d>`RtpD?8b; zXOC1OD!V*IsqUwzbMF1)-gEDD=A573Z-&G7^LoAC9|WO7Xc0Cx1g^Zu0u_SjAPB3vGa^W|sj)80f#V0@M_CAZTIO(t--xg= z!sii`1giyH7EKL_+Wi0ab<)&E_0KD!3Rp2^HNB*K2@PHCs4PWSA32*-^7d{9nH2_E zmC{C*N*)(vEF1_aMamw2A{ZH5aIDqiabnFdJ|y0%aS|64E$`s2ccV~3lR!u<){eS` z#^Mx6o(iP1Ix%4dv`t@!&Za-K@mTm#vadc{0aWDV*_%EiGK7qMC_(`exc>-$Gb9~W!w_^{*pYRm~G zBN{nA;cm^w$VWg1O^^<6vY`1XCD|s_zv*g*5&V#wv&s#h$xlUilPe4U@I&UXZbL z0)%9Uj&@yd03n;!7do+bfixH^FeZ-Ema}s;DQX2gY+7g0s(9;`8GyvPY1*vxiF&|w z>!vA~GA<~JUqH}d;DfBSi^IT*#lrzXl$fNpq0_T1tA+`A$1?(gLb?e#0>UELvljtQ zK+*74m0jn&)5yk8mLBv;=@}c{t0ztT<v;Avck$S6D`Z)^c0(jiwKhQsn|LDRY&w(Fmi91I7H6S;b0XM{e zXp0~(T@k_r-!jkLwd1_Vre^v$G4|kh4}=Gi?$AaJ)3I+^m|Zyj#*?Kp@w(lQdJZf4 z#|IJW5z+S^e9@(6hW6N~{pj8|NO*>1)E=%?nNUAkmv~OY&ZV;m-%?pQ_11)hAr0oAwILrlsGawpxx4D43J&K=n+p3WLnlDsQ$b(9+4 z?mO^hmV^F8MV{4Lx>(Q=aHhQ1){0d*(e&s%G=i5rq3;t{JC zmgbn5Nkl)t@fPH$v;af26lyhH!k+#}_&aBK4baYPbZy$5aFx4}ka&qxl z$=Rh$W;U)>-=S-0=?7FH9dUAd2(q#4TCAHky!$^~;Dz^j|8_wuKc*YzfdAht@Q&ror?91Dm!N03=4=O!a)I*0q~p0g$Fm$pmr$ zb;wD;STDIi$@M%y1>p&_>%?UP($15gou_ue1u0!4(%81;qcIW8NyxFEvXpiJ|H4wz z*mFT(qVx1FKufG11hByuX%lPk4t#WZ{>8ka2efjY`~;AL6vWyQKpJun2nRiZYDij$ zP>4jQXPaP$UC$yIVgGa)jDV;F0l^n(V=HMRB5)20V7&r$jmk{UUIe zVjKroK}JAbD>B`2cwNQ&GDLx8{pg`7hbA~grk|W6LgiZ`8y`{Iq0i>t!3p2}MS6S+ zO_ruKyAElt)rdS>CtF7j{&6rP-#c=7evGMt7B6`7HG|-(WL`bDUAjyn+k$mx$CH;q2Dz4x;cPP$hW=`pFfLO)!jaCL@V2+F)So3}vg|%O*^T1j>C2lx zsURO-zIJC$^$g2byVbRIo^w>UxK}74^TqUiRR#7s_X$e)$6iYG1(PcW7un-va-S&u zHk9-6Zn&>T==A)lM^D~bk{&rFzCi35>UR!ZjQkdSiNX*-;l4z9j*7|q`TBl~Au`5& z+c)*8?#-tgUR$Zd%Q3bs96w6k7q@#tUn`5rj+r@_sAVVLqco|6O{ILX&U-&-cbVa3 zY?ngHR@%l{;`ri%H*0EhBWrGjv!LE4db?HEWb5mu*t@{kv|XwK8?npOshmzf=vZA@ zVSN9sL~!sn?r(AK)Q7Jk2(|M67Uy3I{eRy z_l&Y@A>;vjkWN5I2xvFFTLX0i+`{qz7C_@bo`ZUzDugfq4+>a3?1v%)O+YTd6@Ul7 zAfLfm=nhZ`)P~&v90$&UcF+yXm9sq!qCx3^9gzIcO|Y(js^Fj)Rvq>nQAHI92ap=P z10A4@prk+AGWCb`2)dQYFuR$|H6iDE8p}9a?#nV2}LBCoCf(Xi2@szia7#gY>b|l!-U`c}@ zLdhvQjc!BdLJvYvzzzngnw51yRYCqh4}$oRCy-z|v3Hc*d|?^Wj=l~18*E~*cR_kU z{XsxM1i{V*4GujHQ3DBpl2w4FgFR48Nma@HPgnyKoIEY-MqmMeY=I<%oG~l!f<+FN z1ZY^;10j4M4#HYXP zw5eJpA_y(>uLQ~OucgxDLuf}fVs272FaMxhn4xnDGIyLXnw>Xsd^J8XhcWIwIoQ9} z%FoSJTAGW(SRGwJwb=@pY7r$uQRK3Zd~XbxU)ts!4XsJrCycrWSI?e!IqwqIR8+Jh zlRjZ`UO1I!BtJR_2~7AbkbSm%XQqxEPkz6BTGWx8e}nQ=w7bZ|eVP4?*Tb!$(R)iC z9)&%bS*u(lXqzitAN)Oo=&Ytn>%Hzjc<5liuPi>zC_nw;Z0AE3Y$Jao_Q90R-gl~5 z_xAb2J%eArrC1CN4G$}-zVvCqF1;H;abAu6G*+PDHSYFx@Tdbfox*uEd3}BUyYY-l zTfEsOqsi#f9^FoLO;ChK<554qkri&Av~SIM*{fEYRE?vH7pTAOmu2pz3X?Wn*!ROX ztd54huAk&mFBemMooL33RV-*1f0Q3_(7hl$<#*|WF9P!;r;4_+X~k~uKEqdzZ$5Al zV63XN@)j$FN#cCD;ek1R#l zv%pGrhB~KWgoCj%GT?%{@@o(AJGt*PG#l3i>lhmb_twKH^EYvacVY-6bsCl5*^~L0 zonm@lk2UvvTKr2RS%}T>^~EYqdL1q4nD%0n&Xqr^cK^`J5W;lRRB^R-O8b&HENO||mo0xaD+S=I8RTlIfVgqN@SXDr2&-)we--K7w= zJVU8?Z+7k9dy;s;^gDkQa`0nz6N{T?(A&Iz)2!DEecLyRa&FI!id#5Z7B*O2=PsR0 zEvc|8{NS^)!d)MDX(97Xw}m&kEO@5jqRaDZ!+%`wYOI<23q|&js`&o4xvjP7D_xv@ z5hEwpsp{HezI9!~6O{~)lLR@oF7?J7i>1|5a~UuoN=q&6N}EJPV_GD`&M*v8Y`^2j zKII*d_@Fi$+i*YEW+Hbzn{iQk~yP z>7N{S4)r*!NwQ`(qcN#8SRQsNK6>{)X12nbF`*7#ecO7I)Q$uZsV+xS4E7aUn+U(K baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ literal 0 HcmV?d00001 diff --git a/almanac-web/public/index.html b/almanac-web/public/index.html new file mode 100644 index 0000000..bd79eb9 --- /dev/null +++ b/almanac-web/public/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + + Almanac + + +
+ + + diff --git a/almanac-web/public/manifest.json b/almanac-web/public/manifest.json new file mode 100644 index 0000000..1f2f141 --- /dev/null +++ b/almanac-web/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/almanac-web/src/App.js b/almanac-web/src/App.js index fadf377..487d0b5 100644 --- a/almanac-web/src/App.js +++ b/almanac-web/src/App.js @@ -2,7 +2,7 @@ import agent from './agent'; import Header from './Header'; import React from 'react'; import { connect } from 'react-redux'; -import { APP_LOAD, REDIRECT } from './constants/actionTypes'; +import { APP_LOAD, REDIRECT, SIGN_IN, LOGOUT } from './constants/actionTypes'; import { Route, Switch } from 'react-router-dom'; import Home from './containers/Home'; import { store } from './store'; @@ -25,7 +25,7 @@ const mapStateToProps = state => { appLoaded: state.common.appLoaded, appName: state.common.appName, currentUser: state.common.currentUser, - redirectTo: state.common.redirectTo + redirectTo: state.common.redirectTo, } }; @@ -33,33 +33,47 @@ const mapDispatchToProps = dispatch => ({ onLoad: (payload, token) => dispatch({ type: APP_LOAD, payload, token, skipTracking: true }), onRedirect: () => - dispatch({ type: REDIRECT }) + dispatch({ type: REDIRECT }), + onLoadProfile: (payload) => + dispatch({ type: SIGN_IN, payload }), + onClickLogout: () => dispatch({ type: LOGOUT }) }); + class App extends React.Component { componentWillReceiveProps(nextProps) { if (nextProps.redirectTo) { - // this.context.router.replace(nextProps.redirectTo); store.dispatch(push(nextProps.redirectTo)); this.props.onRedirect(); } } componentWillMount() { - const token = window.localStorage.getItem('jwt'); + const token = window.localStorage.getItem('access_token'); if (token) { agent.setToken(token); + this.props.onLoadProfile(agent.Auth.current()) } this.props.onLoad(token ? agent.Auth.current() : null, token); } + logout = () => { + this.props.onClickLogout(); + } render() { + if (!(window.location.pathname === "/login" || + window.location.pathname === "/register") && + !window.localStorage.getItem('access_token')) { + store.dispatch(push("/login")); + } + if (this.props.appLoaded) { return (
+ currentUser={this.props.currentUser} + onClickLogout={this.logout} /> @@ -80,14 +94,11 @@ class App extends React.Component {
+ currentUser={this.props.currentUser} + onClickLogout={this.logout} />
); } } -// App.contextTypes = { -// router: PropTypes.object.isRequired -// }; - export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/almanac-web/src/Header.js b/almanac-web/src/Header.js index 68a6fba..702e7f8 100644 --- a/almanac-web/src/Header.js +++ b/almanac-web/src/Header.js @@ -11,8 +11,7 @@ class Header extends React.Component { {this.props.appName.toLowerCase()} - - +
); diff --git a/almanac-web/src/agent.js b/almanac-web/src/agent.js index d1ad137..bf81019 100644 --- a/almanac-web/src/agent.js +++ b/almanac-web/src/agent.js @@ -1,6 +1,6 @@ import superagentPromise from 'superagent-promise'; import _superagent from 'superagent'; -import { PAGE_SIZE } from './constants/commonConstants' +import { DEFAULT_PAGE_SIZE } from './constants/commonConstants' const superagent = superagentPromise(_superagent, global.Promise); @@ -12,9 +12,13 @@ const responseBody = res => res.body; let token = null; const tokenPlugin = req => { if (token) { - req.set('authorization', `Token ${token}`); + req.set('authorization', `Bearer ${token}`); } } +let basicAuth = "YWNtZTphY21lc2VjcmV0"; +const basicPlugin = req => { + req.set('authorization', `Basic ${basicAuth}`) +} const requests = { del: url => @@ -24,8 +28,11 @@ const requests = { put: (url, body) => superagent.put(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody), post: (url, body) => - superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody) + superagent.post(`${API_ROOT}${url}`, body).use(tokenPlugin).then(responseBody), + postWithBasic: (url, body) => + superagent.post(`${API_ROOT}${url}?grant_type=password&username=${body.username}&password=${body.password}`).use(basicPlugin).then(responseBody) }; + const directRequest = { del: url => superagent.del(`${url}`).use(tokenPlugin).then(responseBody), @@ -39,9 +46,9 @@ const directRequest = { const Auth = { current: () => - requests.get('/user'), + requests.get('/users/user'), login: (email, password) => - requests.post('/users/login', { user: { email, password } }), + requests.postWithBasic('/oauth/token', { username: email, password: password }), register: (username, email, password) => requests.post('/users', { username, email, password }), save: user => @@ -53,10 +60,9 @@ const Tags = { }; const limit = (size, page) => `size=${size}&page=${page ? page : 0}`; -const omitSlug = article => Object.assign({}, article, { slug: undefined }) const Books = { all: page => - requests.get(`/books?${limit(PAGE_SIZE, page)}`), + requests.get(`/books?${limit(localStorage.getItem("page_size") ? localStorage.getItem("page_size") : DEFAULT_PAGE_SIZE, page)}`), del: slug => requests.del(`/books/${slug}`), get: slug => @@ -78,12 +84,10 @@ const Articles = { }; const Profile = { - follow: username => - requests.post(`/profiles/${username}/follow`), get: id => requests.get(`/users/${id}`), - unfollow: username => - requests.del(`/profiles/${username}/follow`) + me: () => + requests.get(`/users/me`) }; export default { diff --git a/almanac-web/src/components/HeaderList.js b/almanac-web/src/components/HeaderList.js index f6d0023..9d6ed35 100644 --- a/almanac-web/src/components/HeaderList.js +++ b/almanac-web/src/components/HeaderList.js @@ -1,57 +1,54 @@ import React from 'react'; import { Link } from 'react-router-dom'; - const HeaderList = props => { - return ( -
    -
  • - - Home - -
  • +class HeaderList extends React.Component { + render() { + let props = this.props; + return ( +
      - {!props.currentUser &&
    • - - Sign in - -
    • } - - {!props.currentUser && -
    • - - Sign up - -
    • - } - - {props.currentUser && -
    • - -  New Post + + Home
    • - } - {props.currentUser && -
    • - -  Settings - -
    • - } - {props.currentUser && -
    • - - {props.currentUser.username} - {props.currentUser.username} + {!props.currentUser && +
    • + + Sign in -
    • - } -
    - ); + } + + {!props.currentUser && +
  • + + Sign up + +
  • + } + + {props.currentUser && +
  • + + {/* {props.currentUser.username} */} + {props.currentUser.username} + +
  • + } + {props.currentUser && + + } +
+ ); + } } + export default HeaderList; diff --git a/almanac-web/src/components/ListPagination.js b/almanac-web/src/components/ListPagination.js index 4296588..f0d56b7 100644 --- a/almanac-web/src/components/ListPagination.js +++ b/almanac-web/src/components/ListPagination.js @@ -2,7 +2,7 @@ import React from 'react'; import agent from '../agent'; import { connect } from 'react-redux'; import { SET_PAGE } from '../constants/actionTypes'; -import { PAGE_SIZE } from '../constants/commonConstants'; +import { DEFAULT_PAGE_SIZE } from '../constants/commonConstants'; const mapDispatchToProps = dispatch => ({ onSetPage: (page, payload) => @@ -10,7 +10,10 @@ const mapDispatchToProps = dispatch => ({ }); const ListPagination = props => { - if (props.booksCount <= PAGE_SIZE) { + let pageSize = localStorage.getItem("page_size") + ? localStorage.getItem("page_size") + : DEFAULT_PAGE_SIZE; + if (props.booksCount <= pageSize) { return null; } @@ -20,7 +23,7 @@ const ListPagination = props => { } const setPage = page => { - props.onSetPage(page, agent.Books.all(page)) + props.onSetPage(page, agent.Books.all(page)) }; return ( @@ -36,7 +39,7 @@ const ListPagination = props => { }; return (
  • @@ -46,7 +49,6 @@ const ListPagination = props => { ); }) } - ); diff --git a/almanac-web/src/constants/actionTypes.js b/almanac-web/src/constants/actionTypes.js index f8cbc0a..dd49735 100644 --- a/almanac-web/src/constants/actionTypes.js +++ b/almanac-web/src/constants/actionTypes.js @@ -42,4 +42,6 @@ export const CREATE_ARTICLE = 'CREATE_ARTICLE'; export const DELETE_ARTICLE = 'DELETE_ARTICLE'; export const UPDATE_ARTICLE = 'UPDATE_ARTICLE'; export const BOOK_UPDATED = 'BOOK_UPDATED'; +export const SIGN_IN = 'SIGN_IN'; + diff --git a/almanac-web/src/constants/commonConstants.js b/almanac-web/src/constants/commonConstants.js index 2f38546..54c4a4a 100644 --- a/almanac-web/src/constants/commonConstants.js +++ b/almanac-web/src/constants/commonConstants.js @@ -1 +1 @@ -export const PAGE_SIZE = 2; \ No newline at end of file +export const DEFAULT_PAGE_SIZE = 2; \ No newline at end of file diff --git a/almanac-web/src/constants/config.js b/almanac-web/src/constants/config.js new file mode 100644 index 0000000..a9649da --- /dev/null +++ b/almanac-web/src/constants/config.js @@ -0,0 +1,9 @@ + +export const providerConfig = { + clientId : 'react-client', + redirectUri : window.location.origin + '/login', + authorizationUrl: 'http://localhost:8080/login/github', + scope :'', + width : 1080, + height : 640 + }; \ No newline at end of file diff --git a/almanac-web/src/containers/Article/ArticleCreate.js b/almanac-web/src/containers/Article/ArticleCreate.js index e606670..e8ebe6f 100644 --- a/almanac-web/src/containers/Article/ArticleCreate.js +++ b/almanac-web/src/containers/Article/ArticleCreate.js @@ -1,10 +1,7 @@ import React from 'react'; import agent from '../../agent'; -import { - CREATE_ARTICLE -} from '../../constants/actionTypes'; -import { Editor } from '@tinymce/tinymce-react'; import RichTextEditor from 'react-rte'; +import axios from 'axios'; class ArticleCreate extends React.Component { constructor() { @@ -14,6 +11,8 @@ class ArticleCreate extends React.Component { } this.updateField = this.updateField.bind(this); this.createArticle = this.createArticle.bind(this); + this.parseFile = this.parseFile.bind(this); + this.updateFile = this.updateFile.bind(this); }; createArticle() { var article = this.state.article; @@ -23,14 +22,37 @@ class ArticleCreate extends React.Component { this.setState({ article: { content: RichTextEditor.createEmptyValue() } }); this.props.history.push(`/books/${this.props.match.params.bookId}/edit`) }; + parseFile() { + if (this.state.file) { + const url = `http://localhost:8080/files`; + const formData = new FormData(); + formData.append('file', this.state.file) + const config = { + headers: { + 'content-type': 'multipart/form-data' + } + } + axios.post(url, formData, config) + .then(response => { + let newArticle = this.state.article; + newArticle.content =response.data.content? RichTextEditor.createValueFromString(response.data.content, "markdown") : RichTextEditor.createEmptyValue(); + this.setState({ + ...this.state, article: newArticle + }) + }); + } + }; + updateFile(event) { + this.setState({ file: event.target.files[0] }) + } updateField(event) { var article = { ...this.state.article, [event.target.name]: event.target.value }; this.setState( { article: article } ); - } + } onChangeRte = (value) => { - var article = { ...this.state.article,content: value }; + var article = { ...this.state.article, content: value }; this.setState({ article: article }); }; render() { @@ -38,36 +60,43 @@ class ArticleCreate extends React.Component { if (this.state.redirectTo) { } return ( -
    -
    - - -
    -
    - - -
    -
    - -
    -
    - +
    +
    + + - - + ); } } diff --git a/almanac-web/src/containers/Article/ArticleEdit.js b/almanac-web/src/containers/Article/ArticleEdit.js index ccd1ca0..85b1dc6 100644 --- a/almanac-web/src/containers/Article/ArticleEdit.js +++ b/almanac-web/src/containers/Article/ArticleEdit.js @@ -33,6 +33,7 @@ class ArticleCreate extends React.Component { } componentWillReceiveProps(newProps) { var article = { ...newProps.article }; + article.content = article.content ? RichTextEditor.createValueFromString(article.content, "markdown") : RichTextEditor.createEmptyValue(); this.setState({ article: article }) } diff --git a/almanac-web/src/containers/Article/ArticleList.js b/almanac-web/src/containers/Article/ArticleList.js index a2634e1..ceca430 100644 --- a/almanac-web/src/containers/Article/ArticleList.js +++ b/almanac-web/src/containers/Article/ArticleList.js @@ -7,7 +7,8 @@ import axios from 'axios'; const mapStateToProps = state => ({ ...state, link: state.link, - currentUser: state.common.currentUser + currentUser: state.common.currentUser, + token:state.common.token }); class ArticleList extends React.Component { constructor() { @@ -18,9 +19,17 @@ class ArticleList extends React.Component { } } componentWillMount() { - if (this.props.book) { - axios.get(this.props.book.book.articles.href) - .then(response => this.setState({ ...this.state, articles: response.data })) + if (this.props.book + && this.props.book.book.articles + && this.props.book.book.articles.href) { + const req = { + url: this.props.book.book.articles.href, + headers: { Authorization: "bearer " + this.props.token }, + method: 'GET' + }; + axios.get(req.url,req) + .then(response => { + this.setState({ ...this.state, articles: response.data })}) } } @@ -45,7 +54,7 @@ class ArticleList extends React.Component { { articles.map(article => { return ( -
  • +
  • ); }) } diff --git a/almanac-web/src/containers/Article/ArticlePreview.js b/almanac-web/src/containers/Article/ArticlePreview.js index 3294f3a..78a94ab 100644 --- a/almanac-web/src/containers/Article/ArticlePreview.js +++ b/almanac-web/src/containers/Article/ArticlePreview.js @@ -13,7 +13,7 @@ const ArticlePreview = props => {
    {article.title}
    {article.created && -
    } +
    } diff --git a/almanac-web/src/containers/Article/index.js b/almanac-web/src/containers/Article/index.js index bd53cf9..a1cd720 100644 --- a/almanac-web/src/containers/Article/index.js +++ b/almanac-web/src/containers/Article/index.js @@ -50,11 +50,9 @@ class Book extends React.Component {
    edit -
    - -
    + + To book
    - to book
    @@ -63,11 +61,16 @@ class Book extends React.Component { {article.annotation}

    -
    -
    -
    -

    -
    + {article.pageCount && +
    +

    Page count: {article.pageCount}

    +
    } +
    + +
    +
    +
    +

    diff --git a/almanac-web/src/containers/Authentication/Login.js b/almanac-web/src/containers/Authentication/Login.js index e0fa092..147b1f2 100644 --- a/almanac-web/src/containers/Authentication/Login.js +++ b/almanac-web/src/containers/Authentication/Login.js @@ -6,8 +6,11 @@ import { connect } from 'react-redux'; import { UPDATE_FIELD_AUTH, LOGIN, - LOGIN_PAGE_UNLOADED + LOGIN_PAGE_UNLOADED, + SIGN_IN } from '../../constants/actionTypes'; +import OAuthProvider from './OAuthProvider'; +import { providerConfig } from '../../constants/config'; const mapStateToProps = state => ({ ...state.auth }); @@ -33,6 +36,17 @@ class Login extends React.Component { }; } + onOAuthProviderLogin = (data) => { + let token = JSON.stringify(data.code) || JSON.stringify(data); + window.localStorage.setItem('OAuthProvider_token', token); + this.setState({ token: token }); + window.location = "/home" + } + + onOAuthProviderLoginFailure = (err) => { + console.log("something wrong") + console.error(err); + } componentWillUnmount() { this.props.onUnload(); } @@ -82,9 +96,12 @@ class Login extends React.Component { disabled={this.props.inProgress}> Sign in - + diff --git a/almanac-web/src/containers/Authentication/OAuth2.js b/almanac-web/src/containers/Authentication/OAuth2.js new file mode 100644 index 0000000..f26bf12 --- /dev/null +++ b/almanac-web/src/containers/Authentication/OAuth2.js @@ -0,0 +1,41 @@ +import React, { Component } from 'react'; +import Popup from './Popup'; +import qs from 'querystring'; + +class OAuth2 extends Component { + constructor (props) { + super(props); + this.state = { popupOpen: false }; + this.handleClick = this.handleClick.bind(this); + } + + handleClick () { + this.setState({ popupOpen: true }); + console.log('clicked on button'); + } + + render () { + const props = this.props; + + const childrenWithProps = React.Children.map(props.children, (child) => { + return React.cloneElement(child, { onClick: this.handleClick }); + }); + + const params = { + client_id: props.clientId, + redirect_uri: props.redirectUri, + scope: props.scope, + display: 'popup', + response_type: 'token' + }; + + const url = props.authorizationUrl + + return
    + + {childrenWithProps} +
    ; + } +} + +export default OAuth2; diff --git a/almanac-web/src/containers/Authentication/OAuthProvider.js b/almanac-web/src/containers/Authentication/OAuthProvider.js new file mode 100644 index 0000000..09e55ee --- /dev/null +++ b/almanac-web/src/containers/Authentication/OAuthProvider.js @@ -0,0 +1,20 @@ +import React from 'react'; +import OAuth2 from './OAuth2'; + +export const OAuthProvider = props => { + const { config, textDisplay, className, successCallback, errorCallback } = props; + config.successCallback = successCallback; + config.errorCallback = errorCallback; + return ( + + + + ); +}; + +OAuthProvider.defaultProps = { + textDisplay: 'Sign in with OAuthProvider' +}; + + +export default OAuthProvider; diff --git a/almanac-web/src/containers/Authentication/Popup.js b/almanac-web/src/containers/Authentication/Popup.js new file mode 100644 index 0000000..e06e259 --- /dev/null +++ b/almanac-web/src/containers/Authentication/Popup.js @@ -0,0 +1,88 @@ +import React from 'react'; +import qs from 'querystring'; +import url from 'url'; +import Promise from 'bluebird'; + +class Popup extends React.Component { + constructor(props) { + super(props); + } + + componentDidUpdate() { + if (this.props.open) { + this.openPopup(); + } + } + + openPopup() { + const props = this.props; + const width = props.width || 500; + const height = props.height || 500; + + const options = { + width: width, + height: height, + top: window.screenY + ((window.outerHeight - height) / 2.5), + left: window.screenX + ((window.outerWidth - width) / 2) + }; + + const popup = window.open(props.popupUrl, '_blank', qs.stringify(options, ',')); + + if (props.popupUrl === 'about:blank') { + popup.document.body.innerHTML = 'Loading...'; + } + + this.pollPopup(popup).then(props.successCallback).catch(props.errorCallback); + } + + pollPopup(window) { + const props = this.props; + + return new Promise((resolve, reject) => { + const redirectUri = url.parse(props.redirectUri); + const redirectUriPath = redirectUri.host + redirectUri.pathname; + + const polling = setInterval(() => { + if (!window || window.closed || window.closed === undefined) { + clearInterval(polling); + reject(new Error('The popup window was closed')); + } + try { + const popupUrlPath = window.location.host + window.location.pathname; + + if (popupUrlPath === redirectUriPath) { + if (window.location.search || window.location.hash) { + const query = qs.parse(window.location.search.substring(1).replace(/\/$/, '')); + const hash = qs.parse(window.location.hash.substring(1).replace(/[\/$]/, '')); + const params = Object.assign({}, query, hash); + if (params.error) { + reject(new Error(params.error)); + } else { + resolve(params); + } + } else { + reject(new Error('OAuth redirect has occurred but no query or hash parameters were found.')); + } + // cleanup + clearInterval(polling); + window.close(); + } + } catch (error) { + // Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame. + // A hack to get around same-origin security policy errors in Internet Explorer. + } + }, 500); + }); + } + + handleClick() { + console.log('clicked on button'); + } + + render() { + return null; + } +} + + +export default Popup; diff --git a/almanac-web/src/containers/Book/BookCreate.js b/almanac-web/src/containers/Book/BookCreate.js index 47f2039..eeee641 100644 --- a/almanac-web/src/containers/Book/BookCreate.js +++ b/almanac-web/src/containers/Book/BookCreate.js @@ -6,10 +6,12 @@ import { connect } from 'react-redux'; import { CREATE_BOOK } from '../../constants/actionTypes'; +import ListErrors from '../Authentication/ListErrors'; const mapStateToProps = state => ({ ...state.book, - currentUser: state.common.currentUser + currentUser: state.common.currentUser, + token: state.common.token }); const mapDispatchToProps = dispatch => ({ onSubmit: payload => @@ -23,16 +25,63 @@ class BookCreate extends React.Component { } this.updateField = this.updateField.bind(this); this.createBook = this.createBook.bind(this); + this.parseFile = this.parseFile.bind(this); + this.updateFile = this.updateFile.bind(this); }; createBook() { var book = this.state.book; book.author = this.props.currentUser; const payload = agent.Books.create( { ...book }); - this.setState({ book: {}}); - + this.setState({ book: {} }); + this.props.history.push(`/`) }; + parseFile() { + if (this.state.file) { + const url = `http://localhost:8080/files/books`; + const formData = new FormData(); + formData.append('file', this.state.file) + const config = { + headers: { + 'content-type': 'multipart/form-data', + Authorization: "bearer " + this.props.token + } + } + axios.post(url, formData, config) + .then(response => { + let book = response.data; + this.setState({ ...this.state, book: book }); + + this.props.history.push(`/books/${book.bookId}`) + }) + .catch( + error => { + if (error.response) { + switch (error.response.status) { + case 400: + this.setState({ error: error.response.data.content }); + break; + case 500: + alert("Ka-boom") + break; + } + + } else if (error.request) { + alert("Server doesn't send response") + console.log(error.request); + } else { + // Something happened in setting up the request that triggered an Error + console.log('Error', error.message); + } + + } + ); + } + }; + updateFile(event) { + this.setState({ file: event.target.files[0] }) + } updateField(event) { var book = { ...this.state.book, [event.target.name]: event.target.value }; this.setState( @@ -41,49 +90,61 @@ class BookCreate extends React.Component { } render() { var book = this.props.book; - + return ( -
    -
    - - -
    -
    - -
    -
    - -
    +
    + +
    + + +
    +
    + +
    +
    + +
    -
    - +
    + +
    + {this.state.error && +
    + {this.state.error} +
    } + +
    - - +
    ); } } diff --git a/almanac-web/src/containers/Book/BookList.js b/almanac-web/src/containers/Book/BookList.js index 75dbb1e..13a02aa 100644 --- a/almanac-web/src/containers/Book/BookList.js +++ b/almanac-web/src/containers/Book/BookList.js @@ -8,6 +8,12 @@ const BookList = props => {
    Loading...
    ); } + const changeSize = e => { + console.dir(e.target.value) + localStorage.setItem("page_size", e.target.value) + this.state = { size: e.target.value } + + } if (props.books.length === 0) { return ( @@ -18,11 +24,19 @@ const BookList = props => { } return ( +
    + + { props.books.map(book => { return ( - + ); }) } diff --git a/almanac-web/src/containers/Book/BookPreview.js b/almanac-web/src/containers/Book/BookPreview.js index 785c4ae..d75920a 100644 --- a/almanac-web/src/containers/Book/BookPreview.js +++ b/almanac-web/src/containers/Book/BookPreview.js @@ -2,11 +2,11 @@ import React from 'react'; import { Link } from 'react-router-dom'; import axios from 'axios'; import { connect } from 'react-redux'; -import Profilepreview from '../Profile/ProfilePreview'; const mapStateToProps = state => ({ ...state.book, - currentUser: state.common.currentUser + currentUser: state.auth.currentUser, + token: state.common.token }); class BookPreview extends React.Component { @@ -17,8 +17,15 @@ class BookPreview extends React.Component { } } componentWillMount() { - if (this.props.book) { - axios.get(this.props.book.author && this.props.book.author.href) + if (this.props.book + && this.props.book.author + && this.props.book.author.href) { + let req = { + url: this.props.book.author.href, + headers: { Authorization: "bearer " + this.props.token }, + method: 'GET' + }; + axios.get(req.url, req) .then(response => { this.setState({ ...this.state, author: response.data }) }) @@ -33,10 +40,13 @@ class BookPreview extends React.Component {
    -

    {book.title}

    +
    {book.title}

    -

    {book.description}

    +

    {book.description + && (book.description.length < 120 + ? book.description + : book.description.substring(0, 120) + "...")}

    diff --git a/almanac-web/src/containers/Book/index.js b/almanac-web/src/containers/Book/index.js index d8f3dd5..c0b9a52 100644 --- a/almanac-web/src/containers/Book/index.js +++ b/almanac-web/src/containers/Book/index.js @@ -12,7 +12,8 @@ import axios from 'axios'; const mapStateToProps = state => ({ ...state.book, - currentUser: state.common.currentUser + currentUser: state.common.currentUser, + token: state.common.token }); const mapDispatchToProps = dispatch => ({ @@ -30,7 +31,17 @@ class Book extends React.Component { } downloadBook() { var links = this.props.book._links; - axios.get(links && links.download.href) + + if (links + && links.download + && links.download.href) { + let req = { + url: links.download.href, + headers: { Authorization: "bearer " + this.props.token }, + method: 'GET' + } + axios.get(req) + } } componentWillMount() { var getBook = agent.Books.get(this.props.match.params.id); @@ -57,7 +68,7 @@ class Book extends React.Component { {this.props.book._links && this.props.book._links.download &&
    download} @@ -85,7 +96,7 @@ class Book extends React.Component {
    -

    Size: {book.size}

    +

    Size: {book.pageCount}

    State: {book.state}

    diff --git a/almanac-web/src/containers/Profile/ProfilePreview.js b/almanac-web/src/containers/Profile/ProfilePreview.js index 2dc0ff9..0bce4db 100644 --- a/almanac-web/src/containers/Profile/ProfilePreview.js +++ b/almanac-web/src/containers/Profile/ProfilePreview.js @@ -6,7 +6,8 @@ import auth from '../../reducers/auth'; const mapStateToProps = state => ({ ...state.book, - currentUser: state.common.currentUser + currentUser: state.common.currentUser, + token: state.common.token }); class ProfilePreview extends React.Component { @@ -19,7 +20,12 @@ class ProfilePreview extends React.Component { componentWillMount() { console.dir(this.props.link) if (this.props.link) { - axios.get(this.props.link) + const req = { + url: this.props.link, + headers: { Authorization: "bearer " + this.props.token }, + method: 'GET' + }; + axios.get(req.url,req) .then(response => this.setState({ ...this.state, author: response.data })) } } diff --git a/almanac-web/src/containers/Profile/index.js b/almanac-web/src/containers/Profile/index.js index a6a50e7..edf56f5 100644 --- a/almanac-web/src/containers/Profile/index.js +++ b/almanac-web/src/containers/Profile/index.js @@ -6,7 +6,6 @@ import { PROFILE_PAGE_LOADED, PROFILE_PAGE_UNLOADED } from '../../constants/actionTypes'; -import axios from 'axios'; import { Link } from 'react-router-dom'; const mapStateToProps = state => ({ @@ -33,8 +32,6 @@ class Profile extends React.Component { componentWillMount() { var getProfile = agent.Profile.get(this.props.match.params.id); this.props.onLoad(Promise.all([getProfile])); - console.dir(this.props) - } componentWillUnmount() { this.props.onUnload(); @@ -80,8 +77,8 @@ class Profile extends React.Component {
    Books as coauthor:
    {booksAsSubAuthor.length === 0 &&
    There is no any book as coauthor
    - } - + } +
      { booksAsSubAuthor && booksAsSubAuthor.map(book =>
    1. @@ -91,6 +88,8 @@ class Profile extends React.Component {
    2. )}
    + + Create book
    diff --git a/almanac-web/src/middleware.js b/almanac-web/src/middleware.js index 070ad03..7497d91 100644 --- a/almanac-web/src/middleware.js +++ b/almanac-web/src/middleware.js @@ -47,13 +47,15 @@ const promiseMiddleware = store => next => action => { }; const localStorageMiddleware = store => next => action => { - if (action.type === REGISTER || action.type === LOGIN) { + if (action.type === LOGIN) { if (!action.error) { - window.localStorage.setItem('jwt', action.payload.user.token); - agent.setToken(action.payload.user.token); + window.localStorage.setItem('access_token', action.payload.access_token); + window.localStorage.setItem('refresh_token', action.payload.refresh_token); + agent.setToken(action.payload.access_token); } } else if (action.type === LOGOUT) { - window.localStorage.setItem('jwt', ''); + window.localStorage.setItem('access_token',''); + window.localStorage.setItem('refresh_token',''); agent.setToken(null); } diff --git a/almanac-web/src/reducers/auth.js b/almanac-web/src/reducers/auth.js index 6e83838..d3b63ea 100644 --- a/almanac-web/src/reducers/auth.js +++ b/almanac-web/src/reducers/auth.js @@ -4,7 +4,8 @@ import { LOGIN_PAGE_UNLOADED, REGISTER_PAGE_UNLOADED, ASYNC_START, - UPDATE_FIELD_AUTH + UPDATE_FIELD_AUTH, + SIGN_IN } from '../constants/actionTypes'; export default (state = {}, action) => { diff --git a/almanac-web/src/reducers/book.js b/almanac-web/src/reducers/book.js index a57b8e1..958c04e 100644 --- a/almanac-web/src/reducers/book.js +++ b/almanac-web/src/reducers/book.js @@ -11,29 +11,11 @@ export default (state = {}, action) => { var book = action.payload[0]; return { ...state, - book: book + book }; } - case CREATE_ARTICLE: - return { - ...state, redirectTo: '/' - }; case BOOK_PAGE_UNLOADED: return {}; - // case ADD_COMMENT: - // return { - // ...state, - // commentErrors: action.error ? action.payload.errors : null, - // comments: action.error ? - // null : - // (state.comments || []).concat([action.payload.comment]) - // }; - // case DELETE_COMMENT: - // const commentId = action.commentId - // return { - // ...state, - // comments: state.comments.filter(comment => comment.id !== commentId) - // }; default: return state; } diff --git a/almanac-web/src/reducers/bookList.js b/almanac-web/src/reducers/bookList.js index 1c5e26d..2a30331 100644 --- a/almanac-web/src/reducers/bookList.js +++ b/almanac-web/src/reducers/bookList.js @@ -16,15 +16,17 @@ export default (state = {}, action) => { currentPage: action.payload.page.number }; case HOME_PAGE_LOADED: - return { - ...state, - pager: action.payload[0].page, - books: action.payload[0]._embedded - ? action.payload[0]._embedded.bookResponseList - : [], - booksCount: action.payload[0].page.totalElements, - currentPage: 0 - }; + return !action.error ? + { + ...state, + pager: action.payload[0].page, + books: action.payload[0]._embedded + ? action.payload[0]._embedded.bookResponseList + : [], + booksCount: action.payload[0].page.totalElements, + currentPage: 0 + } + : { ...state, error: action.payload }; case HOME_PAGE_UNLOADED: return {}; default: diff --git a/almanac-web/src/reducers/common.js b/almanac-web/src/reducers/common.js index fb9b83d..2ee4e07 100644 --- a/almanac-web/src/reducers/common.js +++ b/almanac-web/src/reducers/common.js @@ -14,7 +14,8 @@ import { PROFILE_FAVORITES_PAGE_UNLOADED, SETTINGS_PAGE_UNLOADED, LOGIN_PAGE_UNLOADED, - REGISTER_PAGE_UNLOADED + REGISTER_PAGE_UNLOADED, + SIGN_IN } from '../constants/actionTypes'; const defaultState = { @@ -29,13 +30,17 @@ export default (state = defaultState, action) => { return { ...state, token: action.token || null, - appLoaded: true, - currentUser: action.payload ? action.payload.user : null + appLoaded: true }; case REDIRECT: return { ...state, redirectTo: null }; case LOGOUT: return { ...state, redirectTo: '/', token: null, currentUser: null }; + case SIGN_IN: + return { + ...state, + currentUser: action.payload + }; case ARTICLE_SUBMITTED: const redirectUrl = `/article/${action.payload.article.slug}`; return { ...state, redirectTo: redirectUrl }; @@ -50,7 +55,7 @@ export default (state = defaultState, action) => { return { ...state, redirectTo: action.error ? null : '/', - token: action.error ? null : action.payload.user.token, + token: action.error ? null : action.payload.access_token, currentUser: action.error ? null : action.payload.user }; case DELETE_ARTICLE: