17 Commits

Author SHA1 Message Date
maxim
cba4c19f9f сдал 2025-11-29 09:49:04 +04:00
maxim
ba9e0a908b перенес 2025-11-29 04:50:52 +04:00
nezui1
91de0ee0c0 lab 2025-11-29 04:18:20 +04:00
nezui1
3960c0d557 4(1) 2025-11-29 03:44:48 +04:00
nezui1
ea8b6591c2 4 2025-11-28 18:29:29 +04:00
nezui1
14ae167d61 с отчетом(доделать) 2025-11-15 01:12:45 +04:00
nezui1
9dddb11baf четвертая лаба 2025-11-15 00:26:47 +04:00
maxim
2c210e6657 третья лаба 2025-11-14 20:46:33 +04:00
maxim
c7dc979193 слава богу она сделалась 2025-11-12 18:29:47 +04:00
maxim
17014bcb67 первая лаба 2025-09-19 18:00:01 +04:00
maxim
4a409b63d7 первая лаба(2) 2025-09-05 22:29:19 +04:00
nezui1
12007bd3ea первая лаба 2025-09-05 21:46:02 +04:00
maxim
743b7c1c6a отчет 2025-05-28 22:01:52 +04:00
maxim
499b51d2e1 лаба + отчет 2025-05-28 20:58:31 +04:00
maxim
51875bcc5e лаба + отчет 2025-05-28 20:22:51 +04:00
maxim
51545137be лаба + отчет 2025-05-28 19:45:47 +04:00
maxim
0dd5c03d7d вторая лаба + отчет 2025-05-28 19:30:55 +04:00
296 changed files with 34022 additions and 316 deletions

14
.gitignore vendored
View File

@@ -1,14 +0,0 @@
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

9
.idea/internetDev.iml generated Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/internetDev.iml" filepath="$PROJECT_DIR$/.idea/internetDev.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

Binary file not shown.

BIN
LabWork4Report.docx Normal file

Binary file not shown.

View File

@@ -1,2 +0,0 @@
# InternetDev

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<link rel="stylesheet" href="styles.css">
<head>
<title>Каталог</title>
</head>
<body>
<h3>Тут разбита музыка на жанры</h3>
<p>Инфа будет +- такая</p>
<div class="list">
<ul class="punk-list">
<li>
<div class="item">
<img class="catalog" src="pankrock.jpg" alt="Панк-Рок" width=200>
<a href="punkrock.html">Панк-Рок</a>
</div>
</li>
<li>
<div class="item">
<img class="catalog" src="psy.png" alt="Психоделический рок" width=200>
<a href="">Психоделика</a>
</div>
</li>
<li>
<div class="item">
<img class="catalog" src="garajnipunk.jpg" alt="Гаражный-панк" width=200>
<a href="">Гражный-панк</a>
</div>
</li>
</ul>
</div>
<a href="index.html"> Вернуться назад</a>
</body>
</html>

3
demo/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary

37
demo/.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/

0
demo/Configure Normal file
View File

0
demo/Frontend Normal file
View File

1
demo/Get Normal file
View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> <20><><EFBFBD><E0A0AC><EFBFBD>: //help.gradle.org.

273
demo/README.md Normal file
View File

