Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab929504e1 | ||
|
|
8ccd325ba9 |
BIN
LabWork1Report.docx
Normal file
BIN
LabWork1Report.docx
Normal file
Binary file not shown.
BIN
LabWork2Report.docx
Normal file
BIN
LabWork2Report.docx
Normal file
Binary file not shown.
Binary file not shown.
564
demo/ANSWERS.md
564
demo/ANSWERS.md
@@ -1,564 +0,0 @@
|
||||
# Ответы на вопросы по персистентности, ORM, JPA и Hibernate
|
||||
|
||||
## 1. Что такое персистентность?
|
||||
|
||||
**Персистентность** — это способность данных сохраняться после завершения работы приложения. В нашем проекте данные сохраняются в файле базы данных H2.
|
||||
|
||||
**Пример из проекта:**
|
||||
- Файл `demo/src/main/resources/application.properties`, строки 6-11:
|
||||
```properties
|
||||
spring.datasource.url=jdbc:h2:file:./data
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
```
|
||||
Настройка `ddl-auto=update` позволяет сохранять данные между перезапусками приложения в файл `./data.mv.db`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Каким образом можно добавить поддержку персистентности в приложение?
|
||||
|
||||
В нашем проекте персистентность добавлена через:
|
||||
1. **Spring Data JPA** — зависимость в `build.gradle`
|
||||
2. **H2 Database** — встроенная БД
|
||||
3. **JPA аннотации** на сущностях
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/build.gradle`, строки 30-31:
|
||||
```gradle
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation "com.h2database:h2:${h2Version}"
|
||||
```
|
||||
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 9-10:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "artists")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Что такое сущность и атрибут сущности?
|
||||
|
||||
**Сущность** — это объект предметной области, который имеет идентичность и может быть сохранен в БД. **Атрибут** — это свойство сущности.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`:
|
||||
- **Сущность**: `ArtistEntity` (строка 11)
|
||||
- **Атрибуты**: `name` (строка 13), `description` (строка 15), `epoch` (строка 20), `country` (строка 24)
|
||||
|
||||
---
|
||||
|
||||
## 4. Как сущности и их атрибуты представлены в реляционной модели данных?
|
||||
|
||||
В реляционной модели:
|
||||
- **Сущность** → **Таблица**
|
||||
- **Атрибут** → **Колонка**
|
||||
- **Связь** → **Внешний ключ (Foreign Key)**
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 9-24:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "artists") // Таблица "artists"
|
||||
public class ArtistEntity extends BaseEntity {
|
||||
@Column(nullable = false) // Колонка "name"
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "text") // Колонка "description"
|
||||
private String description;
|
||||
|
||||
@JoinColumn(name = "epoch_id", nullable = false) // Внешний ключ "epoch_id"
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Как сущности и их атрибуты представлены в ООП?
|
||||
|
||||
В ООП:
|
||||
- **Сущность** → **Класс**
|
||||
- **Атрибут** → **Поле класса**
|
||||
- **Связь** → **Ссылка на другой объект**
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 11-24:
|
||||
```java
|
||||
public class ArtistEntity extends BaseEntity { // Класс (сущность)
|
||||
private String name; // Поле (атрибут)
|
||||
private String description; // Поле (атрибут)
|
||||
private EpochEntity epoch; // Ссылка на другой объект (связь)
|
||||
private CountryEntity country; // Ссылка на другой объект (связь)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Как соотносятся понятия ООП и реляционной модели данных?
|
||||
|
||||
**ORM (Object-Relational Mapping)** преобразует:
|
||||
- Класс ↔ Таблица
|
||||
- Поле ↔ Колонка
|
||||
- Объект ↔ Строка таблицы
|
||||
- Ссылка ↔ Внешний ключ
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`:
|
||||
- Класс `ArtistEntity` → таблица `artists` (строка 10)
|
||||
- Поле `name` → колонка `name` (строка 12-13)
|
||||
- Поле `epoch` → внешний ключ `epoch_id` (строка 18-20)
|
||||
|
||||
---
|
||||
|
||||
## 7. Что такое ORM?
|
||||
|
||||
**ORM (Object-Relational Mapping)** — технология, которая автоматически преобразует объекты в записи БД и обратно.
|
||||
|
||||
**Пример из проекта:**
|
||||
- Используется через Spring Data JPA и Hibernate
|
||||
- `demo/src/main/java/com/example/demo/repository/ArtistRepository.java`, строка 12:
|
||||
```java
|
||||
public interface ArtistRepository extends JpaRepository<ArtistEntity, Long>
|
||||
```
|
||||
JpaRepository автоматически реализует методы для работы с БД через ORM.
|
||||
|
||||
---
|
||||
|
||||
## 8. Когда следует использовать ORM?
|
||||
|
||||
ORM следует использовать когда:
|
||||
- Приложение использует ООП
|
||||
- Нужна быстрая разработка
|
||||
- Бизнес-логика сложная
|
||||
- Нужна переносимость между БД
|
||||
|
||||
**Пример из проекта:**
|
||||
- Проект использует Spring Boot с JPA для быстрой разработки
|
||||
- `demo/src/main/resources/application.properties`, строка 10:
|
||||
```properties
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||
```
|
||||
Можно легко сменить БД, изменив только настройки.
|
||||
|
||||
---
|
||||
|
||||
## 9. Когда НЕ следует использовать ORM?
|
||||
|
||||
ORM НЕ следует использовать когда:
|
||||
- Критична производительность (высоконагруженные системы)
|
||||
- Нужны сложные SQL-запросы
|
||||
- Работа с большими объемами данных (batch-обработка)
|
||||
- Уже есть оптимизированные SQL-запросы
|
||||
|
||||
**Пример из проекта:**
|
||||
- Для статистики используются JPQL запросы, но для очень сложных запросов может понадобиться нативный SQL
|
||||
- `demo/src/main/java/com/example/demo/repository/ArtistRepository.java`, строки 19-23:
|
||||
```java
|
||||
@Query("select count(a) as totalArtists, ...")
|
||||
ArtistStatsProjection getArtistsStatistics();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Что такое ассоциации в ООП?
|
||||
|
||||
**Ассоциация** — это связь между классами, когда один объект ссылается на другой.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 18-24:
|
||||
```java
|
||||
@ManyToOne
|
||||
private EpochEntity epoch; // Ассоциация с EpochEntity
|
||||
|
||||
@ManyToOne
|
||||
private CountryEntity country; // Ассоциация с CountryEntity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Перечислите свойства ассоциаций в ООП.
|
||||
|
||||
Свойства ассоциаций:
|
||||
- **Кратность** (1:1, 1:N, N:M)
|
||||
- **Направленность** (однонаправленная/двунаправленная)
|
||||
- **Обязательность** (обязательная/опциональная)
|
||||
- **Каскадность** (cascade)
|
||||
|
||||
**Пример из проекта:**
|
||||
- **Кратность**: `@ManyToOne` (строка 19) — многие артисты к одной эпохе
|
||||
- **Направленность**: Двунаправленная связь (ArtistEntity ↔ EpochEntity)
|
||||
- **Обязательность**: `nullable = false` (строка 18) — обязательная связь
|
||||
- **Каскадность**: Не используется (по умолчанию нет каскада)
|
||||
|
||||
---
|
||||
|
||||
## 12. Какими двумя способами можно представить связи между сущностями в реляционной БД?
|
||||
|
||||
1. **Внешний ключ (Foreign Key)** — в таблице "многих"
|
||||
2. **Связующая таблица (Join Table)** — для связи многие-ко-многим
|
||||
|
||||
**Пример из проекта:**
|
||||
- Используется внешний ключ:
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 18-20:
|
||||
```java
|
||||
@JoinColumn(name = "epoch_id", nullable = false) // Внешний ключ в таблице artists
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Что такое JPA?
|
||||
|
||||
**JPA (Java Persistence API)** — стандарт Java для работы с персистентностью, часть спецификации Jakarta EE.
|
||||
|
||||
**Пример из проекта:**
|
||||
- Используются JPA аннотации из пакета `jakarta.persistence`:
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 3-6:
|
||||
```java
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Что такое Hibernate?
|
||||
|
||||
**Hibernate** — реализация JPA, ORM-фреймворк для Java.
|
||||
|
||||
**Пример из проекта:**
|
||||
- Hibernate используется через Spring Boot:
|
||||
- `demo/src/main/resources/application.properties`, строка 10:
|
||||
```properties
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. Чем JPA отличается от Hibernate?
|
||||
|
||||
- **JPA** — это спецификация (стандарт)
|
||||
- **Hibernate** — это реализация JPA
|
||||
|
||||
**Пример из проекта:**
|
||||
- Используются JPA аннотации (`@Entity`, `@Column`), но работает через Hibernate
|
||||
- `demo/src/main/java/com/example/demo/entity/BaseEntity.java`, строки 11-13:
|
||||
```java
|
||||
@Id // JPA аннотация
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE) // JPA
|
||||
@SequenceGenerator(name = "hibernate_sequence") // Hibernate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Что такое Spring Data?
|
||||
|
||||
**Spring Data** — проект Spring для упрощения работы с данными, предоставляет репозитории.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/repository/ArtistRepository.java`, строка 12:
|
||||
```java
|
||||
public interface ArtistRepository extends JpaRepository<ArtistEntity, Long>
|
||||
```
|
||||
`JpaRepository` — часть Spring Data JPA, автоматически реализует CRUD операции.
|
||||
|
||||
---
|
||||
|
||||
## 17. Что такое соглашение по умолчанию (convention over configuration)?
|
||||
|
||||
**Convention over Configuration** — принцип, когда фреймворк использует разумные значения по умолчанию, уменьшая необходимость в конфигурации.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/repository/ArtistRepository.java`, строки 13-17:
|
||||
```java
|
||||
List<ArtistEntity> findByEpochId(Long epochId);
|
||||
```
|
||||
Spring Data автоматически создает SQL-запрос по имени метода, не нужно писать реализацию.
|
||||
|
||||
---
|
||||
|
||||
## 18. Для чего используется аннотация @Entity?
|
||||
|
||||
`@Entity` помечает класс как JPA сущность, которая будет отображаться в таблицу БД.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строка 9:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "artists")
|
||||
public class ArtistEntity extends BaseEntity {
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Для чего используется аннотация @SecondaryTable?
|
||||
|
||||
`@SecondaryTable` используется для отображения сущности на несколько таблиц (в нашем проекте не используется).
|
||||
|
||||
---
|
||||
|
||||
## 20. Для чего используются аннотации @Embeddable и @Embedded?
|
||||
|
||||
`@Embeddable` и `@Embedded` используются для встраивания одного объекта в другой без отдельной таблицы (в нашем проекте не используется).
|
||||
|
||||
---
|
||||
|
||||
## 21. Для чего используются аннотации @Id и @GeneratedValue?
|
||||
|
||||
`@Id` помечает поле как первичный ключ, `@GeneratedValue` указывает стратегию генерации ID.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/BaseEntity.java`, строки 11-13:
|
||||
```java
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
@SequenceGenerator(name = "hibernate_sequence")
|
||||
protected Long id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. Для чего используются аннотации @Embeddable и @EmbeddedId?
|
||||
|
||||
Используются для составных первичных ключей через встраиваемый класс (в нашем проекте не используется).
|
||||
|
||||
---
|
||||
|
||||
## 23. Для чего используется аннотация @IdClass?
|
||||
|
||||
`@IdClass` используется для составных первичных ключей через отдельный класс (в нашем проекте не используется).
|
||||
|
||||
---
|
||||
|
||||
## 24. В чем разница между @EmbeddedId и @IdClass?
|
||||
|
||||
- `@EmbeddedId` — составной ключ как встроенный объект
|
||||
- `@IdClass` — составной ключ как отдельный класс
|
||||
|
||||
В нашем проекте используется простой `@Id` (Long), составные ключи не используются.
|
||||
|
||||
---
|
||||
|
||||
## 25. Для чего используются аннотации @Basic и @Column?
|
||||
|
||||
`@Basic` указывает базовое отображение поля, `@Column` настраивает колонку в таблице.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 12-16:
|
||||
```java
|
||||
@Column(nullable = false) // Колонка не может быть null
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "text") // Тип колонки - text
|
||||
private String description;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 26. Для чего используются аннотации @Lob, @Basic, @Column, @Temporal и @Transient?
|
||||
|
||||
- `@Lob` — для больших объектов (BLOB/CLOB)
|
||||
- `@Basic` — базовое отображение
|
||||
- `@Column` — настройка колонки
|
||||
- `@Temporal` — для дат/времени
|
||||
- `@Transient` — поле не сохраняется в БД
|
||||
|
||||
**Пример из проекта:**
|
||||
- `@Column` используется в `ArtistEntity.java`, строка 15:
|
||||
```java
|
||||
@Column(columnDefinition = "text") // Текстовое поле
|
||||
private String description;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 27. Для чего используется аннотация @Enumerated?
|
||||
|
||||
`@Enumerated` используется для сохранения enum в БД (в нашем проекте не используется).
|
||||
|
||||
---
|
||||
|
||||
## 28. Для чего используется аннотация @Access?
|
||||
|
||||
`@Access` определяет способ доступа к полям (FIELD или PROPERTY) (в нашем проекте используется FIELD по умолчанию).
|
||||
|
||||
---
|
||||
|
||||
## 29. Какие аннотации используются для отображения разных типов связей?
|
||||
|
||||
- `@OneToOne` — один к одному
|
||||
- `@OneToMany` — один ко многим
|
||||
- `@ManyToOne` — многие к одному
|
||||
- `@ManyToMany` — многие ко многим
|
||||
|
||||
**Примеры из проекта:**
|
||||
- `@ManyToOne`: `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строка 19:
|
||||
```java
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
```
|
||||
|
||||
- `@OneToMany`: `demo/src/main/java/com/example/demo/entity/EpochEntity.java`, строка 18:
|
||||
```java
|
||||
@OneToMany(mappedBy = "epoch")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 30. Приведите пример реализации однонаправленной one to one связи.
|
||||
|
||||
В нашем проекте нет one-to-one связи. Пример был бы:
|
||||
```java
|
||||
@Entity
|
||||
public class ArtistEntity {
|
||||
@OneToOne
|
||||
@JoinColumn(name = "profile_id")
|
||||
private ProfileEntity profile;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 31. Приведите пример реализации однонаправленной one to many связи.
|
||||
|
||||
В нашем проекте все связи двунаправленные. Пример однонаправленной:
|
||||
```java
|
||||
@Entity
|
||||
public class EpochEntity {
|
||||
@OneToMany
|
||||
@JoinColumn(name = "epoch_id")
|
||||
private List<ArtistEntity> artists;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 32. Приведите пример реализации двунаправленной one to many связи.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/EpochEntity.java`, строки 18-20:
|
||||
```java
|
||||
@OneToMany(mappedBy = "epoch")
|
||||
@OrderBy("id ASC")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
```
|
||||
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строки 18-20:
|
||||
```java
|
||||
@JoinColumn(name = "epoch_id", nullable = false)
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
```
|
||||
|
||||
Связь двунаправленная: `EpochEntity` имеет коллекцию `artists`, а `ArtistEntity` имеет ссылку `epoch`.
|
||||
|
||||
---
|
||||
|
||||
## 33. Приведите пример реализации двунаправленной many to many связи.
|
||||
|
||||
В нашем проекте нет many-to-many связи. Пример:
|
||||
```java
|
||||
@Entity
|
||||
public class ArtistEntity {
|
||||
@ManyToMany
|
||||
@JoinTable(name = "artist_genre",
|
||||
joinColumns = @JoinColumn(name = "artist_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "genre_id"))
|
||||
private Set<GenreEntity> genres;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 34. Какие стратегии выборки данных для связей можно использовать?
|
||||
|
||||
- **LAZY** — ленивая загрузка (по умолчанию для `@OneToMany`, `@ManyToMany`)
|
||||
- **EAGER** — жадная загрузка (по умолчанию для `@OneToOne`, `@ManyToOne`)
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/EpochEntity.java`, строка 18:
|
||||
```java
|
||||
@OneToMany(mappedBy = "epoch") // LAZY по умолчанию
|
||||
private Set<ArtistEntity> artists;
|
||||
```
|
||||
|
||||
- `demo/src/main/java/com/example/demo/entity/ArtistEntity.java`, строка 19:
|
||||
```java
|
||||
@ManyToOne // EAGER по умолчанию
|
||||
private EpochEntity epoch;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 35. Чем отличаются стратегии Lazy и Eager?
|
||||
|
||||
- **LAZY** — данные загружаются только при обращении (ленивая загрузка)
|
||||
- **EAGER** — данные загружаются сразу вместе с родительским объектом (жадная загрузка)
|
||||
|
||||
**Пример из проекта:**
|
||||
- При загрузке `ArtistEntity` эпоха загружается сразу (EAGER)
|
||||
- При загрузке `EpochEntity` артисты загружаются только при обращении к `getArtists()` (LAZY)
|
||||
|
||||
---
|
||||
|
||||
## 36. Как принимается решение об используемой стратегии выборки данных?
|
||||
|
||||
Решение принимается на основе:
|
||||
- Частоты использования связи
|
||||
- Производительности
|
||||
- Размера связанных данных
|
||||
|
||||
**Пример из проекта:**
|
||||
- `@ManyToOne` — EAGER, так как обычно нужна эпоха/страна артиста
|
||||
- `@OneToMany` — LAZY, так как коллекция может быть большой
|
||||
|
||||
---
|
||||
|
||||
## 37. Для чего используются аннотации @OrderBy и @OrderColumn?
|
||||
|
||||
`@OrderBy` сортирует коллекцию при загрузке, `@OrderColumn` сохраняет порядок в отдельной колонке.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/EpochEntity.java`, строки 18-20:
|
||||
```java
|
||||
@OneToMany(mappedBy = "epoch")
|
||||
@OrderBy("id ASC") // Сортировка по id при загрузке
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 38. Когда для работы со связями следует использовать изменение данных в коллекции объекта?
|
||||
|
||||
Когда связь управляется владельцем (owner side), изменения в коллекции синхронизируются с БД.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/entity/EpochEntity.java`, строки 43-50:
|
||||
```java
|
||||
public void addArtist(ArtistEntity artist) {
|
||||
if (!artists.contains(artist)) {
|
||||
artists.add(artist); // Изменение коллекции
|
||||
if (artist.getEpoch() != this) {
|
||||
artist.setEpoch(this); // Синхронизация обратной связи
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 39. Когда для работы со связями следует использовать DAO?
|
||||
|
||||
DAO (Data Access Object) используется для сложных операций, которые нельзя выразить через методы сущности.
|
||||
|
||||
**Пример из проекта:**
|
||||
- `demo/src/main/java/com/example/demo/repository/ArtistRepository.java`, строки 19-23:
|
||||
```java
|
||||
@Query("select count(a) as totalArtists, ...")
|
||||
ArtistStatsProjection getArtistsStatistics();
|
||||
```
|
||||
Сложные запросы со статистикой выполняются через репозиторий (аналог DAO), а не через методы сущности.
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
# Инструкция по запуску приложения
|
||||
|
||||
## Требования
|
||||
|
||||
- **Java 21** (JDK 21)
|
||||
- **Gradle** (обычно входит в проект через Gradle Wrapper)
|
||||
|
||||
Проверьте версию Java:
|
||||
```bash
|
||||
java -version
|
||||
```
|
||||
|
||||
Должно быть что-то вроде:
|
||||
```
|
||||
openjdk version "21.x.x"
|
||||
```
|
||||
|
||||
## Способы запуска
|
||||
|
||||
### 1. Через Gradle Wrapper (рекомендуется)
|
||||
|
||||
#### Windows:
|
||||
```bash
|
||||
cd demo
|
||||
.\gradlew.bat bootRun
|
||||
```
|
||||
|
||||
#### Linux/Mac:
|
||||
```bash
|
||||
cd demo
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
### 2. Через IDE (IntelliJ IDEA / Eclipse)
|
||||
|
||||
1. Откройте проект в IDE
|
||||
2. Найдите класс `DemoApplication.java` в пакете `com.example.demo`
|
||||
3. Правой кнопкой мыши → `Run 'DemoApplication'`
|
||||
|
||||
### 3. Сборка и запуск JAR файла
|
||||
|
||||
#### Сборка:
|
||||
```bash
|
||||
cd demo
|
||||
.\gradlew.bat bootJar # Windows
|
||||
# или
|
||||
./gradlew bootJar # Linux/Mac
|
||||
```
|
||||
|
||||
#### Запуск:
|
||||
```bash
|
||||
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar
|
||||
```
|
||||
|
||||
## Что происходит при запуске
|
||||
|
||||
1. **Создается база данных H2** в файле `./data.mv.db` (в папке `demo`)
|
||||
2. **Автоматически создаются таблицы** (благодаря `ddl-auto=create`):
|
||||
- `epochs`
|
||||
- `countries`
|
||||
- `artists`
|
||||
3. **Автоматически создаются начальные данные** (если база пустая):
|
||||
- **Страны**: Россия, США, Великобритания, Германия, Франция, Япония
|
||||
- **Эпохи**: 1970-е, 1980-е, 1990-е, 2000-е, 2010-е, 2020-е
|
||||
4. **Приложение запускается** на порту **8080**
|
||||
|
||||
> **Примечание**: Начальные данные создаются только если база данных пустая. При последующих запусках данные сохраняются (если не удалить файл `data.mv.db`).
|
||||
|
||||
## Проверка работы
|
||||
|
||||
### 1. Проверка через браузер
|
||||
|
||||
Откройте в браузере:
|
||||
- **Swagger UI**: http://localhost:8080/swagger-ui.html
|
||||
- **API Docs**: http://localhost:8080/api-docs
|
||||
|
||||
### 2. Проверка через H2 Console
|
||||
|
||||
1. Откройте в браузере: http://localhost:8080/h2-console
|
||||
2. Настройки подключения:
|
||||
- **JDBC URL**: `jdbc:h2:file:./data`
|
||||
- **User Name**: `sa`
|
||||
- **Password**: `sa`
|
||||
3. Нажмите "Connect"
|
||||
4. Выполните SQL запрос:
|
||||
```sql
|
||||
SELECT * FROM artists;
|
||||
SELECT * FROM epochs;
|
||||
SELECT * FROM countries;
|
||||
```
|
||||
|
||||
### 3. Проверка через API
|
||||
|
||||
Используйте Swagger UI или любой HTTP клиент (Postman, curl):
|
||||
|
||||
#### Примеры запросов:
|
||||
|
||||
**Создать эпоху:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/epochs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "1980-е"}'
|
||||
```
|
||||
|
||||
**Создать страну:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/countries \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Россия"}'
|
||||
```
|
||||
|
||||
**Получить всех артистов:**
|
||||
```bash
|
||||
curl http://localhost:8080/api/artists
|
||||
```
|
||||
|
||||
## Запуск тестов
|
||||
|
||||
### Через Gradle:
|
||||
```bash
|
||||
cd demo
|
||||
.\gradlew.bat test # Windows
|
||||
# или
|
||||
./gradlew test # Linux/Mac
|
||||
```
|
||||
|
||||
### Через IDE:
|
||||
Правой кнопкой на папке `test` → `Run Tests`
|
||||
|
||||
## Структура базы данных
|
||||
|
||||
После первого запуска в папке `demo` появится файл:
|
||||
- `data.mv.db` - файл базы данных H2
|
||||
|
||||
**Важно**: При каждом запуске с `ddl-auto=create` таблицы пересоздаются заново, поэтому данные не сохраняются между перезапусками.
|
||||
|
||||
Если нужно сохранять данные между запусками, измените в `application.properties`:
|
||||
```properties
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
```
|
||||
|
||||
## Возможные проблемы
|
||||
|
||||
### 1. Порт 8080 занят
|
||||
|
||||
**Ошибка**: `Port 8080 is already in use`
|
||||
|
||||
**Решение**:
|
||||
- Измените порт в `application.properties`:
|
||||
```properties
|
||||
server.port=8081
|
||||
```
|
||||
- Или остановите процесс, использующий порт 8080
|
||||
|
||||
### 2. Java версия не подходит
|
||||
|
||||
**Ошибка**: `Unsupported class file major version`
|
||||
|
||||
**Решение**: Установите Java 21
|
||||
|
||||
### 3. База данных не создается
|
||||
|
||||
**Решение**:
|
||||
- Убедитесь, что у приложения есть права на запись в папку `demo`
|
||||
- Проверьте настройки в `application.properties`
|
||||
|
||||
### 4. Ошибки компиляции
|
||||
|
||||
**Решение**:
|
||||
```bash
|
||||
cd demo
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
## Полезные команды
|
||||
|
||||
### Очистка проекта:
|
||||
```bash
|
||||
.\gradlew.bat clean
|
||||
```
|
||||
|
||||
### Пересборка:
|
||||
```bash
|
||||
.\gradlew.bat clean build
|
||||
```
|
||||
|
||||
### Просмотр зависимостей:
|
||||
```bash
|
||||
.\gradlew.bat dependencies
|
||||
```
|
||||
|
||||
### Запуск с отладкой:
|
||||
Добавьте в `application.properties`:
|
||||
```properties
|
||||
logging.level.com.example.demo=DEBUG
|
||||
```
|
||||
|
||||
## Остановка приложения
|
||||
|
||||
- В терминале: `Ctrl + C`
|
||||
- В IDE: нажмите кнопку "Stop" в панели запуска
|
||||
|
||||
## Следующие шаги
|
||||
|
||||
1. Откройте Swagger UI: http://localhost:8080/swagger-ui.html
|
||||
2. Протестируйте API через Swagger
|
||||
3. Проверьте данные в H2 Console
|
||||
4. Запустите тесты для проверки функциональности
|
||||
|
||||
@@ -18,18 +18,10 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
ext {
|
||||
h2Version = "2.4.240"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation "com.h2database:h2:${h2Version}"
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
BIN
demo/data.mv.db
BIN
demo/data.mv.db
Binary file not shown.
@@ -1,60 +0,0 @@
|
||||
2025-11-14 20:15:51.893939+04:00 jdbc[13]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Синтаксическая ошибка в выражении SQL "[*]DROPTABLE COUNTRIES"; ожидалось "DELETE, DROP"
|
||||
Syntax error in SQL statement "[*]DROPTABLE COUNTRIES"; expected "DELETE, DROP"; SQL statement:
|
||||
DROPTABLE COUNTRIES [42001-240]
|
||||
2025-11-14 20:16:02.103322+04:00 jdbc[13]: exception
|
||||
org.h2.jdbc.JdbcSQLSyntaxErrorException: Невозможно удалить "COUNTRIES", пока существует зависимый объект "FK7SDOJ1330RH52SHDCVIL7SD4J"
|
||||
Cannot drop "COUNTRIES" because "FK7SDOJ1330RH52SHDCVIL7SD4J" depends on it; SQL statement:
|
||||
DROP TABLE COUNTRIES [90107-240]
|
||||
at org.h2.message.DbException.getJdbcSQLException(DbException.java:644)
|
||||
at org.h2.message.DbException.getJdbcSQLException(DbException.java:489)
|
||||
at org.h2.message.DbException.get(DbException.java:223)
|
||||
at org.h2.command.ddl.DropTable.prepareDrop(DropTable.java:104)
|
||||
at org.h2.command.ddl.DropTable.update(DropTable.java:129)
|
||||
at org.h2.command.CommandContainer.update(CommandContainer.java:139)
|
||||
at org.h2.command.Command.executeUpdate(Command.java:306)
|
||||
at org.h2.command.Command.executeUpdate(Command.java:250)
|
||||
at org.h2.jdbc.JdbcStatement.executeInternal(JdbcStatement.java:262)
|
||||
at org.h2.jdbc.JdbcStatement.execute(JdbcStatement.java:231)
|
||||
at org.h2.server.web.WebApp.getResult(WebApp.java:1344)
|
||||
at org.h2.server.web.WebApp.query(WebApp.java:1142)
|
||||
at org.h2.server.web.WebApp.query(WebApp.java:1118)
|
||||
at org.h2.server.web.WebApp.process(WebApp.java:244)
|
||||
at org.h2.server.web.WebApp.processRequest(WebApp.java:176)
|
||||
at org.h2.server.web.JakartaWebServlet.doGet(JakartaWebServlet.java:129)
|
||||
at org.h2.server.web.JakartaWebServlet.doPost(JakartaWebServlet.java:166)
|
||||
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:590)
|
||||
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:206)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
|
||||
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
|
||||
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
|
||||
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
|
||||
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
|
||||
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
|
||||
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
|
||||
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
|
||||
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
|
||||
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
|
||||
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
|
||||
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
|
||||
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
|
||||
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
|
||||
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
|
||||
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
|
||||
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)
|
||||
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1736)
|
||||
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
|
||||
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
|
||||
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
|
||||
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
|
||||
at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
@@ -11,8 +11,8 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
public void addCorsMappings(@NonNull CorsRegistry registry) {
|
||||
registry
|
||||
.addMapping(Constants.API_URL + "/**")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedOrigins("http://localhost:3000", "http://localhost:5173", Constants.DEV_ORIGIN)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
|
||||
.allowedOrigins("http://localhost:3000", "http://localhost:5173", "http://localhost:5174", Constants.DEV_ORIGIN)
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class ArtistRq {
|
||||
@NotBlank
|
||||
@Size(min = 0, max = 500)
|
||||
private String name;
|
||||
private String description;
|
||||
@NotNull
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class CountryRq {
|
||||
@NotBlank
|
||||
@Size(min = 0, max = 50)
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.example.demo.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
public class EpochRq {
|
||||
@NotBlank
|
||||
@Size(min = 0, max = 7)
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "artists")
|
||||
public class ArtistEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "text")
|
||||
private String description;
|
||||
|
||||
@JoinColumn(name = "epoch_id", nullable = false)
|
||||
@ManyToOne
|
||||
private EpochEntity epoch;
|
||||
|
||||
@JoinColumn(name = "country_id", nullable = false)
|
||||
@ManyToOne
|
||||
private CountryEntity country;
|
||||
|
||||
public ArtistEntity() {
|
||||
@@ -31,8 +14,8 @@ public class ArtistEntity extends BaseEntity {
|
||||
this();
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
setEpoch(epoch);
|
||||
setCountry(country);
|
||||
this.epoch = epoch;
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -56,18 +39,7 @@ public class ArtistEntity extends BaseEntity {
|
||||
}
|
||||
|
||||
public void setEpoch(EpochEntity epoch) {
|
||||
if (this.epoch == epoch) {
|
||||
return;
|
||||
}
|
||||
if (this.epoch != null) {
|
||||
EpochEntity oldEpoch = this.epoch;
|
||||
this.epoch = null;
|
||||
oldEpoch.removeArtist(this);
|
||||
}
|
||||
this.epoch = epoch;
|
||||
if (epoch != null) {
|
||||
epoch.addArtist(this);
|
||||
}
|
||||
}
|
||||
|
||||
public CountryEntity getCountry() {
|
||||
@@ -75,18 +47,7 @@ public class ArtistEntity extends BaseEntity {
|
||||
}
|
||||
|
||||
public void setCountry(CountryEntity country) {
|
||||
if (this.country == country) {
|
||||
return;
|
||||
}
|
||||
if (this.country != null) {
|
||||
CountryEntity oldCountry = this.country;
|
||||
this.country = null;
|
||||
oldCountry.removeArtist(this);
|
||||
}
|
||||
this.country = country;
|
||||
if (country != null) {
|
||||
country.addArtist(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,6 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import jakarta.persistence.SequenceGenerator;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class BaseEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE)
|
||||
@SequenceGenerator(name = "hibernate_sequence")
|
||||
protected Long id;
|
||||
|
||||
protected BaseEntity() {
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "countries")
|
||||
public class CountryEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "country")
|
||||
@OrderBy("id ASC")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
|
||||
public CountryEntity() {
|
||||
super();
|
||||
@@ -35,27 +19,5 @@ public class CountryEntity extends BaseEntity {
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Set<ArtistEntity> getArtists() {
|
||||
return artists;
|
||||
}
|
||||
|
||||
public void addArtist(ArtistEntity artist) {
|
||||
if (!artists.contains(artist)) {
|
||||
artists.add(artist);
|
||||
if (artist.getCountry() != this) {
|
||||
artist.setCountry(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeArtist(ArtistEntity artist) {
|
||||
if (artists.contains(artist)) {
|
||||
artists.remove(artist);
|
||||
if (artist.getCountry() == this) {
|
||||
artist.setCountry(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
package com.example.demo.entity;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OrderBy;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "epochs")
|
||||
public class EpochEntity extends BaseEntity {
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "epoch")
|
||||
@OrderBy("id ASC")
|
||||
private Set<ArtistEntity> artists = new HashSet<>();
|
||||
|
||||
public EpochEntity() {
|
||||
super();
|
||||
@@ -35,27 +19,5 @@ public class EpochEntity extends BaseEntity {
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Set<ArtistEntity> getArtists() {
|
||||
return artists;
|
||||
}
|
||||
|
||||
public void addArtist(ArtistEntity artist) {
|
||||
if (!artists.contains(artist)) {
|
||||
artists.add(artist);
|
||||
if (artist.getEpoch() != this) {
|
||||
artist.setEpoch(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeArtist(ArtistEntity artist) {
|
||||
if (artists.contains(artist)) {
|
||||
artists.remove(artist);
|
||||
if (artist.getEpoch() == this) {
|
||||
artist.setEpoch(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
public interface ArtistStatsProjection {
|
||||
Long getTotalArtists();
|
||||
Long getArtistsWithDescription();
|
||||
Long getArtistsWithoutDescription();
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
|
||||
public interface CountryStatsProjection {
|
||||
CountryEntity getCountry();
|
||||
Long getArtistsCount();
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.example.demo.entity.projection;
|
||||
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
|
||||
public interface EpochStatsProjection {
|
||||
EpochEntity getEpoch();
|
||||
Long getArtistsCount();
|
||||
}
|
||||
|
||||
@@ -1,39 +1,10 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.example.demo.entity.ArtistEntity;
|
||||
import com.example.demo.entity.projection.ArtistStatsProjection;
|
||||
|
||||
public interface ArtistRepository extends JpaRepository<ArtistEntity, Long> {
|
||||
List<ArtistEntity> findByEpochId(Long epochId);
|
||||
|
||||
List<ArtistEntity> findByCountryId(Long countryId);
|
||||
|
||||
List<ArtistEntity> findByEpochIdAndCountryId(Long epochId, Long countryId);
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a")
|
||||
ArtistStatsProjection getArtistsStatistics();
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a " +
|
||||
"where a.epoch.id = :epochId")
|
||||
ArtistStatsProjection getArtistsStatisticsByEpoch(@Param("epochId") Long epochId);
|
||||
|
||||
@Query("select count(a) as totalArtists, " +
|
||||
"sum(case when a.description is not null and length(a.description) > 0 then 1 else 0 end) as artistsWithDescription, " +
|
||||
"sum(case when a.description is null or length(a.description) = 0 then 1 else 0 end) as artistsWithoutDescription " +
|
||||
"from ArtistEntity a " +
|
||||
"where a.country.id = :countryId")
|
||||
ArtistStatsProjection getArtistsStatisticsByCountry(@Param("countryId") Long countryId);
|
||||
@Repository
|
||||
public class ArtistRepository extends MapRepository<ArtistEntity> {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.ulstu.is.server.repository;
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -13,3 +13,4 @@ public interface CommonRepository<E, T> {
|
||||
|
||||
void deleteAll();
|
||||
}
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.example.demo.entity.CountryEntity;
|
||||
import com.example.demo.entity.projection.CountryStatsProjection;
|
||||
|
||||
public interface CountryRepository extends JpaRepository<CountryEntity, Long> {
|
||||
Optional<CountryEntity> findOneByNameIgnoreCase(String name);
|
||||
|
||||
@Query("select c as country, count(a) as artistsCount " +
|
||||
"from CountryEntity c left join c.artists a " +
|
||||
"group by c " +
|
||||
"order by c.id")
|
||||
List<CountryStatsProjection> getAllCountriesStatistics();
|
||||
|
||||
@Query("select c as country, count(a) as artistsCount " +
|
||||
"from CountryEntity c left join c.artists a " +
|
||||
"where c.id = :countryId " +
|
||||
"group by c")
|
||||
CountryStatsProjection getCountryStatistics(@Param("countryId") Long countryId);
|
||||
@Repository
|
||||
public class CountryRepository extends MapRepository<CountryEntity> {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,10 @@
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.example.demo.entity.EpochEntity;
|
||||
import com.example.demo.entity.projection.EpochStatsProjection;
|
||||
|
||||
public interface EpochRepository extends JpaRepository<EpochEntity, Long> {
|
||||
Optional<EpochEntity> findOneByNameIgnoreCase(String name);
|
||||
|
||||
@Query("select e as epoch, count(a) as artistsCount " +
|
||||
"from EpochEntity e left join e.artists a " +
|
||||
"group by e " +
|
||||
"order by e.id")
|
||||
List<EpochStatsProjection> getAllEpochsStatistics();
|
||||
|
||||
@Query("select e as epoch, count(a) as artistsCount " +
|
||||
"from EpochEntity e left join e.artists a " +
|
||||
"where e.id = :epochId " +
|
||||
"group by e")
|
||||
EpochStatsProjection getEpochStatistics(@Param("epochId") Long epochId);
|
||||
@Repository
|
||||
public class EpochRepository extends MapRepository<EpochEntity> {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package ru.ulstu.is.server.repository;
|
||||
package com.example.demo.repository;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentNavigableMap;
|
||||
import java.util.concurrent.ConcurrentSkipListMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import ru.ulstu.is.server.entity.BaseEntity;
|
||||
import com.example.demo.entity.BaseEntity;
|
||||
|
||||
public abstract class MapRepository<E extends BaseEntity> implements CommonRepository<E, Long> {
|
||||
private final ConcurrentNavigableMap<Long, E> entities = new ConcurrentSkipListMap<>();
|
||||
@@ -66,3 +66,4 @@ public abstract class MapRepository<E extends BaseEntity> implements CommonRepos
|
||||
idGenerator.set(0L);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ package com.example.demo.service;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.dto.ArtistRq;
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
@@ -30,24 +28,20 @@ public class ArtistService {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public ArtistEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(ArtistEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ArtistRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ArtistRs get(Long id) {
|
||||
final ArtistEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs create(ArtistRq dto) {
|
||||
final EpochEntity epoch = epochService.getEntity(dto.getEpochId());
|
||||
final CountryEntity country = countryService.getEntity(dto.getCountryId());
|
||||
@@ -60,7 +54,6 @@ public class ArtistService {
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs update(Long id, ArtistRq dto) {
|
||||
ArtistEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
@@ -71,7 +64,6 @@ public class ArtistService {
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ArtistRs delete(Long id) {
|
||||
final ArtistEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
|
||||
@@ -3,8 +3,6 @@ package com.example.demo.service;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.dto.CountryRq;
|
||||
import com.example.demo.dto.CountryRs;
|
||||
@@ -23,31 +21,26 @@ public class CountryService {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public CountryEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(CountryEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CountryRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CountryRs get(Long id) {
|
||||
final CountryEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs create(CountryRq dto) {
|
||||
CountryEntity entity = new CountryEntity(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs update(Long id, CountryRq dto) {
|
||||
CountryEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
@@ -55,7 +48,6 @@ public class CountryService {
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CountryRs delete(Long id) {
|
||||
final CountryEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
|
||||
@@ -3,8 +3,6 @@ package com.example.demo.service;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.example.demo.dto.EpochRq;
|
||||
import com.example.demo.dto.EpochRs;
|
||||
@@ -23,31 +21,26 @@ public class EpochService {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public EpochEntity getEntity(Long id) {
|
||||
return repository.findById(id)
|
||||
.orElseThrow(() -> new NotFoundException(EpochEntity.class, id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<EpochRs> getAll() {
|
||||
return mapper.toRsDtoList(repository.findAll());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public EpochRs get(Long id) {
|
||||
final EpochEntity entity = getEntity(id);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs create(EpochRq dto) {
|
||||
EpochEntity entity = new EpochEntity(dto.getName());
|
||||
entity = repository.save(entity);
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs update(Long id, EpochRq dto) {
|
||||
EpochEntity entity = getEntity(id);
|
||||
entity.setName(dto.getName());
|
||||
@@ -55,7 +48,6 @@ public class EpochService {
|
||||
return mapper.toRsDto(entity);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public EpochRs delete(Long id) {
|
||||
final EpochEntity entity = getEntity(id);
|
||||
repository.delete(entity);
|
||||
|
||||
@@ -2,11 +2,3 @@ spring.application.name=demo
|
||||
server.port=8080
|
||||
springdoc.api-docs.path=/api-docs
|
||||
springdoc.swagger-ui.path=/swagger-ui.html
|
||||
|
||||
spring.datasource.url=jdbc:h2:file:./data
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=sa
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.h2.console.enabled=true
|
||||
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
@@ -15,7 +14,6 @@ import com.example.demo.mapper.ArtistMapper;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
|
||||
public class ArtistServiceTests {
|
||||
@Autowired
|
||||
private ArtistService service;
|
||||
@@ -27,20 +25,12 @@ public class ArtistServiceTests {
|
||||
private ArtistMapper mapper;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void getAllTest() {
|
||||
Assertions.assertNotNull(service.getAll());
|
||||
Assertions.assertTrue(service.getAll().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
// Создаем необходимые зависимости
|
||||
final var epochRq1 = new com.example.demo.dto.EpochRq();
|
||||
@@ -59,56 +49,52 @@ public class ArtistServiceTests {
|
||||
countryRq2.setName("США");
|
||||
final var country2 = countryService.create(countryRq2);
|
||||
|
||||
final ArtistRs artist1 = service.create(mapper.toRqDto("Artist 1", "Description 1", epoch1.getId(), country1.getId()));
|
||||
final ArtistRs artist2 = service.create(mapper.toRqDto("Artist 2", "Description 2", epoch2.getId(), country2.getId()));
|
||||
final ArtistRs artist3 = service.create(mapper.toRqDto("Artist 3", "Description 3", epoch1.getId(), country1.getId()));
|
||||
service.create(mapper.toRqDto("Artist 1", "Description 1", epoch1.getId(), country1.getId()));
|
||||
service.create(mapper.toRqDto("Artist 2", "Description 2", epoch2.getId(), country2.getId()));
|
||||
final ArtistRs last = service.create(mapper.toRqDto("Artist 3", "Description 3", epoch1.getId(), country1.getId()));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertNotNull(artist1.getId());
|
||||
Assertions.assertNotNull(artist2.getId());
|
||||
Assertions.assertNotNull(artist3.getId());
|
||||
|
||||
final ArtistRs cmpEntity = service.get(artist3.getId());
|
||||
Assertions.assertEquals(artist3.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(artist3.getName(), cmpEntity.getName());
|
||||
final ArtistRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final var allArtists = service.getAll();
|
||||
Assertions.assertFalse(allArtists.isEmpty());
|
||||
|
||||
final ArtistRs entity = allArtists.get(0);
|
||||
final Long entityId = entity.getId();
|
||||
final String test = "TEST ARTIST";
|
||||
final ArtistRs entity = service.get(3L);
|
||||
final String oldName = entity.getName();
|
||||
// Используем эпоху и страну из созданных в createTest
|
||||
final var epoch = entity.getEpoch();
|
||||
final var country = entity.getCountry();
|
||||
|
||||
final String test = "TEST ARTIST";
|
||||
final ArtistRs newEntity = service.update(entityId, mapper.toRqDto(test, "New Description", epoch.getId(), country.getId()));
|
||||
final ArtistRs newEntity = service.update(3L, mapper.toRqDto(test, "New Description", epoch.getId(), country.getId()));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(test, newEntity.getName());
|
||||
Assertions.assertNotEquals(oldName, newEntity.getName());
|
||||
|
||||
final ArtistRs cmpEntity = service.get(entityId);
|
||||
final ArtistRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@Order(3)
|
||||
void deleteTest() {
|
||||
final var allArtists = service.getAll();
|
||||
final int initialSize = allArtists.size();
|
||||
Assertions.assertTrue(initialSize > 0);
|
||||
|
||||
final ArtistRs toDelete = allArtists.get(0);
|
||||
final Long deleteId = toDelete.getId();
|
||||
|
||||
service.delete(deleteId);
|
||||
Assertions.assertEquals(initialSize - 1, service.getAll().size());
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final ArtistRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
// Используем эпоху и страну из существующего артиста
|
||||
final var epoch = last.getEpoch();
|
||||
final var country = last.getCountry();
|
||||
final ArtistRs newEntity = service.create(mapper.toRqDto("Artist 4", "Description 4", epoch.getId(), country.getId()));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import com.example.demo.dto.CountryRs;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
@@ -15,7 +14,6 @@ import com.example.demo.mapper.CountryMapper;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
|
||||
public class CountryServiceTests {
|
||||
@Autowired
|
||||
private CountryService service;
|
||||
@@ -23,68 +21,52 @@ public class CountryServiceTests {
|
||||
private CountryMapper mapper;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void getAllTest() {
|
||||
Assertions.assertNotNull(service.getAll());
|
||||
Assertions.assertTrue(service.getAll().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
final CountryRs country1 = service.create(mapper.toRqDto("Россия"));
|
||||
final CountryRs country2 = service.create(mapper.toRqDto("США"));
|
||||
final CountryRs country3 = service.create(mapper.toRqDto("Тайга"));
|
||||
service.create(mapper.toRqDto("Россия"));
|
||||
service.create(mapper.toRqDto("США"));
|
||||
final CountryRs last = service.create(mapper.toRqDto("Тайга"));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertNotNull(country1.getId());
|
||||
Assertions.assertNotNull(country2.getId());
|
||||
Assertions.assertNotNull(country3.getId());
|
||||
|
||||
final CountryRs cmpEntity = service.get(country3.getId());
|
||||
Assertions.assertEquals(country3.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(country3.getName(), cmpEntity.getName());
|
||||
final CountryRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final var allCountries = service.getAll();
|
||||
Assertions.assertFalse(allCountries.isEmpty());
|
||||
|
||||
final CountryRs entity = allCountries.get(0);
|
||||
final Long entityId = entity.getId();
|
||||
final String oldName = entity.getName();
|
||||
|
||||
final String test = "TEST";
|
||||
final CountryRs newEntity = service.update(entityId, mapper.toRqDto(test));
|
||||
final CountryRs entity = service.get(3L);
|
||||
final String oldName = entity.getName();
|
||||
final CountryRs newEntity = service.update(3L, mapper.toRqDto(test));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(test, newEntity.getName());
|
||||
Assertions.assertNotEquals(oldName, newEntity.getName());
|
||||
|
||||
final CountryRs cmpEntity = service.get(entityId);
|
||||
final CountryRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@Order(3)
|
||||
void deleteTest() {
|
||||
final var allCountries = service.getAll();
|
||||
final int initialSize = allCountries.size();
|
||||
Assertions.assertTrue(initialSize > 0);
|
||||
|
||||
final CountryRs toDelete = allCountries.get(0);
|
||||
final Long deleteId = toDelete.getId();
|
||||
|
||||
service.delete(deleteId);
|
||||
Assertions.assertEquals(initialSize - 1, service.getAll().size());
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final CountryRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
final CountryRs newEntity = service.create(mapper.toRqDto("Германия"));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
import com.example.demo.dto.EpochRs;
|
||||
import com.example.demo.error.NotFoundException;
|
||||
@@ -15,7 +14,6 @@ import com.example.demo.mapper.EpochMapper;
|
||||
|
||||
@SpringBootTest
|
||||
@TestMethodOrder(OrderAnnotation.class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS)
|
||||
public class EpochServiceTests {
|
||||
@Autowired
|
||||
private EpochService service;
|
||||
@@ -23,69 +21,53 @@ public class EpochServiceTests {
|
||||
private EpochMapper mapper;
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
void getTest() {
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(2)
|
||||
void getAllTest() {
|
||||
Assertions.assertNotNull(service.getAll());
|
||||
Assertions.assertTrue(service.getAll().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(3)
|
||||
@Order(1)
|
||||
void createTest() {
|
||||
final EpochRs epoch1 = service.create(mapper.toRqDto("1980-е"));
|
||||
final EpochRs epoch2 = service.create(mapper.toRqDto("1990-е"));
|
||||
final EpochRs epoch3 = service.create(mapper.toRqDto("2000-е"));
|
||||
service.create(mapper.toRqDto("1980-е"));
|
||||
service.create(mapper.toRqDto("1990-е"));
|
||||
final EpochRs last = service.create(mapper.toRqDto("2000-е"));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertNotNull(epoch1.getId());
|
||||
Assertions.assertNotNull(epoch2.getId());
|
||||
Assertions.assertNotNull(epoch3.getId());
|
||||
|
||||
final EpochRs cmpEntity = service.get(epoch3.getId());
|
||||
Assertions.assertEquals(epoch3.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(epoch3.getName(), cmpEntity.getName());
|
||||
final EpochRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(4)
|
||||
@Order(2)
|
||||
void updateTest() {
|
||||
final var allEpochs = service.getAll();
|
||||
Assertions.assertFalse(allEpochs.isEmpty());
|
||||
|
||||
final EpochRs entity = allEpochs.get(0);
|
||||
final Long entityId = entity.getId();
|
||||
final String oldName = entity.getName();
|
||||
|
||||
final String test = "TEST";
|
||||
final EpochRs newEntity = service.update(entityId, mapper.toRqDto(test));
|
||||
final EpochRs entity = service.get(3L);
|
||||
final String oldName = entity.getName();
|
||||
final EpochRs newEntity = service.update(3L, mapper.toRqDto(test));
|
||||
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(test, newEntity.getName());
|
||||
Assertions.assertNotEquals(oldName, newEntity.getName());
|
||||
|
||||
final EpochRs cmpEntity = service.get(entityId);
|
||||
final EpochRs cmpEntity = service.get(3L);
|
||||
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(5)
|
||||
@Order(3)
|
||||
void deleteTest() {
|
||||
final var allEpochs = service.getAll();
|
||||
final int initialSize = allEpochs.size();
|
||||
Assertions.assertTrue(initialSize > 0);
|
||||
|
||||
final EpochRs toDelete = allEpochs.get(0);
|
||||
final Long deleteId = toDelete.getId();
|
||||
|
||||
service.delete(deleteId);
|
||||
Assertions.assertEquals(initialSize - 1, service.getAll().size());
|
||||
Assertions.assertThrows(NotFoundException.class, () -> service.get(deleteId));
|
||||
service.delete(3L);
|
||||
Assertions.assertEquals(2, service.getAll().size());
|
||||
|
||||
final EpochRs last = service.get(2L);
|
||||
Assertions.assertEquals(2L, last.getId());
|
||||
|
||||
final EpochRs newEntity = service.create(mapper.toRqDto("2010-е"));
|
||||
Assertions.assertEquals(3, service.getAll().size());
|
||||
Assertions.assertEquals(4L, newEntity.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
|
||||
@@ -9,14 +9,16 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Received countries:', countries);
|
||||
console.log('Received epochs:', epochs);
|
||||
console.log('ArtistForm - Received countries:', countries);
|
||||
console.log('ArtistForm - Received epochs:', epochs);
|
||||
console.log('ArtistForm - Countries array length:', countries?.length);
|
||||
console.log('ArtistForm - Epochs array length:', epochs?.length);
|
||||
if (artist) {
|
||||
setFormData({
|
||||
name: artist.name,
|
||||
description: artist.description,
|
||||
epochId: artist.epoch?.id || '',
|
||||
countryId: artist.country?.id || ''
|
||||
epochId: artist.epoch?.id ? String(artist.epoch.id) : '',
|
||||
countryId: artist.country?.id ? String(artist.country.id) : ''
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
@@ -45,8 +47,8 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
||||
onSubmit({
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
epochId: Number(formData.epochId),
|
||||
countryId: Number(formData.countryId)
|
||||
epochId: formData.epochId ? Number(formData.epochId) : null,
|
||||
countryId: formData.countryId ? Number(formData.countryId) : null
|
||||
});
|
||||
if (!artist) {
|
||||
setFormData({
|
||||
@@ -100,12 +102,12 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
||||
required
|
||||
>
|
||||
<option value="">Выберите эпоху</option>
|
||||
{epochs.length > 0 ? (
|
||||
{epochs && epochs.length > 0 ? (
|
||||
epochs.map(epoch => (
|
||||
<option key={epoch.id} value={epoch.id}>{epoch.name}</option>
|
||||
<option key={epoch.id} value={String(epoch.id)}>{epoch.name}</option>
|
||||
))
|
||||
) : (
|
||||
<option disabled>Нет данных</option>
|
||||
<option disabled>Загрузка эпох... ({epochs?.length || 0} загружено)</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -120,12 +122,12 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
||||
required
|
||||
>
|
||||
<option value="">Выберите страну</option>
|
||||
{countries.length > 0 ? (
|
||||
{countries && countries.length > 0 ? (
|
||||
countries.map(country => (
|
||||
<option key={country.id} value={country.id}>{country.name}</option>
|
||||
<option key={country.id} value={String(country.id)}>{country.name}</option>
|
||||
))
|
||||
) : (
|
||||
<option disabled>Нет данных</option>
|
||||
<option disabled>Загрузка стран... ({countries?.length || 0} загружено)</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,11 @@ const PunkRockPage = () => {
|
||||
console.log('Fetched Artists:', artistsData);
|
||||
console.log('Fetched Countries:', countriesData);
|
||||
console.log('Fetched Epochs:', epochsData);
|
||||
setArtists(artistsData);
|
||||
setCountries(countriesData);
|
||||
setEpochs(epochsData);
|
||||
console.log('Countries length:', countriesData?.length);
|
||||
console.log('Epochs length:', epochsData?.length);
|
||||
setArtists(artistsData || []);
|
||||
setCountries(countriesData || []);
|
||||
setEpochs(epochsData || []);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
|
||||
BIN
~$bWork2Report.docx
Normal file
BIN
~$bWork2Report.docx
Normal file
Binary file not shown.
@@ -1,5 +0,0 @@
|
||||
Swagger UI URL:
|
||||
http://localhost:8080/swagger-ui/index.html
|
||||
|
||||
MVN Repository:
|
||||
https://mvnrepository.com/
|
||||
@@ -1,17 +0,0 @@
|
||||
Установка зависимостей
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Запуск в режиме разработки
|
||||
|
||||
```
|
||||
npm start
|
||||
```
|
||||
|
||||
Запуск для использования в продуктовой среде
|
||||
|
||||
```
|
||||
npm run prod
|
||||
```
|
||||
@@ -1,52 +0,0 @@
|
||||
import js from "@eslint/js";
|
||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||
import * as pluginImport from "eslint-plugin-import";
|
||||
import reactPlugin from "eslint-plugin-react";
|
||||
import reactHooks from "eslint-plugin-react-hooks";
|
||||
import reactRefresh from "eslint-plugin-react-refresh";
|
||||
import globals from "globals";
|
||||
import viteConfigObj from "./vite.config.js";
|
||||
|
||||
export default [
|
||||
{ ignores: ["dist", "vite.config.js"] },
|
||||
{
|
||||
files: ["**/*.{js,jsx}"],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
},
|
||||
"import/resolver": {
|
||||
node: {
|
||||
extensions: [".js", ".jsx", ".ts", ".tsx"],
|
||||
},
|
||||
vite: {
|
||||
viteConfig: viteConfigObj,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
react: reactPlugin,
|
||||
"react-hooks": reactHooks,
|
||||
},
|
||||
},
|
||||
js.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
reactRefresh.configs.recommended,
|
||||
reactPlugin.configs.flat.recommended,
|
||||
reactPlugin.configs.flat["jsx-runtime"],
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
"no-unused-vars": ["error", { varsIgnorePattern: "^[A-Z_]" }],
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"react/prop-types": ["off"],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="ru" class="h-100">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Мой сайт</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<div class="h-100 d-flex flex-column" id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"checkJs": true,
|
||||
"paths": {
|
||||
"@entities/*": ["./src/entities/*"],
|
||||
"@pages/*": ["./src/pages/*"],
|
||||
"@shared/*": ["./src/shared/*"],
|
||||
"@widgets/*": ["./src/widgets/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
6037
Лекция 2 (3)/front/package-lock.json
generated
6037
Лекция 2 (3)/front/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"name": "int-prog",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "http-server -p 3000 ./dist/",
|
||||
"prod": "npm-run-all build serve",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-router-dom": "^7.5.2",
|
||||
"react-hook-form": "^7.56.3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-bootstrap-icons": "^1.11.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"http-server": "^14.1.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"vite": "^6.2.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"eslint": "^9.21.0",
|
||||
"@eslint/js": "^9.21.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-import-resolver-vite": "^2.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.5",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"globals": "^15.15.0"
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="512" height="512" viewBox="0 0 756 756" xml:space="preserve">
|
||||
<desc>Created with Fabric.js 5.2.4</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
<g transform="matrix(1 0 0 1 540 540)" id="beef9158-5ec8-4a0c-8ee6-d6113704e99c" >
|
||||
<rect style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1; visibility: hidden;" vector-effect="non-scaling-stroke" x="-540" y="-540" rx="0" ry="0" width="1080" height="1080" />
|
||||
</g>
|
||||
<g transform="matrix(1 0 0 1 540 540)" id="9a1bc3be-5fa4-4249-87bd-f588de4f9768" >
|
||||
</g>
|
||||
<g transform="matrix(1 0 0 1 377.66 377.72)" >
|
||||
<g style="" vector-effect="non-scaling-stroke" >
|
||||
<g transform="matrix(2 0 0 2 0 0)" id="rect30" >
|
||||
<rect style="stroke: none; stroke-width: 0.494654; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(68,138,255); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" x="-182.5" y="-182.5" rx="7.1521459" ry="7.1521459" width="365" height="365" />
|
||||
</g>
|
||||
<g transform="matrix(1 0 0 1 -11.72 -94.45)" id="path33" >
|
||||
<path style="stroke: none; stroke-width: 2; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-489.41, -406)" d="M 614.48828 177.41406 C 585.03403 177.64615 555.102 180 528 180 C 509.268 180 474.11592 173.20281 457.41992 181.83789 C 441.12592 190.26483 422.34 188.47467 404 190.33789 C 365.3278 194.26683 326.787 197.16411 288 199.82031 C 261.38 201.64331 230.50279 201.95901 212.77539 226.25781 C 196.16547 249.02541 217.5264 250.08661 219.7832 270.00781 C 223.4616 302.47861 220 337.3084 220 370 C 220 424.688 216.64862 482.378 228.69922 536 C 234.13282 560.178 241.07448 585.21445 258.58008 603.81445 C 286.96128 633.96645 327.183 628.83584 364 632.33984 C 434.958 639.09184 507.266 629.02169 578 624.17969 C 620.728 621.25169 671.094 620.42538 710 600.35938 C 785.886 561.21938 770.028 472.564 770 402 C 769.978 343.7938 783.18837 263.01536 746.73438 213.03516 C 735.77437 198.01096 714.65742 193.13757 698.10742 187.56055 C 672.91842 179.07202 643.94253 177.18197 614.48828 177.41406 z M 451.28516 278.24414 C 508.18157 277.94477 575.51534 292.60888 617.99609 321.03906 C 665.52009 352.84446 673.38009 409.60016 625.99609 446.66016 C 609.19809 459.79816 583.47409 470.56595 561.99609 465.75195 C 553.62409 463.87395 546.58609 456.87805 537.99609 456.49805 C 522.09609 455.79605 512.77609 463.8217 495.99609 456.5957 C 476.36009 448.1397 462.80717 414.5508 471.20117 394.2168 C 474.36317 386.5588 491.70145 379.35103 497.93945 387.20703 C 508.38745 400.36503 494.40964 432.52702 516.18164 440.79102 C 530.60964 446.26702 534.58192 411.00817 555.66992 414.32617 C 563.02792 415.48417 559.99497 424.656 562.04297 430 C 565.00297 437.722 572.19609 445.56767 579.99609 448.51367 C 613.82609 461.29167 642.2743 406.694 629.9043 380 C 614.3683 346.4774 578.65809 326.98059 545.99609 313.90039 C 482.01609 288.27899 396.70598 263.8648 347.51758 330 C 318.46498 369.0616 331.97353 426.708 360.51953 462 C 409.14233 522.116 497.53009 557.79392 573.99609 553.91992 C 589.34209 553.14192 620.65517 534.20281 630.95117 522.88281 C 639.49717 513.48281 642.08522 495.18492 661.69922 503.79492 C 677.95722 510.93492 656.70214 530.07166 649.99414 535.34766 C 623.11814 556.49766 583.00409 562 549.99609 562 C 468.35409 562 372.36956 530.03 324.10156 460 C 268.98216 380.028 324.58909 293.80382 413.99609 280.85742 C 425.58059 279.17993 438.15521 278.31323 451.28516 278.24414 z" stroke-linecap="round" />
|
||||
</g>
|
||||
<g transform="matrix(1 0 0 1 -0.29 232.94)" id="path39" >
|
||||
<path style="stroke: none; stroke-width: 2; stroke-dasharray: none; stroke-linecap: butt; stroke-dashoffset: 0; stroke-linejoin: miter; stroke-miterlimit: 4; fill: rgb(255,255,255); fill-rule: nonzero; opacity: 1;" vector-effect="non-scaling-stroke" transform=" translate(-500.84, -733.38)" d="M 668.33594 643.47656 C 662.50706 643.55654 656.40463 644.00634 650 644.84766 C 557.68 656.97566 466.83 659.8418 374 659.8418 C 334.445 659.8418 285.9012 644.14103 248 660.95703 C 236.5832 666.02303 220.35223 674.28205 214.67383 685.99805 C 208.78323 698.15205 227.05259 701.474 224.27539 714 C 219.13139 737.202 163.19081 780.43105 192.00977 796.62305 C 213.03631 808.43705 243.23832 802.31272 265.85352 808.63672 C 272.15212 810.39872 271.30588 817.50928 276.70508 819.36328 C 297.12228 826.37328 330.3592 820 352 820 L 522 820 C 564.108 820 614.34895 827.63112 655.62695 819.70312 C 662.58095 818.36712 661.85506 809.62887 670.03906 808.29688 C 720.71106 800.04488 767.32912 814.45956 813.70312 783.85156 C 829.97513 773.11156 793.83588 738.344 786.17188 726 C 755.44819 676.52238 724.68174 642.70346 668.33594 643.47656 z M 632.96094 696.08594 C 656.9772 696.33526 670.5987 710.58 684.6582 732 C 687.2762 735.988 699.90202 751.25736 696.16602 755.94336 C 692.75802 760.21936 680.742 757.31823 676 757.49023 C 655.976 758.22223 636.016 761.75419 616 761.99219 C 537.304 762.92819 456.724 769.21231 378 763.82031 C 355.2814 762.26631 332.628 759.24231 310 756.57031 C 305.7648 756.07031 289.0863 758.23892 287.8125 752.79492 C 284.9009 740.34892 309.79135 708.10719 322.00195 708.11719 C 423.72835 708.20119 520.668 711.33947 622 696.85547 C 625.88675 696.29997 629.53004 696.05032 632.96094 696.08594 z" stroke-linecap="round" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 640 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 812 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 767 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
@@ -1,30 +0,0 @@
|
||||
import { FifthEditPage, FifthPage, FourthPage, MainPage, NotFoundPage, SecondPage, ThirdPage } from "@pages/index";
|
||||
import { ModalContainer, ModalProvider } from "@shared/modal";
|
||||
import { ToastProvider } from "@shared/toast";
|
||||
import { ToastContainer } from "@shared/toast/ui";
|
||||
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||
import { MainLayout } from "./layouts";
|
||||
|
||||
export const App = () => {
|
||||
return (
|
||||
<ModalProvider>
|
||||
<ToastProvider>
|
||||
<ModalContainer />
|
||||
<ToastContainer />
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<MainLayout />}>
|
||||
<Route path="/" element={<MainPage />} />
|
||||
<Route path="/page2" element={<SecondPage />} />
|
||||
<Route path="/page3" element={<ThirdPage />} />
|
||||
<Route path="/page4" element={<FourthPage />} />
|
||||
<Route path="/page5" element={<FifthPage />} />
|
||||
<Route path="/page5/edit/:studentId?" element={<FifthEditPage />} />
|
||||
<Route path="*" element={<NotFoundPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</ToastProvider>
|
||||
</ModalProvider>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./main-layout";
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Footer, Header } from "@widgets/index";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
export const MainLayout = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="flex-grow-1 container-fluid p-2">
|
||||
<Outlet />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./student-group-hook";
|
||||
export * from "./student-hook";
|
||||
export * from "./students-hook";
|
||||
@@ -1,20 +0,0 @@
|
||||
import { getAllItems } from "@shared/index";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export const useStudentGroup = () => {
|
||||
const [groups, setGroups] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
getAllItems("group").then((result) => {
|
||||
if (!ignore) {
|
||||
setGroups(result);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return groups;
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { createItem, getItem, updateItem } from "@shared/index";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const PATH = "student";
|
||||
|
||||
export const useStudent = () => {
|
||||
const params = useParams();
|
||||
const [student, setStudent] = useState(null);
|
||||
const studentId = params.studentId;
|
||||
|
||||
const getStudent = (id) => {
|
||||
return getItem(PATH, id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
if (!studentId) {
|
||||
return;
|
||||
}
|
||||
getStudent(studentId).then((result) => {
|
||||
if (!ignore) {
|
||||
setStudent(result);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [studentId]);
|
||||
|
||||
const saveStudent = async (data) => {
|
||||
let currentId = student?.id;
|
||||
data.groupId = data.group.id;
|
||||
delete data.group;
|
||||
if (!currentId) {
|
||||
const newStudent = await createItem(PATH, data);
|
||||
currentId = newStudent.id;
|
||||
} else {
|
||||
await updateItem(PATH, currentId, data);
|
||||
}
|
||||
const result = await getStudent(currentId);
|
||||
setStudent(result);
|
||||
};
|
||||
|
||||
const clearStudent = () => {
|
||||
setStudent(null);
|
||||
};
|
||||
|
||||
return { student, saveStudent, clearStudent };
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { deleteItem, getAllItems } from "@shared/index";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const PATH = "student";
|
||||
const PAGE_SIZE = 5;
|
||||
const PAGE_PARAM = "page";
|
||||
|
||||
export const useStudents = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [students, setStudents] = useState(null);
|
||||
const [page, setPage] = useState(parseInt(searchParams.get(PAGE_PARAM)) || 1);
|
||||
|
||||
const total = students?.pages ?? 0;
|
||||
const current = Math.min(Math.max(1, page), total);
|
||||
const pages = { current, total };
|
||||
|
||||
const getStudents = (page) => {
|
||||
return getAllItems(PATH, `_page=${page}&_per_page=${PAGE_SIZE}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let ignore = false;
|
||||
|
||||
getStudents(page).then((result) => {
|
||||
if (!ignore) {
|
||||
setStudents(result);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
ignore = true;
|
||||
};
|
||||
}, [page]);
|
||||
|
||||
const changePage = (newPage) => {
|
||||
setSearchParams((params) => {
|
||||
params.set(PAGE_PARAM, newPage);
|
||||
return params;
|
||||
});
|
||||
setPage(newPage);
|
||||
};
|
||||
|
||||
const deleteStudent = async (id) => {
|
||||
if (!id) {
|
||||
throw new Error("Student id is not defined");
|
||||
}
|
||||
await deleteItem(PATH, id);
|
||||
const newStudents = await getStudents(page);
|
||||
setStudents(newStudents);
|
||||
if (newStudents.pages < page) {
|
||||
changePage(newStudents.pages);
|
||||
}
|
||||
};
|
||||
|
||||
return { students: students ?? null, deleteStudent, pages, changePage };
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
/* eslint-disable import/export */
|
||||
export * from "./hooks";
|
||||
export * from "./model";
|
||||
export * from "./ui";
|
||||
@@ -1,7 +0,0 @@
|
||||
export const studentModel = {
|
||||
last_name: ["Фамилия", "text"],
|
||||
first_name: ["Имя", "text"],
|
||||
email: ["Почта", "email"],
|
||||
phone: ["Телефон", "tel"],
|
||||
bdate: ["Дата рождения", "date"],
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./students";
|
||||
export * from "./students-form";
|
||||
export * from "./students-table";
|
||||
@@ -1,106 +0,0 @@
|
||||
import { studentModel, useStudent, useStudentGroup } from "@entities/student";
|
||||
import { base64, useBSForm } from "@shared/index";
|
||||
import { TOAST_ACTION, useToastsDispatch } from "@shared/toast";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import "./styles.css";
|
||||
|
||||
export const StudentForm = () => {
|
||||
const groups = useStudentGroup();
|
||||
const { student, saveStudent, clearStudent } = useStudent();
|
||||
const { register, validated, handleSubmit, reset, setValue } = useBSForm(null, false);
|
||||
|
||||
const [image, setImage] = useState("/images/200.png");
|
||||
const [isSubmit, setIsSubmit] = useState(false);
|
||||
|
||||
const toast = useToastsDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
if (student) {
|
||||
reset(student);
|
||||
}
|
||||
if (student?.image) {
|
||||
setImage(student.image);
|
||||
}
|
||||
}, [student, reset]);
|
||||
|
||||
const handleSave = async (data) => {
|
||||
let text = "";
|
||||
setIsSubmit(true);
|
||||
try {
|
||||
await saveStudent(data);
|
||||
text = "Элемент успешно сохранен";
|
||||
} catch (error) {
|
||||
text = "Ошибка сохранения";
|
||||
console.error(error);
|
||||
}
|
||||
toast({
|
||||
type: TOAST_ACTION.add,
|
||||
title: "Сохранение",
|
||||
text,
|
||||
});
|
||||
setIsSubmit(false);
|
||||
};
|
||||
|
||||
const handleImageChange = async (event) => {
|
||||
const { files } = event.target;
|
||||
const file = await base64(files.item(0));
|
||||
setValue("image", file);
|
||||
setImage(file);
|
||||
};
|
||||
|
||||
const inputs = Object.keys(studentModel).map((field) => {
|
||||
return (
|
||||
<Form.Group className="mb-2" key={field} controlId={field}>
|
||||
<Form.Label>{studentModel[field][0]}</Form.Label>
|
||||
<Form.Control type={studentModel[field][1]} required disabled={isSubmit} {...register(field)} />
|
||||
</Form.Group>
|
||||
);
|
||||
});
|
||||
|
||||
const groupOptions = groups?.map((group) => {
|
||||
return (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-md-4 text-center">
|
||||
<img className="image-preview rounded" alt="placeholder" src={image} />
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<Form noValidate validated={validated} onSubmit={(event) => handleSubmit(event, handleSave)}>
|
||||
{inputs}
|
||||
<Form.Group className="mb-2" controlId="groupId">
|
||||
<Form.Label>Группа</Form.Label>
|
||||
<Form.Select
|
||||
className="mb-2"
|
||||
name="selected"
|
||||
required
|
||||
disabled={isSubmit}
|
||||
{...register("group.id")}
|
||||
>
|
||||
<option value="">Выберите группу</option>
|
||||
{groupOptions}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="image">
|
||||
<Form.Label>Фотография</Form.Label>
|
||||
<Form.Control type="file" accept="image/*" disabled={isSubmit} onChange={handleImageChange} />
|
||||
</Form.Group>
|
||||
<div className="text-center">
|
||||
<Button type="submit" disabled={isSubmit}>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button className="mx-2" type="button" disabled={isSubmit} onClick={() => clearStudent()}>
|
||||
Очистить
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
.image-preview {
|
||||
width: 200px;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Button, Table } from "react-bootstrap";
|
||||
import { PencilFill, TrashFill } from "react-bootstrap-icons";
|
||||
|
||||
export const StudentsTable = ({ data, onUpdate, onDelete }) => {
|
||||
const body = data?.map((student) => {
|
||||
return (
|
||||
<tr key={student.id}>
|
||||
<td>{student.id}</td>
|
||||
<td>{student.first_name}</td>
|
||||
<td>{student.last_name}</td>
|
||||
<td>{student.email}</td>
|
||||
<td>{student.phone}</td>
|
||||
<td>{student.bdate}</td>
|
||||
<td>{student.group?.name ?? ""}</td>
|
||||
<td className="p-1">
|
||||
<Button variant="warning" size="sm" onClick={() => onUpdate(student.id)}>
|
||||
<PencilFill />
|
||||
</Button>
|
||||
</td>
|
||||
<td className="p-1">
|
||||
<Button variant="danger" size="sm" onClick={() => onDelete(student.id)}>
|
||||
<TrashFill />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
const bodyRender = data ? (
|
||||
body
|
||||
) : (
|
||||
<tr>
|
||||
<td align="center" colSpan={100}>
|
||||
<h5 className="text-align-center">Данные отсутствуют или загружаются</h5>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
return (
|
||||
<Table hover responsive size="sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>№</th>
|
||||
<th>Фамилия</th>
|
||||
<th>Имя</th>
|
||||
<th>Почта</th>
|
||||
<th>Телефон</th>
|
||||
<th>Дата рождения</th>
|
||||
<th>Группа</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{bodyRender}</tbody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { StudentsTable, useStudents } from "@entities/student";
|
||||
import { useModal } from "@shared/modal";
|
||||
import { TOAST_ACTION, useToastsDispatch } from "@shared/toast";
|
||||
import { Pagination } from "@widgets/index";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { PlusCircle } from "react-bootstrap-icons";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
export const Students = () => {
|
||||
const navigate = useNavigate();
|
||||
const { students, deleteStudent, pages, changePage } = useStudents();
|
||||
const toast = useToastsDispatch();
|
||||
const { show } = useModal();
|
||||
|
||||
const deleteItem = async (id) => {
|
||||
let text = "";
|
||||
try {
|
||||
await deleteStudent(id);
|
||||
text = "Элемент успешно удален";
|
||||
} catch (error) {
|
||||
text = "Ошибка удаления";
|
||||
console.error(error);
|
||||
}
|
||||
toast({
|
||||
type: TOAST_ACTION.add,
|
||||
title: "Удаление",
|
||||
text,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = (id) => {
|
||||
navigate(`/page5/edit/${id}`);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
show("Удаление", "Удалить запись?", async () => await deleteItem(id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link to="/page5/edit">
|
||||
<Button className="mb-2">
|
||||
<PlusCircle /> Добавить студена
|
||||
</Button>
|
||||
</Link>
|
||||
<StudentsTable data={students} onUpdate={handleUpdate} onDelete={handleDelete} />
|
||||
<Pagination page={pages.current} total={pages.total} onChange={changePage} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./app";
|
||||
import "./styles.css";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
@@ -1,22 +0,0 @@
|
||||
import { StudentForm } from "@entities/student";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export const FifthEditPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<div className="mb-2">
|
||||
<StudentForm />
|
||||
</div>
|
||||
<div className="col-md-12 text-center">
|
||||
<Button onClick={handleClick}>Вернуться назад</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Students } from "@entities/student";
|
||||
|
||||
export const FifthPage = () => {
|
||||
return <Students />;
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Calc, Paragraphs, Separator } from "@widgets/index";
|
||||
|
||||
export const FourthPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Calc />
|
||||
<Separator />
|
||||
<Paragraphs />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from "./fifth-edit-page";
|
||||
export * from "./fifth-page";
|
||||
export * from "./fourth-page";
|
||||
export * from "./main-page";
|
||||
export * from "./not-found-page";
|
||||
export * from "./second-page";
|
||||
export * from "./third-page";
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Banner } from "@widgets/index";
|
||||
import { Link } from "react-router-dom";
|
||||
import "./styles.css";
|
||||
|
||||
export const MainPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Banner className="mb-4" />
|
||||
<section className="content">
|
||||
<h1>Пример web-страницы</h1>
|
||||
<h2>1. Структурные элементы</h2>
|
||||
<p>
|
||||
<b>
|
||||
Полужирное начертание <i>курсив</i>
|
||||
</b>
|
||||
</p>
|
||||
<p>
|
||||
Абзац 2 <Link to="/page2">Ссылка</Link>
|
||||
</p>
|
||||
<h3>1.1. Списки</h3>
|
||||
<p>Список маркированный:</p>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/page2" target="_blank">
|
||||
Элемент списка 1
|
||||
</Link>
|
||||
</li>
|
||||
<li>Элемент списка 2</li>
|
||||
<li>...</li>
|
||||
</ul>
|
||||
<p>Список нумерованный:</p>
|
||||
<ol>
|
||||
<li>Элемент списка 1</li>
|
||||
<li>Элемент списка 2</li>
|
||||
<li>...</li>
|
||||
</ol>
|
||||
<img
|
||||
className="d-block d-md-inline float-md-start mb-4 me-md-2 my-md-2 illustration"
|
||||
src="https://ulstu.ru/upload/iblock/1b4/7h8wjum3zmw61bjvb31s6gacil6mw6wq/ulgtu.jpg"
|
||||
alt="Main"
|
||||
/>
|
||||
<p>
|
||||
Lorem ipsum odor amet, consectetuer adipiscing elit. Mus nisl sociosqu sapien, suspendisse enim
|
||||
laoreet et. Taciti adipiscing cras ipsum libero fames mollis. Sociosqu aliquet a taciti ridiculus
|
||||
tincidunt dolor dui malesuada rutrum. Maecenas tellus quis suscipit proin egestas. Risus nostra erat
|
||||
porttitor habitasse platea donec quisque conubia. Consequat tempus est interdum leo et cras potenti
|
||||
ullamcorper.
|
||||
</p>
|
||||
<p>
|
||||
Dictum iaculis enim nullam luctus hac tincidunt suscipit dictum convallis. Suscipit id iaculis
|
||||
venenatis purus conubia lacinia suspendisse donec. Non adipiscing ultricies potenti euismod dapibus;
|
||||
quis morbi. Himenaeos eros elit non duis nullam ante dictum etiam platea. Taciti nunc nostra mi urna
|
||||
turpis nulla per congue. Mus vestibulum proin suscipit iaculis facilisis. Magnis in laoreet tempus
|
||||
quis dictum quis quam. Et interdum posuere pulvinar torquent vulputate lacus. Augue facilisis
|
||||
sodales lacinia leo ligula!
|
||||
</p>
|
||||
<p>
|
||||
Et placerat vehicula malesuada aptent fringilla sit ullamcorper maximus. Non pharetra morbi inceptos
|
||||
quis tellus aenean magnis aenean. Metus ante tincidunt himenaeos suspendisse arcu curabitur cubilia.
|
||||
Maximus aliquet odio potenti ex; class dapibus euismod. Pharetra sed praesent placerat efficitur
|
||||
dolor enim nisi. Maximus est suspendisse semper phasellus; cubilia luctus feugiat netus. Maecenas
|
||||
dis donec varius sapien tincidunt augue lectus. Porttitor id dui commodo vitae ad vivamus pulvinar.
|
||||
</p>
|
||||
<p>
|
||||
Consectetur aptent montes lacinia egestas augue nunc integer. Fames purus risus non rhoncus arcu
|
||||
quisque justo eu. Elementum natoque dapibus bibendum euismod justo fames velit. Venenatis senectus
|
||||
nisl odio facilisis laoreet fusce nibh at sollicitudin? Feugiat primis mus curae nostra tempor
|
||||
libero morbi ultrices dolor. Adipiscing class ligula vivamus eu facilisi non. Ipsum congue nascetur
|
||||
rhoncus dictumst varius quam risus in sem.
|
||||
</p>
|
||||
<p>
|
||||
Quam bibendum commodo a curae neque lacus. Mauris semper netus ridiculus ac cursus malesuada turpis
|
||||
tristique? Dui proin sit pharetra natoque gravida scelerisque. Donec mollis aliquam, venenatis
|
||||
bibendum mattis urna sed. Vel vel eleifend est justo efficitur augue habitant. At cras phasellus
|
||||
sapien torquent suscipit ex. Amet ornare mauris cubilia ullamcorper aptent molestie sem a. Curae
|
||||
dolor habitant id molestie ut fames orci. Per nec mattis, hendrerit aliquam mus enim aliquet.
|
||||
</p>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
.illustration {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.content p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.illustration {
|
||||
width: 50% !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 992px) {
|
||||
.illustration {
|
||||
width: 25% !important;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Button, Container } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const NotFoundPage = () => {
|
||||
return (
|
||||
<Container className="text-center">
|
||||
<h5>Страница не найдена</h5>
|
||||
<p>Страница, которую Вы ищете, не существует.</p>
|
||||
<Link to="/">
|
||||
<Button>На главную</Button>
|
||||
</Link>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
export const SecondPage = () => {
|
||||
return (
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<p>Вторая страница содержит пример рисунка (рис. 1) и таблицы (таб. 1).</p>
|
||||
<div>
|
||||
<img src="/images/logo.png" alt="logo" width="190" />
|
||||
<br />
|
||||
Рис. 1. Пример рисунка.
|
||||
</div>
|
||||
<table className="table table-bordered w-50 mt-2">
|
||||
<caption>Таблица 1. Пример таблицы.</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-25">Номер</th>
|
||||
<th>ФИО</th>
|
||||
<th className="w-25">Телефон</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>Иванов</td>
|
||||
<td>89999999999</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Петров</td>
|
||||
<td>89999999991</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { useBSForm } from "@shared/index";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
|
||||
const initState = {
|
||||
lastname: "",
|
||||
firstname: "",
|
||||
email: "",
|
||||
password: "",
|
||||
date: "",
|
||||
disabled: "Некоторое значение",
|
||||
readonly: "Некоторое значение",
|
||||
color: "#3c3c3c",
|
||||
checkbox1: false,
|
||||
checkbox2: true,
|
||||
radioExample: "0",
|
||||
selected: "",
|
||||
};
|
||||
|
||||
export const ThirdPage = () => {
|
||||
const { register, validated, handleSubmit } = useBSForm(initState);
|
||||
|
||||
const onSubmit = (values) => {
|
||||
console.debug(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<Form
|
||||
className="col-md-6"
|
||||
noValidate
|
||||
validated={validated}
|
||||
onSubmit={(event) => handleSubmit(event, onSubmit)}
|
||||
>
|
||||
<Form.Group className="mb-2" controlId="lastname">
|
||||
<Form.Label>Фамилия</Form.Label>
|
||||
<Form.Control type="text" required {...register("lastname")} />
|
||||
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
|
||||
<Form.Control.Feedback type="invalid">Some error!</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="firstname">
|
||||
<Form.Label>Имя</Form.Label>
|
||||
<Form.Control type="text" required {...register("firstname")} />
|
||||
<Form.Control.Feedback type="invalid">Some error!</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="email">
|
||||
<Form.Label>E-mail</Form.Label>
|
||||
<Form.Control type="email" required {...register("email")} />
|
||||
<Form.Control.Feedback>Looks good!</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="password">
|
||||
<Form.Label>Пароль</Form.Label>
|
||||
<Form.Control type="password" required autoComplete="off" {...register("password")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="date">
|
||||
<Form.Label>Дата</Form.Label>
|
||||
<Form.Control type="date" required {...register("date")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="disabled">
|
||||
<Form.Label>Выключенный компонент</Form.Label>
|
||||
<Form.Control type="text" disabled {...register("disabled")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="readonly">
|
||||
<Form.Label>Компонент только для чтения</Form.Label>
|
||||
<Form.Control type="text" readOnly {...register("readonly")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="color">
|
||||
<Form.Label>Выбор цвета</Form.Label>
|
||||
<Form.Control type="color" readOnly {...register("color")} />
|
||||
</Form.Group>
|
||||
<div className="mb-2 d-md-flex flex-md-row">
|
||||
<Form.Check className="me-md-3" type="checkbox" label="Флажок 1" {...register("checkbox1")} />
|
||||
<Form.Check type="checkbox" label="Флажок 2" {...register("checkbox2")} />
|
||||
</div>
|
||||
<div className="mb-2 d-md-flex flex-md-row">
|
||||
<Form.Check
|
||||
className="me-md-3"
|
||||
type="radio"
|
||||
name="radioExample"
|
||||
label="Переключатель 1"
|
||||
value="0"
|
||||
{...register("radioExample")}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
name="radioExample"
|
||||
label="Переключатель 2"
|
||||
value="1"
|
||||
{...register("radioExample")}
|
||||
/>
|
||||
</div>
|
||||
<Form.Select className="mb-2" name="selected" required {...register("selected")}>
|
||||
<option value="">Выберите значение</option>
|
||||
<option value="1">Один</option>
|
||||
<option value="2">Два</option>
|
||||
<option value="3">Три</option>
|
||||
</Form.Select>
|
||||
<Button className="d-block m-auto" type="submit">
|
||||
Отправить
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
export const base64 = async (file) => {
|
||||
const reader = new FileReader();
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onloadend = () => {
|
||||
const fileContent = reader.result;
|
||||
resolve(fileContent);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
reject(new Error("Oops, something went wrong with the file reader."));
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
const URL = "http://localhost:8080/api/1.0/";
|
||||
|
||||
const makeRequest = async (path, params, vars, method = "GET", data = null) => {
|
||||
try {
|
||||
const requestParams = params ? `?${params}` : "";
|
||||
const pathVariables = vars ? `/${vars}` : "";
|
||||
const options = { method };
|
||||
const hasBody = (method === "POST" || method === "PUT") && data;
|
||||
if (hasBody) {
|
||||
options.headers = { "Content-Type": "application/json;charset=utf-8" };
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
const response = await fetch(`${URL}${path}${pathVariables}${requestParams}`, options);
|
||||
if (!response.ok) {
|
||||
const errorJson = await response.json();
|
||||
console.debug(errorJson);
|
||||
throw new Error(`Response status: ${response.status}: ${errorJson.message}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
console.debug(path, json);
|
||||
return json;
|
||||
} catch (error) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAllItems = (path, params) => makeRequest(path, params);
|
||||
|
||||
export const getItem = (path, id) => makeRequest(path, null, id);
|
||||
|
||||
export const createItem = (path, data) => makeRequest(path, null, null, "POST", data);
|
||||
|
||||
export const updateItem = (path, id, data) => makeRequest(path, null, id, "PUT", data);
|
||||
|
||||
export const deleteItem = (path, id) => makeRequest(path, null, id, "DELETE");
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export const useBSForm = (initState, stayValidated = true) => {
|
||||
const { register, handleSubmit, setValue, reset } = useForm({ defaultValues: initState });
|
||||
const [validated, setValidated] = useState(false);
|
||||
|
||||
const handleSubmitWrapper = (event, onSubmit) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const form = event.currentTarget;
|
||||
if (form.checkValidity()) {
|
||||
handleSubmit(onSubmit)(event);
|
||||
}
|
||||
|
||||
setValidated(stayValidated ? true : !form.checkValidity());
|
||||
};
|
||||
|
||||
return { register, validated, handleSubmit: handleSubmitWrapper, setValue, reset };
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./base64";
|
||||
export * from "./client";
|
||||
export * from "./form";
|
||||
export * from "./storage";
|
||||
@@ -1,36 +0,0 @@
|
||||
import { createContext, useCallback, useContext, useMemo, useState } from "react";
|
||||
|
||||
const ModalContext = createContext(null);
|
||||
|
||||
export const ModalProvider = ({ children }) => {
|
||||
const [modal, setModal] = useState(null);
|
||||
|
||||
const show = useCallback((title, text, onConfirm) => {
|
||||
const close = () => setModal(() => null);
|
||||
const newModal = {};
|
||||
newModal.id = crypto.randomUUID();
|
||||
newModal.title = title;
|
||||
newModal.text = text;
|
||||
newModal.onConfirm = async () => {
|
||||
await onConfirm();
|
||||
close();
|
||||
};
|
||||
newModal.onClose = () => close();
|
||||
setModal(() => newModal);
|
||||
}, []);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
modal,
|
||||
show,
|
||||
}),
|
||||
[modal, show]
|
||||
);
|
||||
|
||||
return <ModalContext.Provider value={contextValue}>{children}</ModalContext.Provider>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useModal = () => {
|
||||
return useContext(ModalContext);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./context";
|
||||
export * from "./ui";
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Button, Modal } from "react-bootstrap";
|
||||
import { useModal } from "../context";
|
||||
|
||||
export const ModalContainer = () => {
|
||||
const { modal } = useModal();
|
||||
|
||||
if (!modal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal show={true} key={modal.id} backdrop="static" centered onHide={modal.onClose}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>{modal.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{modal.text}</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button className="w-25" onClick={modal.onClose}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button className="w-25" onClick={modal.onConfirm}>
|
||||
ОК
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export const lsSave = (key, value) => {
|
||||
localStorage.setItem(key, JSON.stringify(value));
|
||||
};
|
||||
|
||||
export const lsReadArray = (key) => JSON.parse(localStorage.getItem(key)) || [];
|
||||
@@ -1,84 +0,0 @@
|
||||
import { lsReadArray, lsSave } from "@shared/storage";
|
||||
import { createContext, useContext, useEffect, useReducer } from "react";
|
||||
|
||||
const KEY = "toasts";
|
||||
|
||||
const ToastContext = createContext(null);
|
||||
const ToastDispatcherContext = createContext(null);
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const TOAST_ACTION = {
|
||||
add: "add",
|
||||
hide: "hide",
|
||||
delete: "delete",
|
||||
deleteAll: "delete-all",
|
||||
};
|
||||
|
||||
const getDate = () => {
|
||||
const zeroPad = (num) => String(num).padStart(2, "0");
|
||||
const date = new Date();
|
||||
const day = zeroPad(date.getDate());
|
||||
const month = zeroPad(date.getMonth() + 1);
|
||||
const hours = zeroPad(date.getHours());
|
||||
const minutes = zeroPad(date.getMinutes());
|
||||
return `${day}.${month} ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
const toastsReducer = (toasts, action) => {
|
||||
switch (action.type) {
|
||||
case TOAST_ACTION.add: {
|
||||
return [
|
||||
...toasts,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
title: action.title,
|
||||
text: action.text,
|
||||
date: getDate(),
|
||||
show: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
case TOAST_ACTION.hide: {
|
||||
return toasts.map((toast) => {
|
||||
if (toast.id === action.id) {
|
||||
return { ...toast, show: false };
|
||||
} else {
|
||||
return toast;
|
||||
}
|
||||
});
|
||||
}
|
||||
case TOAST_ACTION.delete: {
|
||||
return toasts.filter((toast) => toast.id !== action.id);
|
||||
}
|
||||
case TOAST_ACTION.deleteAll: {
|
||||
return [];
|
||||
}
|
||||
default: {
|
||||
throw Error("Unknown action: " + action.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ToastProvider = ({ children }) => {
|
||||
const [toasts, dispatch] = useReducer(toastsReducer, [], () => lsReadArray(KEY));
|
||||
|
||||
useEffect(() => {
|
||||
lsSave(KEY, toasts);
|
||||
}, [toasts]);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={toasts}>
|
||||
<ToastDispatcherContext.Provider value={dispatch}>{children}</ToastDispatcherContext.Provider>
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useToasts = () => {
|
||||
return useContext(ToastContext);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const useToastsDispatch = () => {
|
||||
return useContext(ToastDispatcherContext);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./context";
|
||||
@@ -1,34 +0,0 @@
|
||||
import { ToastContainer as BSToastContainer, Toast } from "react-bootstrap";
|
||||
import { TOAST_ACTION, useToasts, useToastsDispatch } from "../../context";
|
||||
|
||||
export const ToastContainer = () => {
|
||||
const toasts = useToasts();
|
||||
const toast = useToastsDispatch();
|
||||
|
||||
const handleClose = (id) => {
|
||||
toast({
|
||||
type: TOAST_ACTION.hide,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const content = toasts
|
||||
.filter((toast) => toast.show)
|
||||
.map((toast) => {
|
||||
return (
|
||||
<Toast key={toast.id} onClose={() => handleClose(toast.id)} bg="light" delay={3000} autohide>
|
||||
<Toast.Header>
|
||||
<strong className="me-auto">{toast.title}</strong>
|
||||
<small className="text-muted">{toast.date}</small>
|
||||
</Toast.Header>
|
||||
<Toast.Body>{toast.text}</Toast.Body>
|
||||
</Toast>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<BSToastContainer position="top-end" className="p-2 pt-5">
|
||||
{content}
|
||||
</BSToastContainer>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./container";
|
||||
export * from "./menu";
|
||||
@@ -1,56 +0,0 @@
|
||||
import { TOAST_ACTION, useToasts, useToastsDispatch } from "@shared/toast/context";
|
||||
import { Dropdown } from "react-bootstrap";
|
||||
import { BellFill, BellSlashFill } from "react-bootstrap-icons";
|
||||
import "./styles.css";
|
||||
|
||||
export const ToastMenu = () => {
|
||||
const toasts = useToasts();
|
||||
const toast = useToastsDispatch();
|
||||
|
||||
const data = toasts.filter((toast) => !toast.show);
|
||||
const isEmpty = data.length === 0;
|
||||
const icon = isEmpty ? <BellSlashFill /> : <BellFill />;
|
||||
const variant = isEmpty ? "secondary" : "primary";
|
||||
|
||||
const handleDelete = (id) => {
|
||||
toast({
|
||||
type: TOAST_ACTION.delete,
|
||||
id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteAll = () => {
|
||||
toast({
|
||||
type: TOAST_ACTION.deleteAll,
|
||||
});
|
||||
};
|
||||
|
||||
const content = data.map((toast) => {
|
||||
return (
|
||||
<Dropdown.Item key={toast.id} className="toast-menu-item" onClick={() => handleDelete(toast.id)}>
|
||||
<span className="fw-bold">{toast.date}</span>
|
||||
|
||||
<span>{toast.text}</span>
|
||||
</Dropdown.Item>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle className="toast-toggle" variant={variant}>
|
||||
{icon}
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu className="toast-menu" align="end">
|
||||
{content}
|
||||
<Dropdown.Item
|
||||
disabled={isEmpty}
|
||||
className="toast-menu-item text-center text-body-tertiary"
|
||||
onClick={handleDeleteAll}
|
||||
>
|
||||
Очистить
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
.toast-toggle.dropdown-toggle::after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.toast-menu {
|
||||
width: 250px;
|
||||
max-height: 400px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.toast-menu-item {
|
||||
font-size: 0.8rem !important;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
:root {
|
||||
--my-navbar-color: #3c3c3c;
|
||||
--my-footer-color: color-mix(in srgb, var(--my-navbar-color), transparent 50%);
|
||||
/* так тоже можно */
|
||||
/* --my-footer-color: rgba(from var(--my-navbar-color) r g b / 0.5); */
|
||||
}
|
||||
|
||||
.row,
|
||||
form {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.bi {
|
||||
vertical-align: -0.125em;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Carousel, Image } from "react-bootstrap";
|
||||
import "./styles.css";
|
||||
|
||||
export const Banner = ({ className }) => {
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const handleSelect = (selectedIndex) => {
|
||||
console.debug("banner loop");
|
||||
setIndex(selectedIndex);
|
||||
};
|
||||
|
||||
return (
|
||||
<Carousel activeIndex={index} onSelect={handleSelect} className={className}>
|
||||
<Carousel.Item>
|
||||
<Image src="/images/banner1.png" className="d-block w-100" />
|
||||
<Carousel.Caption>
|
||||
<h5>65 лет УлГТУ!</h5>
|
||||
<p>Какой-то текст, который должен быть показан.</p>
|
||||
</Carousel.Caption>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Image src="/images/banner2.png" className="d-block w-100" />
|
||||
<Carousel.Caption>
|
||||
<h5>История УлГТУ</h5>
|
||||
<p>Какой-то текст, который должен быть показан.</p>
|
||||
</Carousel.Caption>
|
||||
</Carousel.Item>
|
||||
<Carousel.Item>
|
||||
<Image src="/images/banner3.png" className="d-block w-100" />
|
||||
<Carousel.Caption>
|
||||
<h5>Подготовка к экзаменам</h5>
|
||||
<p>Какой-то текст, который должен быть показан.</p>
|
||||
</Carousel.Caption>
|
||||
</Carousel.Item>
|
||||
</Carousel>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
.carousel-caption {
|
||||
background-color: var(--my-footer-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.carousel-control-next,
|
||||
.carousel-control-prev {
|
||||
filter: invert(80%);
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { useBSForm } from "@shared/index";
|
||||
import { useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
|
||||
const initState = {
|
||||
a: 2,
|
||||
b: 2,
|
||||
c: 0,
|
||||
operator: "3",
|
||||
};
|
||||
|
||||
const getButtonText = (operator) => {
|
||||
const operators = ["+", "-", "*", "/"];
|
||||
return `a ${operators[operator - 1]} b = c`;
|
||||
};
|
||||
|
||||
const getResult = (operator, aVal, bVal) => {
|
||||
const a = parseInt(aVal);
|
||||
const b = parseInt(bVal);
|
||||
return ((op) => {
|
||||
switch (op) {
|
||||
case "1":
|
||||
return a + b;
|
||||
case "2":
|
||||
return a - b;
|
||||
case "3":
|
||||
return a * b;
|
||||
case "4":
|
||||
return b === 0 ? 0 : a / b;
|
||||
default:
|
||||
throw new Error(`Unknown operator ${op}`);
|
||||
}
|
||||
})(operator);
|
||||
};
|
||||
|
||||
export const Calc = () => {
|
||||
const { register, validated, handleSubmit, setValue } = useBSForm(initState, false);
|
||||
const [operator, setOperator] = useState(initState.operator);
|
||||
|
||||
const buttonText = getButtonText(operator);
|
||||
|
||||
const onSubmit = (values) => {
|
||||
const result = getResult(values.operator, values.a, values.b);
|
||||
setValue("c", result);
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
setOperator(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<Form
|
||||
className="col col-md-6"
|
||||
noValidate
|
||||
validated={validated}
|
||||
onSubmit={(event) => handleSubmit(event, onSubmit)}
|
||||
>
|
||||
<Form.Group className="mb-2" controlId="a">
|
||||
<Form.Label>a</Form.Label>
|
||||
<Form.Control type="number" required {...register("a")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="b">
|
||||
<Form.Label>b</Form.Label>
|
||||
<Form.Control type="number" required {...register("b")} />
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="operator">
|
||||
<Form.Label>Операция</Form.Label>
|
||||
<Form.Select required {...register("operator", { onChange: handleChange })}>
|
||||
<option value="1">+</option>
|
||||
<option value="2">-</option>
|
||||
<option value="3">*</option>
|
||||
<option value="4">/</option>
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-2" controlId="с">
|
||||
<Form.Label>c</Form.Label>
|
||||
<Form.Control type="number" readOnly {...register("c")} />
|
||||
</Form.Group>
|
||||
<Button className="d-block m-auto" type="submit">
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import "./styles.css";
|
||||
|
||||
export const Footer = () => {
|
||||
return (
|
||||
<footer className="d-flex flex-shrink-0 align-items-center justify-content-center">
|
||||
Автор, {new Date().getFullYear()}
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
footer {
|
||||
background-color: var(--my-footer-color);
|
||||
height: 48px;
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { ToastMenu } from "@shared/toast/ui";
|
||||
import { Container, Nav, Navbar, NavbarBrand } from "react-bootstrap";
|
||||
import { Backpack3 } from "react-bootstrap-icons";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import "./styles.css";
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<header>
|
||||
<Navbar expand="md" variant="dark">
|
||||
<Container fluid>
|
||||
<NavbarBrand href="/">
|
||||
<Backpack3 />
|
||||
Мой сайт
|
||||
</NavbarBrand>
|
||||
<Navbar.Toggle aria-controls="navbar-nav" />
|
||||
<Navbar.Collapse id="navbar-nav" className="justify-content-end">
|
||||
<Nav>
|
||||
<Nav.Link to="/" as={NavLink}>
|
||||
Главная страница
|
||||
</Nav.Link>
|
||||
<Nav.Link to="/page2" as={NavLink}>
|
||||
Вторая страница
|
||||
</Nav.Link>
|
||||
<Nav.Link to="/page3" as={NavLink}>
|
||||
Третья страница
|
||||
</Nav.Link>
|
||||
<Nav.Link to="/page4" as={NavLink}>
|
||||
Четвертая страница
|
||||
</Nav.Link>
|
||||
<Nav.Link to="/page5" as={NavLink}>
|
||||
Пятая страница
|
||||
</Nav.Link>
|
||||
</Nav>
|
||||
<ToastMenu />
|
||||
</Navbar.Collapse>
|
||||
</Container>
|
||||
</Navbar>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
header nav {
|
||||
background-color: var(--my-navbar-color);
|
||||
}
|
||||
|
||||
header nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export * from "./banner";
|
||||
export * from "./calc";
|
||||
export * from "./footer";
|
||||
export * from "./header";
|
||||
export * from "./pagination";
|
||||
export * from "./paragraphs";
|
||||
export * from "./separator";
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Pagination as BSPagination } from "react-bootstrap";
|
||||
|
||||
export const Pagination = ({ page, total, onChange }) => {
|
||||
const items = [...Array(total || 0).keys()].map((item) => {
|
||||
const index = item + 1;
|
||||
const isActive = index === page;
|
||||
const handler = isActive ? null : () => onChange(index);
|
||||
return (
|
||||
<BSPagination.Item key={index} active={isActive} onClick={handler}>
|
||||
{index}
|
||||
</BSPagination.Item>
|
||||
);
|
||||
});
|
||||
|
||||
return <BSPagination className="justify-content-center">{items}</BSPagination>;
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import "./styles.css";
|
||||
|
||||
const getParagraphStyle = (isInverse, index) => {
|
||||
if (!isInverse) {
|
||||
return index % 2 === 0 ? "even" : "odd";
|
||||
} else {
|
||||
return index % 2 === 0 ? "odd" : "even";
|
||||
}
|
||||
};
|
||||
|
||||
export const Paragraphs = () => {
|
||||
const [data, setData] = useState(null);
|
||||
const [inverse, setInverse] = useState(false);
|
||||
|
||||
const handleParagraphClick = (index) => {
|
||||
console.debug(index);
|
||||
};
|
||||
|
||||
const handleClick = (handleClick = null) => {
|
||||
setData({ count: 10, handler: handleClick });
|
||||
};
|
||||
|
||||
const handleClick1 = () => {
|
||||
handleClick();
|
||||
};
|
||||
|
||||
const handleClick2 = () => {
|
||||
handleClick(handleParagraphClick);
|
||||
};
|
||||
|
||||
const handleClickInverse = () => {
|
||||
setInverse(!inverse);
|
||||
};
|
||||
|
||||
const paragraphs = [...Array(data?.count ?? 0).keys()].map((item) => {
|
||||
const index = item + 1;
|
||||
const cursor = data.handler !== null ? "myp" : "";
|
||||
const style = getParagraphStyle(inverse, index);
|
||||
const classes = [cursor, style].join(" ");
|
||||
const handleClick = data.handler ? () => data.handler(index) : null;
|
||||
return (
|
||||
<p key={index} className={classes} onClick={handleClick}>
|
||||
Paragraph #{index}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="row row-cols-1 justify-content-center">
|
||||
<div className="col col-md-6 col-lg-4 col-xl-2 m-lg-0 mb-2">
|
||||
<Button className="d-block m-auto w-100" onClick={handleClick1}>
|
||||
Создать 1
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col col-md-6 col-lg-4 col-xl-2 m-lg-0 mb-2">
|
||||
<Button className="d-block m-auto w-100" onClick={handleClick2}>
|
||||
Создать 2
|
||||
</Button>
|
||||
</div>
|
||||
<div className="col col-md-6 col-lg-4 col-xl-2 m-lg-0 mb-2">
|
||||
<Button id="inverse" className="d-block m-auto w-100" onClick={handleClickInverse}>
|
||||
Инвертировать
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row justify-content-center mt-2">
|
||||
<div className="col col-md-6">{paragraphs}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
.myp {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.odd {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.even {
|
||||
background-color: gray;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const Separator = () => {
|
||||
return <hr className="w-75 m-auto my-2" />;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const __dirname = path.resolve();
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@entities": path.resolve(__dirname, "./src/entities"),
|
||||
"@pages": path.resolve(__dirname, "./src/pages"),
|
||||
"@shared": path.resolve(__dirname, "./src/shared"),
|
||||
"@widgets": path.resolve(__dirname, "./src/widgets"),
|
||||
},
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user