2 Commits

Author SHA1 Message Date
maxim
ab929504e1 сдал 2025-11-15 09:29:01 +04:00
nezui1
8ccd325ba9 с отчетами 2025-11-15 00:53:15 +04:00
174 changed files with 1184 additions and 9604 deletions

BIN
LabWork1Report.docx Normal file

Binary file not shown.

BIN
LabWork2Report.docx Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -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), а не через методы сущности.

View File

@@ -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. Запустите тесты для проверки функциональности

View File

@@ -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'
}

Binary file not shown.

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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);
}
}
}

View File

@@ -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() {

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -1,8 +0,0 @@
package com.example.demo.entity.projection;
public interface ArtistStatsProjection {
Long getTotalArtists();
Long getArtistsWithDescription();
Long getArtistsWithoutDescription();
}

View File

@@ -1,9 +0,0 @@
package com.example.demo.entity.projection;
import com.example.demo.entity.CountryEntity;
public interface CountryStatsProjection {
CountryEntity getCountry();
Long getArtistsCount();
}

View File

@@ -1,9 +0,0 @@
package com.example.demo.entity.projection;
import com.example.demo.entity.EpochEntity;
public interface EpochStatsProjection {
EpochEntity getEpoch();
Long getArtistsCount();
}

View File

@@ -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> {
}

View File

@@ -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();
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -1,2 +0,0 @@
spring.datasource.url=jdbc:h2:mem:testdb

View File

@@ -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>

View File

@@ -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

Binary file not shown.

View File

@@ -1,5 +0,0 @@
Swagger UI URL:
http://localhost:8080/swagger-ui/index.html
MVN Repository:
https://mvnrepository.com/

View File

@@ -1,17 +0,0 @@
Установка зависимостей
```
npm install
```
Запуск в режиме разработки
```
npm start
```
Запуск для использования в продуктовой среде
```
npm run prod
```

View File

@@ -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"],
},
},
];

View File

@@ -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>

View File

@@ -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/*"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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>
);
};

View File

@@ -1 +0,0 @@
export * from "./main-layout";

View File

@@ -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 />
</>
);
};

View File

@@ -1,3 +0,0 @@
export * from "./student-group-hook";
export * from "./student-hook";
export * from "./students-hook";

View File

@@ -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;
};

View File

@@ -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 };
};

View File

@@ -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 };
};

View File

@@ -1,4 +0,0 @@
/* eslint-disable import/export */
export * from "./hooks";
export * from "./model";
export * from "./ui";

View File

@@ -1,7 +0,0 @@
export const studentModel = {
last_name: ["Фамилия", "text"],
first_name: ["Имя", "text"],
email: ["Почта", "email"],
phone: ["Телефон", "tel"],
bdate: ["Дата рождения", "date"],
};

View File

@@ -1,3 +0,0 @@
export * from "./students";
export * from "./students-form";
export * from "./students-table";

View File

@@ -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>
);
};

View File

@@ -1,3 +0,0 @@
.image-preview {
width: 200px;
}

View File

@@ -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>
);
};

View File

@@ -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} />
</>
);
};

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -1,5 +0,0 @@
import { Students } from "@entities/student";
export const FifthPage = () => {
return <Students />;
};

View File

@@ -1,11 +0,0 @@
import { Calc, Paragraphs, Separator } from "@widgets/index";
export const FourthPage = () => {
return (
<>
<Calc />
<Separator />
<Paragraphs />
</>
);
};

View File

@@ -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";

View File

@@ -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>
</>
);
};

View File

@@ -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;
}
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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);
});
};

View 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");

View File

@@ -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 };
};

View File

@@ -1,4 +0,0 @@
export * from "./base64";
export * from "./client";
export * from "./form";
export * from "./storage";

View File

@@ -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);
};

View File

@@ -1,2 +0,0 @@
export * from "./context";
export * from "./ui";

View File

@@ -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>
);
};

View File

@@ -1,5 +0,0 @@
export const lsSave = (key, value) => {
localStorage.setItem(key, JSON.stringify(value));
};
export const lsReadArray = (key) => JSON.parse(localStorage.getItem(key)) || [];

View File

@@ -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);
};

View File

@@ -1 +0,0 @@
export * from "./context";

View File

@@ -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>
);
};

View File

@@ -1,2 +0,0 @@
export * from "./container";
export * from "./menu";

View File

@@ -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>
&nbsp;
<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>
);
};

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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>
);
};

View File

@@ -1,9 +0,0 @@
.carousel-caption {
background-color: var(--my-footer-color);
border-radius: 5px;
}
.carousel-control-next,
.carousel-control-prev {
filter: invert(80%);
}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -1,5 +0,0 @@
footer {
background-color: var(--my-footer-color);
height: 48px;
color: #ffffff;
}

View File

@@ -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>
);
};

View File

@@ -1,7 +0,0 @@
header nav {
background-color: var(--my-navbar-color);
}
header nav a:hover {
text-decoration: underline;
}

View File

@@ -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";

View File

@@ -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>;
};

View File

@@ -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>
</>
);
};

View File

@@ -1,11 +0,0 @@
.myp {
cursor: pointer;
}
.odd {
background-color: white;
}
.even {
background-color: gray;
}

View File

@@ -1,3 +0,0 @@
export const Separator = () => {
return <hr className="w-75 m-auto my-2" />;
};

View File

@@ -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