@@ -0,0 +1,273 @@
# Инструкция по переносу проекта на новый компьютер
## Быстрый старт
### Что работает сразу (без настройки)
1. **Dev профиль** (H2 база данных) — работает сразу без установки PostgreSQL
- Данные хранятся в файле `data.mv.db` (уже есть в проекте)
- Не требует установки отдельного сервера БД
2. **Java 21** — должна быть установлена
### Что нужно настроить (опционально)
- **PostgreSQL** — только если нужно запускать с профилем `prod`
- **Node.js** — только если нужно собирать фронтенд
---
## Пошаговая инструкция
### Шаг 1: Проверка Java
Убедитесь, что установлена Java 21:
```bash
java -version
```
Должно быть что-то вроде:
```
openjdk version "21.x.x"
```
**Если Java нет:**
- Скачайте с [Oracle JDK](https://www.oracle.com/java/technologies/downloads/#java21) или [OpenJDK](https://adoptium.net/)
- Установите и добавьте в PATH
---
### Шаг 2: Запуск проекта (Dev профиль - H2)
Проект использует Gradle Wrapper, поэтому Gradle не нужно устанавливать отдельно.
**Windows:**
```bash
cd demo
.\gradlew.bat bootRun
```
**Linux/Mac:**
```bash
cd demo
./gradlew bootRun
```
Профиль `dev` используется по умолчанию. Приложение запустится на порту 8080.
**Проверка:**
- Откройте: http://localhost:8080/swagger-ui.html
- Или: http://localhost:8080/api-docs
---
### Шаг 3: Настройка PostgreSQL (только для prod профиля)
Если нужно запускать с профилем `prod`, установите PostgreSQL:
#### Установка PostgreSQL (Windows)
1. Скачайте PostgreSQL: https://www.postgresql.org/download/windows/
2. Установите PostgreSQL (запомните пароль для пользователя `postgres`)
3. Запустите pgAdmin или используйте командную строку
#### Создание базы данных
Откройте psql или pgAdmin и выполните:
```sql
CREATE DATABASE demo;
```
Или через командную строку:
```bash
psql -U postgres
CREATE DATABASE demo;
\q
```
#### Проверка настроек
В файле `src/main/resources/application-prod.yml` должны быть настройки:
```yaml
spring:
datasource:
url: jdbc:postgresql://127.0.0.1/demo
username: postgres
password: postgres # ← Укажите ваш пароль PostgreSQL
```
**⚠️ Важно:** Если пароль PostgreSQL отличается от `postgres`, измените его в файле!
#### Запуск с prod профилем
**Windows:**
```bash
.\gradlew.bat bootRun -Pprod
```
**Linux/Mac:**
```bash
./gradlew bootRun -Pprod
```
---
### Шаг 4: Миграции базы данных
Проект использует Liquibase для миграций. Миграции запускаются автоматически при старте приложения.
**Файлы миграций находятся в:**
- `src/main/resources/db/changes/`
При первом запуске Liquibase:
- Создаст все таблицы
- Заполнит начальные данные (эпохи, страны)
**Если нужно сбросить БД и начать заново:**
- Для H2: удалите файл `data.mv.db` и перезапустите приложение
- Для PostgreSQL: удалите и создайте базу заново
---
## Полезные команды
### Запуск приложения
```bash
# Dev профиль (H2) - по умолчанию
.\gradlew.bat bootRun
# Prod профиль (PostgreSQL)
.\gradlew.bat bootRun -Pprod
# С фронтендом
.\gradlew.bat bootRun -Pfront
```
### Сборка проекта
```bash
# Собрать JAR файл
.\gradlew.bat bootJar
# Очистить и собрать
.\gradlew.bat clean build
```
### Запуск тестов
```bash
# Все тесты (dev профиль)
.\gradlew.bat test
# Тесты с prod профилем
.\gradlew.bat test -Pprod
```
### Просмотр H2 базы данных (dev профиль)
После запуска приложения с dev профилем:
1. Откройте: http://localhost:8080/h2-console
2. JDBC URL: `jdbc:h2:file:./data`
3. Username: `sa`
4. Password: `sa`
5. Нажмите "Connect"
---
## Структура проекта
```
demo/
├── src/
│ ├── main/
│ │ ├── java/ # Java код
│ │ └── resources/
│ │ ├── application.yml # Основная конфигурация
│ │ ├── application-dev.yml # Dev профиль (H2)
│ │ ├── application-prod.yml # Prod профиль (PostgreSQL)
│ │ └── db/
│ │ ├── master.yml # Liquibase master файл
│ │ └── changes/ # Файлы миграций
│ └── test/ # Тесты
├── data.mv.db # H2 база данных (dev)
├── build.gradle # Конфигурация Gradle
├── gradlew # Gradle Wrapper (Linux/Mac)
└── gradlew.bat # Gradle Wrapper (Windows)
```
---
## Частые проблемы
### Ошибка: "Could not find or load main class"
**Решение:** Выполните:
```bash
.\gradlew.bat clean build
```
### Ошибка подключения к PostgreSQL
**Проверьте:**
1. PostgreSQL запущен
2. База данных `demo` создана
3. Пароль в `application-prod.yml` правильный
4. Порт 5432 не заблокирован
**Проверка подключения:**
```bash
psql -U postgres -d demo
```
### Порт 8080 уже занят
Измените порт в `src/main/resources/application.yml`:
```yaml
server:
port: 8081 # или другой свободный порт
```
### Ошибка миграций Liquibase
**Для dev профиля (H2):**
- Удалите файл `data.mv.db` и перезапустите
**Для prod профиля (PostgreSQL):**
- Удалите базу данных и создайте заново
- Или проверьте логи для конкретной ошибки
---
## Что перенеслось автоматически
✅ Код приложения
✅ Конфигурация Gradle
✅ Миграции Liquibase
✅ Данные H2 (файл `data.mv.db`)
✅ Статические файлы фронтенда
❌ PostgreSQL база данных (нужно установить и настроить отдельно)
---
## Контакты и документация
- Swagger UI: http://localhost:8080/swagger-ui.html
- API Docs: http://localhost:8080/api-docs
- H2 Console (dev): http://localhost:8080/h2-console
---
## Следующие шаги
1. ✅ Убедитесь, что Java 21 установлена
2. ✅ Запустите проект с dev профилем
3. ✅ Проверьте Swagger UI
4. ⏭️ (Опционально) Установите PostgreSQL для prod профиля
5. ⏭️ (Опционально) Настройте фронтенд, если нужно
Если что-то не работает, проверьте логи в консоли или файле логов.

0
demo/Run Normal file
View File

50
demo/build.front.gradle Normal file
View File

@@ -0,0 +1,50 @@
apply plugin: "com.github.node-gradle.node"
logger.quiet("Configure front builder")
ext {
frontDir = file("${project.projectDir}/../punkrock-react")
staticDir = file("${project.projectDir}/src/main/resources/static")
if (!frontDir.exists()) {
throw new GradleException("Frontend app directory is not exists")
}
logger.quiet("Webapp dir is {}", frontDir.toString())
}
node {
version = "22.17.1"
npmVersion = "10.9.2"
download = true
}
tasks.register("frontDepsInstall", NpmTask) {
group = "front"
description = "Installs dependencies from package.json"
logger.quiet(description)
workingDir = frontDir
args = ["install"]
}
tasks.register("frontBuild", NpmTask) {
group = "front"
description = "Build frontend webapp"
logger.quiet(description)
workingDir = frontDir
dependsOn frontDepsInstall
args = ["run", "build"]
}
tasks.register("copyFrontend", org.gradle.api.tasks.Copy) {
group = "front"
description = "Copy built frontend to static resources"
dependsOn frontBuild
from("${frontDir}/build")
into(staticDir)
includeEmptyDirs = false
}
if (frontDir.exists()) {
processResources.dependsOn copyFrontend
bootJar.dependsOn copyFrontend
}

91
demo/build.gradle Normal file
View File

@@ -0,0 +1,91 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.5'
id 'com.github.node-gradle.node' version '7.1.0' apply false
id 'org.liquibase.gradle' version '2.2.2' apply false
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
ext {
springdocVersion = "2.2.0"
h2Version = "2.4.240"
postgresVersion = "42.7.8"
liquibaseVersion = "4.33.0"
springProfiles = []
if (project.hasProperty("front")) {
springProfiles.add("front")
}
if (project.hasProperty("prod")) {
springProfiles.add("prod")
} else {
springProfiles.add("dev")
}
currentProfiles = springProfiles.join(",")
logger.quiet("Current profiles are: " + currentProfiles)
}
if (springProfiles.contains("front")) {
apply from: "build.front.gradle"
}
if (springProfiles.contains("dev")) {
apply from: "build.migrations.gradle"
}
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:${springdocVersion}"
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly "org.liquibase:liquibase-core:${liquibaseVersion}"
if (springProfiles.contains("prod")) {
runtimeOnly "org.postgresql:postgresql:${postgresVersion}"
} else {
runtimeOnly "org.postgresql:postgresql:${postgresVersion}"
runtimeOnly "com.h2database:h2:${h2Version}"
}
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
bootRun {
def currentArgs = ["--spring.profiles.active=" + currentProfiles]
if (project.hasProperty("args")) {
currentArgs.addAll(project.args.split(","))
}
args currentArgs
}
bootJar {
archiveFileName = String.format("%s-%s.jar", rootProject.name, version)
}
test {
systemProperty "spring.profiles.active", currentProfiles
useJUnitPlatform()
}
processResources {
filesMatching("**/application.yml") {
filter { line ->
line.replace("active: dev", "active: ${currentProfiles}")
}
}
}

View File

@@ -0,0 +1,67 @@
apply plugin: "org.liquibase.gradle"
logger.quiet("Configure migrations generator")
ext {
picocliVersion = "4.7.7"
timestamp = new Date().format("yyyy-MM-dd-HHmmss")
}
liquibase {
activities {
main {
changelogFile "db/master.yml"
url "jdbc:h2:file:./data"
username "sa"
password "sa"
referenceUrl "hibernate:spring:com.example.demo.entity?dialect=org.hibernate.dialect.H2Dialect"
logLevel "warn"
}
}
}
update.dependsOn processResources
dependencies {
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate6:${liquibaseVersion}"
liquibaseRuntime "info.picocli:picocli:${picocliVersion}"
liquibaseRuntime sourceSets.main.runtimeClasspath
liquibaseRuntime sourceSets.main.output
}
tasks.register("generateFull") {
group = "migrations"
description = "Generate changelog from existing database"
doFirst(){
liquibase {
activities {
main {
changeLogFile "src/main/resources/db/generated-full-${timestamp}.yml"
}
}
}
}
finalizedBy generateChangelog
}
tasks.register("generateDiff") {
group = "liquibase"
description = "Generate diff between DB and JPA entities"
doFirst(){
liquibase {
activities {
main {
changeLogFile "src/main/resources/db/generated-diff-${timestamp}.yml"
}
}
}
}
finalizedBy diffChangelog
}
diffChangelog.dependsOn compileJava
tasks.register("clearCheckSums") {
group = "migrations"
description = "Clear checksums in database (use when changeset was modified after being applied)"
finalizedBy clearChecksums
}

BIN
demo/data.mv.db Normal file

Binary file not shown.

8
demo/data.trace.db Normal file
View File

@@ -0,0 +1,8 @@
2025-11-29 03:33:06.457278+04:00 jdbc[3]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Таблица "DATABASECHANGELOGLOCK" не найдена
Table "DATABASECHANGELOGLOCK" not found; SQL statement:
SELECT COUNT(*) FROM PUBLIC.DATABASECHANGELOGLOCK [42102-240]
2025-11-29 09:45:14.491494+04:00 jdbc[13]: exception
org.h2.jdbc.JdbcSQLSyntaxErrorException: Синтаксическая ошибка в выражении SQL "SELECT * FROM DATABASECHANGELOG ARTISTS [*]DATABASECHANGELOG"
Syntax error in SQL statement "SELECT * FROM DATABASECHANGELOG ARTISTS [*]DATABASECHANGELOG"; SQL statement:
SELECT * FROM DATABASECHANGELOG ARTISTS DATABASECHANGELOG [42000-240]

BIN
demo/gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

0
demo/gradlew vendored Normal file
View File

94
demo/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

6
demo/package-lock.json generated Normal file
View File

@@ -0,0 +1,6 @@
{
"name": "demo",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

1
demo/settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'demo'

View File

@@ -0,0 +1,13 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,15 @@
package com.example.demo.api;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class PageHelper {
private PageHelper() {
}
public static Pageable toPageable(int page, int size) {
return PageRequest.of(page - 1, size, Sort.by("id"));
}
}

View File

@@ -0,0 +1,40 @@
package com.example.demo.api;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.data.domain.Page;
public record PageRs<D>(
List<D> items,
int itemsCount,
int currentPage,
int currentSize,
int totalPages,
long totalItems,
boolean isFirst,
boolean isLast,
boolean hasNext,
boolean hasPrevious) {
public List<D> items() {
return Optional.ofNullable(items).orElse(Collections.emptyList());
}
public static <D, E> PageRs<D> from(Page<E> page, Function<E, D> mapper) {
return new PageRs<>(
page.getContent().stream().map(mapper::apply).toList(),
page.getNumberOfElements(),
page.getNumber() + 1,
page.getSize(),
page.getTotalPages(),
page.getTotalElements(),
page.isFirst(),
page.isLast(),
page.hasNext(),
page.hasPrevious());
}
}

View File

@@ -0,0 +1,23 @@
package com.example.demo.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.Contact;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Punk Rock API")
.version("1.0.0")
.description("REST API для управления данными о панк-рок исполнителях")
.contact(new Contact()
.name("Developer")
.email("developer@example.com")));
}
}

View File

@@ -0,0 +1,10 @@
package com.example.demo.configuration;
public class Constants {
public static final String DEV_ORIGIN = "http://localhost:5173";
public static final String API_URL = "/api";
private Constants() {
}
}

View File

@@ -0,0 +1,20 @@
package com.example.demo.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
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)
.allowedHeaders("*")
.allowCredentials(true);
}
}

View File

@@ -0,0 +1,58 @@
package com.example.demo.controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import com.example.demo.api.PageHelper;
import com.example.demo.api.PageRs;
import com.example.demo.configuration.Constants;
import com.example.demo.dto.ArtistRq;
import com.example.demo.dto.ArtistRs;
import com.example.demo.service.ArtistService;
@RestController
@RequestMapping(Constants.API_URL + ArtistController.URL)
public class ArtistController {
public static final String URL = "/artists";
private final ArtistService artistService;
public ArtistController(ArtistService artistService) {
this.artistService = artistService;
}
@GetMapping
public PageRs<ArtistRs> getAll(
@RequestParam(defaultValue = "1") @Min(1) int page,
@RequestParam(defaultValue = "3") @Min(1) int size) {
return artistService.getAll(PageHelper.toPageable(page, size));
}
@GetMapping("/{id}")
public ArtistRs get(@PathVariable Long id) {
return artistService.get(id);
}
@PostMapping
public ArtistRs create(@RequestBody @Valid ArtistRq dto) {
return artistService.create(dto);
}
@PutMapping("/{id}")
public ArtistRs update(@PathVariable Long id, @RequestBody @Valid ArtistRq dto) {
return artistService.update(id, dto);
}
@DeleteMapping("/{id}")
public ArtistRs delete(@PathVariable Long id) {
return artistService.delete(id);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.demo.controller;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import com.example.demo.configuration.Constants;
import com.example.demo.dto.CountryRq;
import com.example.demo.dto.CountryRs;
import com.example.demo.service.CountryService;
@RestController
@RequestMapping(Constants.API_URL + CountryController.URL)
public class CountryController {
public static final String URL = "/countries";
private final CountryService countryService;
public CountryController(CountryService countryService) {
this.countryService = countryService;
}
@GetMapping
public List<CountryRs> getAll() {
return countryService.getAll();
}
@GetMapping("/{id}")
public CountryRs get(@PathVariable Long id) {
return countryService.get(id);
}
@PostMapping
public CountryRs create(@RequestBody @Valid CountryRq dto) {
return countryService.create(dto);
}
@PutMapping("/{id}")
public CountryRs update(@PathVariable Long id, @RequestBody @Valid CountryRq dto) {
return countryService.update(id, dto);
}
@DeleteMapping("/{id}")
public CountryRs delete(@PathVariable Long id) {
return countryService.delete(id);
}
}

View File

@@ -0,0 +1,54 @@
package com.example.demo.controller;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid;
import com.example.demo.configuration.Constants;
import com.example.demo.dto.EpochRq;
import com.example.demo.dto.EpochRs;
import com.example.demo.service.EpochService;
@RestController
@RequestMapping(Constants.API_URL + EpochController.URL)
public class EpochController {
public static final String URL = "/epochs";
private final EpochService epochService;
public EpochController(EpochService epochService) {
this.epochService = epochService;
}
@GetMapping
public List<EpochRs> getAll() {
return epochService.getAll();
}
@GetMapping("/{id}")
public EpochRs get(@PathVariable Long id) {
return epochService.get(id);
}
@PostMapping
public EpochRs create(@RequestBody @Valid EpochRq dto) {
return epochService.create(dto);
}
@PutMapping("/{id}")
public EpochRs update(@PathVariable Long id, @RequestBody @Valid EpochRq dto) {
return epochService.update(id, dto);
}
@DeleteMapping("/{id}")
public EpochRs delete(@PathVariable Long id) {
return epochService.delete(id);
}
}

View File

@@ -0,0 +1,14 @@
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class SpaController {
@GetMapping(value = "/{path:^(?!api|assets|images|swagger-ui|.*\\.[a-zA-Z0-9]{2,10}).*}/**")
public String forwardToIndex(@PathVariable(required = false) String path) {
return "forward:/index.html";
}
}

View File

@@ -0,0 +1,59 @@
package com.example.demo.dto;
public class ArtistDto {
private Integer id;
private String name;
private String description;
private Integer epochId;
private Integer countryId;
public ArtistDto() {}
public ArtistDto(Integer id, String name, String description, Integer epochId, Integer countryId) {
this.id = id;
this.name = name;
this.description = description;
this.epochId = epochId;
this.countryId = countryId;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getEpochId() {
return epochId;
}
public void setEpochId(Integer epochId) {
this.epochId = epochId;
}
public Integer getCountryId() {
return countryId;
}
public void setCountryId(Integer countryId) {
this.countryId = countryId;
}
}

View File

@@ -0,0 +1,47 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public class ArtistRq {
@NotBlank
private String name;
private String description;
@NotNull
private Long epochId;
@NotNull
private Long countryId;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Long getEpochId() {
return epochId;
}
public void setEpochId(Long epochId) {
this.epochId = epochId;
}
public Long getCountryId() {
return countryId;
}
public void setCountryId(Long countryId) {
this.countryId = countryId;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.demo.dto;
public class ArtistRs {
private Long id;
private String name;
private String description;
private EpochRs epoch;
private CountryRs country;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public EpochRs getEpoch() {
return epoch;
}
public void setEpoch(EpochRs epoch) {
this.epoch = epoch;
}
public CountryRs getCountry() {
return country;
}
public void setCountry(CountryRs country) {
this.country = country;
}
}

View File

@@ -0,0 +1,29 @@
package com.example.demo.dto;
public class CountryDto {
private Integer id;
private String name;
public CountryDto() {}
public CountryDto(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,17 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class CountryRq {
@NotBlank
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,23 @@
package com.example.demo.dto;
public class CountryRs {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,29 @@
package com.example.demo.dto;
public class EpochDto {
private Integer id;
private String name;
public EpochDto() {}
public EpochDto(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,17 @@
package com.example.demo.dto;
import jakarta.validation.constraints.NotBlank;
public class EpochRq {
@NotBlank
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,23 @@
package com.example.demo.dto;
public class EpochRs {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -0,0 +1,92 @@
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() {
super();
}
public ArtistEntity(String name, String description, EpochEntity epoch, CountryEntity country) {
this();
this.name = name;
this.description = description;
setEpoch(epoch);
setCountry(country);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public EpochEntity getEpoch() {
return epoch;
}
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() {
return country;
}
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

@@ -0,0 +1,27 @@
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, generator = "hibernate_sequence")
@SequenceGenerator(name = "hibernate_sequence", sequenceName = "hibernate_sequence", allocationSize = 50)
protected Long id;
protected BaseEntity() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
}

View File

@@ -0,0 +1,61 @@
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();
}
public CountryEntity(String name) {
this();
this.name = name;
}
public String getName() {
return name;
}
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

@@ -0,0 +1,61 @@
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();
}
public EpochEntity(String name) {
this();
this.name = name;
}
public String getName() {
return name;
}
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

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
package com.example.demo.error;
public class NotFoundException extends RuntimeException {
public <T> NotFoundException(Class<T> entClass, Long id) {
super(String.format("%s with id %s is not found", entClass.getSimpleName(), id));
}
}

View File

@@ -0,0 +1,51 @@
package com.example.demo.mapper;
import java.util.List;
import java.util.stream.StreamSupport;
import org.springframework.stereotype.Component;
import com.example.demo.dto.ArtistRq;
import com.example.demo.dto.ArtistRs;
import com.example.demo.entity.ArtistEntity;
@Component
public class ArtistMapper {
private final EpochMapper epochMapper;
private final CountryMapper countryMapper;
public ArtistMapper(EpochMapper epochMapper, CountryMapper countryMapper) {
this.epochMapper = epochMapper;
this.countryMapper = countryMapper;
}
public ArtistRq toRqDto(String name, String description, Long epochId, Long countryId) {
final ArtistRq dto = new ArtistRq();
dto.setName(name);
dto.setDescription(description);
dto.setEpochId(epochId);
dto.setCountryId(countryId);
return dto;
}
public ArtistRs toRsDto(ArtistEntity entity) {
final ArtistRs dto = new ArtistRs();
dto.setId(entity.getId());
dto.setName(entity.getName());
dto.setDescription(entity.getDescription());
if (entity.getEpoch() != null) {
dto.setEpoch(epochMapper.toRsDto(entity.getEpoch()));
}
if (entity.getCountry() != null) {
dto.setCountry(countryMapper.toRsDto(entity.getCountry()));
}
return dto;
}
public List<ArtistRs> toRsDtoList(Iterable<ArtistEntity> entities) {
return StreamSupport.stream(entities.spliterator(), false)
.map(this::toRsDto)
.toList();
}
}

View File

@@ -0,0 +1,33 @@
package com.example.demo.mapper;
import java.util.List;
import java.util.stream.StreamSupport;
import org.springframework.stereotype.Component;
import com.example.demo.dto.CountryRq;
import com.example.demo.dto.CountryRs;
import com.example.demo.entity.CountryEntity;
@Component
public class CountryMapper {
public CountryRq toRqDto(String name) {
final CountryRq dto = new CountryRq();
dto.setName(name);
return dto;
}
public CountryRs toRsDto(CountryEntity entity) {
final CountryRs dto = new CountryRs();
dto.setId(entity.getId());
dto.setName(entity.getName());
return dto;
}
public List<CountryRs> toRsDtoList(Iterable<CountryEntity> entities) {
return StreamSupport.stream(entities.spliterator(), false)
.map(this::toRsDto)
.toList();
}
}

View File

@@ -0,0 +1,33 @@
package com.example.demo.mapper;
import java.util.List;
import java.util.stream.StreamSupport;
import org.springframework.stereotype.Component;
import com.example.demo.dto.EpochRq;
import com.example.demo.dto.EpochRs;
import com.example.demo.entity.EpochEntity;
@Component
public class EpochMapper {
public EpochRq toRqDto(String name) {
final EpochRq dto = new EpochRq();
dto.setName(name);
return dto;
}
public EpochRs toRsDto(EpochEntity entity) {
final EpochRs dto = new EpochRs();
dto.setId(entity.getId());
dto.setName(entity.getName());
return dto;
}
public List<EpochRs> toRsDtoList(Iterable<EpochEntity> entities) {
return StreamSupport.stream(entities.spliterator(), false)
.map(this::toRsDto)
.toList();
}
}

View File

@@ -0,0 +1,39 @@
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 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);
}

View File

@@ -0,0 +1,28 @@
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 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);
}

View File

@@ -0,0 +1,28 @@
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 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);
}

View File

@@ -0,0 +1,83 @@
package com.example.demo.service;
import java.util.List;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.example.demo.api.PageRs;
import com.example.demo.dto.ArtistRq;
import com.example.demo.dto.ArtistRs;
import com.example.demo.entity.ArtistEntity;
import com.example.demo.entity.CountryEntity;
import com.example.demo.entity.EpochEntity;
import com.example.demo.error.NotFoundException;
import com.example.demo.mapper.ArtistMapper;
import com.example.demo.repository.ArtistRepository;
@Service
public class ArtistService {
private final ArtistRepository repository;
private final EpochService epochService;
private final CountryService countryService;
private final ArtistMapper mapper;
public ArtistService(ArtistRepository repository, EpochService epochService,
CountryService countryService, ArtistMapper mapper) {
this.repository = repository;
this.epochService = epochService;
this.countryService = countryService;
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 PageRs<ArtistRs> getAll(Pageable pageable) {
return PageRs.from(repository.findAll(pageable), mapper::toRsDto);
}
@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());
ArtistEntity entity = new ArtistEntity(
dto.getName(),
dto.getDescription(),
epoch,
country);
entity = repository.save(entity);
return mapper.toRsDto(entity);
}
@Transactional
public ArtistRs update(Long id, ArtistRq dto) {
ArtistEntity entity = getEntity(id);
entity.setName(dto.getName());
entity.setDescription(dto.getDescription());
entity.setEpoch(epochService.getEntity(dto.getEpochId()));
entity.setCountry(countryService.getEntity(dto.getCountryId()));
entity = repository.save(entity);
return mapper.toRsDto(entity);
}
@Transactional
public ArtistRs delete(Long id) {
final ArtistEntity entity = getEntity(id);
repository.delete(entity);
return mapper.toRsDto(entity);
}
}

View File

@@ -0,0 +1,65 @@
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;
import com.example.demo.entity.CountryEntity;
import com.example.demo.error.NotFoundException;
import com.example.demo.mapper.CountryMapper;
import com.example.demo.repository.CountryRepository;
@Service
public class CountryService {
private final CountryRepository repository;
private final CountryMapper mapper;
public CountryService(CountryRepository repository, CountryMapper mapper) {
this.repository = repository;
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());
entity = repository.save(entity);
return mapper.toRsDto(entity);
}
@Transactional
public CountryRs delete(Long id) {
final CountryEntity entity = getEntity(id);
repository.delete(entity);
return mapper.toRsDto(entity);
}
}

View File

@@ -0,0 +1,65 @@
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;
import com.example.demo.entity.EpochEntity;
import com.example.demo.error.NotFoundException;
import com.example.demo.mapper.EpochMapper;
import com.example.demo.repository.EpochRepository;
@Service
public class EpochService {
private final EpochRepository repository;
private final EpochMapper mapper;
public EpochService(EpochRepository repository, EpochMapper mapper) {
this.repository = repository;
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());
entity = repository.save(entity);
return mapper.toRsDto(entity);
}
@Transactional
public EpochRs delete(Long id) {
final EpochEntity entity = getEntity(id);
repository.delete(entity);
return mapper.toRsDto(entity);
}
}

View File

@@ -0,0 +1,20 @@
logging:
level:
com:
example:
demo: DEBUG
spring:
datasource:
url: jdbc:h2:file:./data
username: sa
password: sa
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
show-sql: false
properties:
hibernate:
format-sql: false

View File

@@ -0,0 +1,23 @@
# Available levels: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
logging:
level:
com:
example:
demo: INFO
spring:
datasource:
url: jdbc:postgresql://127.0.0.1/demo
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
jpa:
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format-sql: true
jdbc:
lob:
non_contextual_creation: true
use_jdbc_metadata_defaults: false

View File

@@ -0,0 +1,12 @@
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

@@ -0,0 +1,29 @@
spring:
main:
banner-mode: off
application:
name: demo
profiles:
active: dev
jpa:
hibernate:
ddl-auto: validate
open-in-view: false
properties:
hibernate:
jdbc:
lob:
non_contextual_creation: true
liquibase:
enabled: true
drop-first: false
change-log: classpath:db/master.yml
server:
port: 8080
springdoc:
api-docs:
path: /api-docs
swagger-ui:
path: /swagger-ui.html

View File

@@ -0,0 +1,20 @@
databaseChangeLog:
- changeSet:
id: create-sequences
author: user
preConditions:
- onFail: MARK_RAN
- not:
- sequenceExists:
sequenceName: hibernate_sequence
schemaName: public
changes:
- createSequence:
sequenceName: hibernate_sequence
schemaName: public
startValue: 1
incrementBy: 50
minValue: 1
cycle: false
dataType: bigint

View File

@@ -0,0 +1,73 @@
databaseChangeLog:
- changeSet:
id: create-tables
author: user
preConditions:
- onFail: MARK_RAN
- not:
- tableExists:
tableName: epochs
changes:
- createTable:
tableName: epochs
columns:
- column:
name: id
type: BIGINT
constraints:
primaryKey: true
nullable: false
primaryKeyName: pk_epochs
- column:
name: name
type: VARCHAR(255)
constraints:
nullable: false
- createTable:
tableName: countries
columns:
- column:
name: id
type: BIGINT
constraints:
primaryKey: true
nullable: false
primaryKeyName: pk_countries
- column:
name: name
type: VARCHAR(255)
constraints:
nullable: false
- createTable:
tableName: artists
columns:
- column:
name: id
type: BIGINT
constraints:
primaryKey: true
nullable: false
primaryKeyName: pk_artists
- column:
name: name
type: VARCHAR(255)
constraints:
nullable: false
- column:
name: description
type: TEXT
- column:
name: epoch_id
type: BIGINT
constraints:
nullable: false
foreignKeyName: fk_artists_epoch
references: epochs(id)
- column:
name: country_id
type: BIGINT
constraints:
nullable: false
foreignKeyName: fk_artists_country
references: countries(id)

View File

@@ -0,0 +1,53 @@
databaseChangeLog:
- changeSet:
id: populate-epochs
author: user
preConditions:
- onFail: MARK_RAN
- sqlCheck:
expectedResult: 0
sql: "SELECT COUNT(*) FROM epochs"
changes:
- insert:
tableName: epochs
columns:
- column:
name: id
value: 1
- column:
name: name
value: "1980-е"
- insert:
tableName: epochs
columns:
- column:
name: id
value: 2
- column:
name: name
value: "1990-е"
- insert:
tableName: epochs
columns:
- column:
name: id
value: 3
- column:
name: name
value: "2000-е"
- sql:
sql: "SELECT setval('hibernate_sequence', COALESCE((SELECT MAX(id) FROM epochs), 1) + 50)"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT COALESCE(MAX(id), 1) + 50 FROM epochs)"
dbms: h2
rollback:
- delete:
tableName: epochs
- sql:
sql: "SELECT setval('hibernate_sequence', 1)"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH 1"
dbms: h2

View File

@@ -0,0 +1,53 @@
databaseChangeLog:
- changeSet:
id: populate-countries
author: user
preConditions:
- onFail: MARK_RAN
- sqlCheck:
expectedResult: 0
sql: "SELECT COUNT(*) FROM countries"
changes:
- insert:
tableName: countries
columns:
- column:
name: id
value: 1
- column:
name: name
value: "Россия"
- insert:
tableName: countries
columns:
- column:
name: id
value: 2
- column:
name: name
value: "Омск"
- insert:
tableName: countries
columns:
- column:
name: id
value: 3
- column:
name: name
value: "Тайга"
- sql:
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)) + 50)"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM countries)"
dbms: h2
rollback:
- sql:
sql: "DELETE FROM countries WHERE id IN (1, 2, 3)"
- sql:
sql: "SELECT setval('hibernate_sequence', COALESCE((SELECT MAX(id) FROM epochs), 1))"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT COALESCE(MAX(id), 1) FROM epochs)"
dbms: h2

View File

@@ -0,0 +1,80 @@
databaseChangeLog:
- changeSet:
id: populate-artists
author: user
preConditions:
- onFail: MARK_RAN
- sqlCheck:
expectedResult: 0
sql: "SELECT COUNT(*) FROM artists"
changes:
- insert:
tableName: artists
columns:
- column:
name: id
value: 1
- column:
name: name
value: "Кино"
- column:
name: description
value: "Советская рок-группа"
- column:
name: epoch_id
value: 1
- column:
name: country_id
value: 1
- insert:
tableName: artists
columns:
- column:
name: id
value: 2
- column:
name: name
value: "Nirvana"
- column:
name: description
value: "Американская рок-группа"
- column:
name: epoch_id
value: 2
- column:
name: country_id
value: 2
- insert:
tableName: artists
columns:
- column:
name: id
value: 3
- column:
name: name
value: "The Beatles"
- column:
name: description
value: "Британская рок-группа"
- column:
name: epoch_id
value: 1
- column:
name: country_id
value: 3
- sql:
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM artists), 1), COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)) + 50)"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM countries), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM artists)"
dbms: h2
rollback:
- sql:
sql: "DELETE FROM artists WHERE id IN (1, 2, 3)"
- sql:
sql: "SELECT setval('hibernate_sequence', GREATEST(COALESCE((SELECT MAX(id) FROM countries), 1), COALESCE((SELECT MAX(id) FROM epochs), 1)))"
dbms: postgresql
- sql:
sql: "ALTER SEQUENCE hibernate_sequence RESTART WITH (SELECT GREATEST(COALESCE(MAX(id), 1), (SELECT COALESCE(MAX(id), 1) FROM epochs)) + 50 FROM countries)"
dbms: h2

View File

@@ -0,0 +1,6 @@
databaseChangeLog:
- includeAll:
path: changes/
relativeToChangelogFile: true
errorIfMissingOrEmpty: false

View File

@@ -0,0 +1,15 @@
{
"files": {
"main.css": "/punkrock/static/css/main.87b4f72f.css",
"main.js": "/punkrock/static/js/main.e5d90428.js",
"static/media/bootstrap-icons.woff?": "/punkrock/static/media/bootstrap-icons.1295669cd4e305c97f2c.woff",
"static/media/bootstrap-icons.woff2?": "/punkrock/static/media/bootstrap-icons.92ea18a81d737146ff04.woff2",
"index.html": "/punkrock/index.html",
"main.87b4f72f.css.map": "/punkrock/static/css/main.87b4f72f.css.map",
"main.e5d90428.js.map": "/punkrock/static/js/main.e5d90428.js.map"
},
"entrypoints": [
"static/css/main.87b4f72f.css",
"static/js/main.e5d90428.js"
]
}

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="src/styles.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<title>Панкуха</title>
</head>
<body class="bg-dark text-light">
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
<div class="container-fluid">
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
<span class="text-punk fs-4 fw-bold">Панкуха</span>
</a>
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse bg-black" id="navbarContent">
<ul class="navbar-nav w-100 justify-content-end pe-4">
<li class="nav-item dropdown">
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-list me-1"></i>Страницы
</a>
<ul class="dropdown-menu dropdown-menu-end bg-dark">
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
<i class="bi bi-people-fill me-1"></i>Контакты
</a>
</li>
</ul>
</div>
</div>
</header>
<main class="container-fluid my-5 flex-grow-1">
<div class="container">
<div class="card bg-dark border-punk mb-4">
<div class="card-body">
<h3 class="text-punk mb-0"><a class="text-punk" href="/punkrock">Панк-Рок</a></h3>
</div>
</div>
<div class="card bg-dark border-punk mb-4">
<div class="card-body">
<h3 class="text-punk mb-0">Психоделика</h3>
</div>
</div>
<div class="card bg-dark border-punk">
<div class="card-body">
<h3 class="text-punk mb-0">Гаражный панк</h3>
</div>
</div>
</div>
</main>
<footer class="bg-black py-3 border-top border-punk mt-auto">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
<nav class="d-flex align-items-center">
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
</nav>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/styles.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<title>Панкуха</title>
</head>
<body class="bg-dark text-light">
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
<div class="container-fluid">
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
<span class="text-punk fs-4 fw-bold">Панкуха</span>
</a>
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse bg-black" id="navbarContent">
<ul class="navbar-nav w-100 justify-content-end pe-4">
<li class="nav-item dropdown">
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-list me-1"></i>Страницы
</a>
<ul class="dropdown-menu dropdown-menu-end bg-dark">
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
<i class="bi bi-people-fill me-1"></i>Контакты
</a>
</li>
</ul>
</div>
</div>
</header>
<main class="container-fluid my-5 flex-grow-1">
<div class="container">
<div class="card bg-dark border-punk">
<div class="card-body">
<h1 class="text-punk mb-4">Гражданская Оборона</h1>
<div style="text-align: center;">
<img src="/res/grob.jpg" alt="uGrob" class="me-2">
</div>
<p class="lead text-light">
Здесь можно почитать инфу про исполнителя и перейти на песню
</p>
<div class="card bg-dark border-punk mb-4">
<div class="card-body">
<ul class="list-group list-group-flush">
<li class="list-group-item bg-dark text-light border-punk">
«Гражданская Оборона» — культовая советская и российская рок-группа, основанная в 1984 году в Омске Егором Летовым.
Коллектив стал одним из самых влиятельных в андеграундной среде.
</li>
<li class="list-group-item bg-dark text-light border-punk">
Музыка «Гражданской Обороны» сочетает в себе элементы панк-рока, гаражного рока и лоу-фая.
Несмотря на минималистичный подход к звучанию, группа смогла создать уникальный стиль.
</li>
<li class="list-group-item bg-dark text-light border-punk">
Среди самых известных альбомов группы — «Тоталитаризм», «Мышеловка», «Здорово и вечно»,
«Русское поле экспериментов» и «Инструкция по выживанию». Творчество «Гражданской Обороны»
остается актуальным и по сей день, а Егор Летов считается одной из ключевых фигур в истории
русской рок-музыки.
</li>
</ul>
</div>
</div>
<div class="card bg-dark border-punk">
<div class="card-body">
<h3 class="text-punk mb-3">Популярные песни:</h3>
<div class="list-group">
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="/grobKaifIliBolshe">
Кайф или больше
</a>
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="#">
Зоопарк
</a>
<a class="list-group-item list-group-item-action bg-dark text-punk border-punk" href="#">
Новая патриотическая
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<footer class="bg-black py-3 border-top border-punk mt-auto">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
<nav class="d-flex align-items-center">
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
</nav>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,123 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/styles.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<title>Панкуха</title>
</head>
<body class="bg-dark text-light">
<header class="sticky-top navbar navbar-expand-lg navbar-dark bg-black border-bottom border-punk px-0">
<div class="container-fluid">
<a href="/" class="navbar-brand d-flex align-items-center ms-3">
<img src="/res/logo.png" alt="Панкуха" height="60" class="me-2">
<span class="text-punk fs-4 fw-bold">Панкуха</span>
</a>
<button class="navbar-toggler me-3" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse bg-black" id="navbarContent">
<ul class="navbar-nav w-100 justify-content-end pe-4">
<li class="nav-item dropdown">
<a class="nav-link text-punk fw-bold dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-list me-1"></i>Страницы
</a>
<ul class="dropdown-menu dropdown-menu-end bg-dark">
<li><a class="dropdown-item text-punk" href="/">Главная</a></li>
<li><a class="dropdown-item text-punk" href="/grob">Исполнитель</a></li>
<li><a class="dropdown-item text-punk" href="/grobKaifIliBolshe">Песня</a></li>
<li><a class="dropdown-item text-punk" href="/catalog">Каталог</a></li>
<li><a class="dropdown-item text-punk" href="/punkrock">Список исполнителей</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link text-punk fw-bold" href="https://vk.com/kadyshevever">
<i class="bi bi-people-fill me-1"></i>Контакты
</a>
</li>
</ul>
</div>
</div>
</header>
<main class="container-fluid my-5 flex-grow-1">
<div class="container">
<div class="card bg-dark border-punk mb-4">
<div class="card-body">
<h1 class="text-punk mb-4">Кайф или больше</h1>
<h3 class="text-light mb-3">
<div style="text-align: center;">
<img src="/res/nekrofilia.jpg" alt="uGrob" class="me-2">
</div>
<div style="text-align: center;">
<a class="text-punk" href="/grob">Гражданская оборона</a>
</h3>
</div>
</div>
</div>
<div class="card bg-dark border-punk mb-4">
<div class="card-body">
<h5 class="text-punk mb-3">Описание</h5>
<ul class="list-group list-group-flush">
<li class="list-group-item bg-dark text-light border-punk">
Была выпущена в 1987 году
</li>
<li class="list-group-item bg-dark text-light border-punk">
Входит в альбом "Некрофилия"
</li>
</ul>
</div>
</div>
<div class="card bg-dark border-punk">
<div class="card-body">
<h5 class="text-punk mb-3">Текст песни:</h5>
<pre class="text-light" style="white-space: pre-wrap;">
[Куплет 1]
Рука повисла в небе, полном до краёв
Мои ошибки устилают мой позор
Я сочно благодарен, словно кошкин блёв
И смачно богомолен, словно приговор
[Припев]
Но мне придётся выбирать
Кайф или больше
Рай или больше
Свет или больше... Хей-йо
[Куплет 2]
Я буду ласковым, как тёплый банный лист
Я буду вежливым, как битое окно
Я буду благотворен, словно онанист
Я буду зазеркален, словно всё равно
[Припев]
Но мне придётся выбирать
Кайф или больше
Рай или больше
Свет или больше...хей-йо
</pre>
</div>
</div>
</div>
</main>
<footer class="bg-black py-3 border-top border-punk mt-auto">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<p class="mb-0 text-punk">© 2025. Все права защищены.</p>
<nav class="d-flex align-items-center">
<a href="#" class="text-punk me-3">Политика конфиденциальности</a>
</nav>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1 @@
<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><link href="/styles.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"><link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"><title>Панкуха</title><script defer="defer" src="/punkrock/static/js/main.e5d90428.js"></script><link href="/punkrock/static/css/main.87b4f72f.css" rel="stylesheet"></head><body class="bg-dark text-light"><div id="root"></div><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script></body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,15 @@
{
"short_name": "PunkRock",
"name": "PunkRock SPA",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 240 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 969 KiB

After

Width:  |  Height:  |  Size: 969 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,56 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* react-router v7.6.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,189 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--punk-primary: blueviolet;
--punk-dark: #121212;
}
a {
font-size: 16px;
font-weight: 500;
color: blueviolet;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
background-color: var(--punk-dark);
color: white;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
text-align: center;
}
.lyrics {
text-align: center;
line-height: 1.8;
font-size: 1.1rem;
}
.chorus {
font-weight: bold;
margin: 1.5rem 0;
}
#app {
width: 100%;
margin: 0;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #f7df1eaa);
}
/* Анимация карточек */
.catalog-item {
transition: transform 0.3s;
}
.catalog-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.3);
}
.btn-punk {
background-color: blueviolet;
color: white;
border: none;
}
.btn-punk:hover {
background-color: #9d4edd;
color: white;
}
.artist-card {
transition: all 0.3s;
}
.artist-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(138, 43, 226, 0.4);
}
.bg-punk {
background-color: blueviolet !important;
}
/* Стиль кнопок */
.btn-outline-punk {
color: blueviolet;
border-color: blueviolet;
}
.btn-outline-punk:hover {
background-color: blueviolet;
color: white;
}
.card {
padding: 2em;
color: blueviolet;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.bg-punk {
background-color: var(--punk-primary) !important;
}
.text-punk {
color: var(--punk-primary) !important;
}
.border-punk {
border-color: var(--punk-primary) !important;
}
.nav-link:hover, .dropdown-item:hover {
color: white !important;
background-color: var(--punk-primary) !important;
}
.list-group-item-action:hover {
transform: translateX(5px);
transition: transform 0.3s;
}
.lead{
color: blueviolet;
text-align: center;
font-size: 20pt;
}
.navbar {
box-shadow: 0 0 15px rgba(138, 43, 226, 0.4);
}
.dropdown-menu {
background-color: #000 !important;
}
.nav-link:hover,
.nav-link:focus {
text-shadow: 0 0 8px blueviolet;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,13 @@
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
}

Some files were not shown because too many files have changed in this diff Show More