слава богу она сделалась
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,7 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.2.0'
|
id 'org.springframework.boot' version '3.2.5'
|
||||||
id 'io.spring.dependency-management' version '1.1.4'
|
id 'io.spring.dependency-management' version '1.1.5'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'com.example'
|
group = 'com.example'
|
||||||
@@ -20,6 +20,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
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.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
package com.example.demo.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class CorsConfig {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
|
||||||
CorsConfiguration configuration = new CorsConfiguration();
|
|
||||||
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
|
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
|
||||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
|
||||||
configuration.setAllowCredentials(true);
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
||||||
source.registerCorsConfiguration("/**", configuration);
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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")
|
||||||
|
.allowedOrigins(Constants.DEV_ORIGIN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,72 +1,54 @@
|
|||||||
package com.example.demo.controller;
|
package com.example.demo.controller;
|
||||||
|
|
||||||
import com.example.demo.dto.ArtistDto;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import java.util.*;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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.ArtistRq;
|
||||||
|
import com.example.demo.dto.ArtistRs;
|
||||||
|
import com.example.demo.service.ArtistService;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping(Constants.API_URL + ArtistController.URL)
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class ArtistController {
|
public class ArtistController {
|
||||||
|
public static final String URL = "/artists";
|
||||||
private final Map<Integer, ArtistDto> artists = new LinkedHashMap<>();
|
private final ArtistService artistService;
|
||||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
|
||||||
|
|
||||||
public ArtistController() {}
|
public ArtistController(ArtistService artistService) {
|
||||||
|
this.artistService = artistService;
|
||||||
@GetMapping("/artists")
|
|
||||||
public List<ArtistDto> getAllArtists() {
|
|
||||||
return new ArrayList<>(artists.values());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/artists/{id}")
|
@GetMapping
|
||||||
public ArtistDto getArtistById(@PathVariable Integer id) {
|
public List<ArtistRs> getAll() {
|
||||||
return artists.get(id);
|
return artistService.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/artists")
|
@GetMapping("/{id}")
|
||||||
public ArtistDto createArtist(@RequestBody ArtistDto artist) {
|
public ArtistRs get(@PathVariable Long id) {
|
||||||
Integer newId = idCounter.getAndIncrement();
|
return artistService.get(id);
|
||||||
artist.setId(newId);
|
|
||||||
artists.put(newId, artist);
|
|
||||||
return artist;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/artists/{id}")
|
@PostMapping
|
||||||
public ArtistDto updateArtist(@PathVariable Integer id, @RequestBody ArtistDto artist) {
|
public ArtistRs create(@RequestBody @Valid ArtistRq dto) {
|
||||||
if (artists.containsKey(id)) {
|
return artistService.create(dto);
|
||||||
artist.setId(id);
|
|
||||||
artists.put(id, artist);
|
|
||||||
return artist;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping("/artists/{id}")
|
@PutMapping("/{id}")
|
||||||
public ArtistDto patchArtist(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
public ArtistRs update(@PathVariable Long id, @RequestBody @Valid ArtistRq dto) {
|
||||||
ArtistDto existingArtist = artists.get(id);
|
return artistService.update(id, dto);
|
||||||
if (existingArtist != null) {
|
|
||||||
if (updates.containsKey("name")) {
|
|
||||||
existingArtist.setName((String) updates.get("name"));
|
|
||||||
}
|
|
||||||
if (updates.containsKey("description")) {
|
|
||||||
existingArtist.setDescription((String) updates.get("description"));
|
|
||||||
}
|
|
||||||
if (updates.containsKey("epochId")) {
|
|
||||||
existingArtist.setEpochId((Integer) updates.get("epochId"));
|
|
||||||
}
|
|
||||||
if (updates.containsKey("countryId")) {
|
|
||||||
existingArtist.setCountryId((Integer) updates.get("countryId"));
|
|
||||||
}
|
|
||||||
artists.put(id, existingArtist);
|
|
||||||
return existingArtist;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/artists/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public boolean deleteArtist(@PathVariable Integer id) {
|
public ArtistRs delete(@PathVariable Long id) {
|
||||||
return artists.remove(id) != null;
|
return artistService.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,54 @@
|
|||||||
package com.example.demo.controller;
|
package com.example.demo.controller;
|
||||||
|
|
||||||
import com.example.demo.dto.CountryDto;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import java.util.*;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping(Constants.API_URL + CountryController.URL)
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class CountryController {
|
public class CountryController {
|
||||||
|
public static final String URL = "/countries";
|
||||||
private final Map<Integer, CountryDto> countries = new LinkedHashMap<>();
|
private final CountryService countryService;
|
||||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
|
||||||
|
|
||||||
public CountryController() {
|
public CountryController(CountryService countryService) {
|
||||||
|
this.countryService = countryService;
|
||||||
countries.put(1, new CountryDto(1, "Россия"));
|
|
||||||
countries.put(2, new CountryDto(2, "США"));
|
|
||||||
countries.put(3, new CountryDto(3, "Тайга"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/countries")
|
@GetMapping
|
||||||
public List<CountryDto> getAllCountries() {
|
public List<CountryRs> getAll() {
|
||||||
return new ArrayList<>(countries.values());
|
return countryService.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/countries/{id}")
|
@GetMapping("/{id}")
|
||||||
public CountryDto getCountryById(@PathVariable Integer id) {
|
public CountryRs get(@PathVariable Long id) {
|
||||||
return countries.get(id);
|
return countryService.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/countries")
|
@PostMapping
|
||||||
public CountryDto createCountry(@RequestBody CountryDto country) {
|
public CountryRs create(@RequestBody @Valid CountryRq dto) {
|
||||||
Integer newId = idCounter.getAndIncrement();
|
return countryService.create(dto);
|
||||||
country.setId(newId);
|
|
||||||
countries.put(newId, country);
|
|
||||||
return country;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/countries/{id}")
|
@PutMapping("/{id}")
|
||||||
public CountryDto updateCountry(@PathVariable Integer id, @RequestBody CountryDto country) {
|
public CountryRs update(@PathVariable Long id, @RequestBody @Valid CountryRq dto) {
|
||||||
if (countries.containsKey(id)) {
|
return countryService.update(id, dto);
|
||||||
country.setId(id);
|
|
||||||
countries.put(id, country);
|
|
||||||
return country;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping("/countries/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public CountryDto patchCountry(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
public CountryRs delete(@PathVariable Long id) {
|
||||||
CountryDto existingCountry = countries.get(id);
|
return countryService.delete(id);
|
||||||
if (existingCountry != null) {
|
|
||||||
if (updates.containsKey("name")) {
|
|
||||||
existingCountry.setName((String) updates.get("name"));
|
|
||||||
}
|
|
||||||
countries.put(id, existingCountry);
|
|
||||||
return existingCountry;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/countries/{id}")
|
|
||||||
public boolean deleteCountry(@PathVariable Integer id) {
|
|
||||||
return countries.remove(id) != null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,54 @@
|
|||||||
package com.example.demo.controller;
|
package com.example.demo.controller;
|
||||||
|
|
||||||
import com.example.demo.dto.EpochDto;
|
import java.util.List;
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
import java.util.*;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
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
|
@RestController
|
||||||
@RequestMapping("/api")
|
@RequestMapping(Constants.API_URL + EpochController.URL)
|
||||||
@CrossOrigin(origins = "*")
|
|
||||||
public class EpochController {
|
public class EpochController {
|
||||||
|
public static final String URL = "/epochs";
|
||||||
private final Map<Integer, EpochDto> epochs = new LinkedHashMap<>();
|
private final EpochService epochService;
|
||||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
|
||||||
|
|
||||||
public EpochController() {
|
public EpochController(EpochService epochService) {
|
||||||
epochs.put(1, new EpochDto(1, "1980-е"));
|
this.epochService = epochService;
|
||||||
epochs.put(2, new EpochDto(2, "1990-е"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/epochs")
|
@GetMapping
|
||||||
public List<EpochDto> getAllEpochs() {
|
public List<EpochRs> getAll() {
|
||||||
return new ArrayList<>(epochs.values());
|
return epochService.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/epochs/{id}")
|
@GetMapping("/{id}")
|
||||||
public EpochDto getEpochById(@PathVariable Integer id) {
|
public EpochRs get(@PathVariable Long id) {
|
||||||
return epochs.get(id);
|
return epochService.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/epochs")
|
@PostMapping
|
||||||
public EpochDto createEpoch(@RequestBody EpochDto epoch) {
|
public EpochRs create(@RequestBody @Valid EpochRq dto) {
|
||||||
Integer newId = idCounter.getAndIncrement();
|
return epochService.create(dto);
|
||||||
epoch.setId(newId);
|
|
||||||
epochs.put(newId, epoch);
|
|
||||||
return epoch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/epochs/{id}")
|
@PutMapping("/{id}")
|
||||||
public EpochDto updateEpoch(@PathVariable Integer id, @RequestBody EpochDto epoch) {
|
public EpochRs update(@PathVariable Long id, @RequestBody @Valid EpochRq dto) {
|
||||||
if (epochs.containsKey(id)) {
|
return epochService.update(id, dto);
|
||||||
epoch.setId(id);
|
|
||||||
epochs.put(id, epoch);
|
|
||||||
return epoch;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PatchMapping("/epochs/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public EpochDto patchEpoch(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
public EpochRs delete(@PathVariable Long id) {
|
||||||
EpochDto existingEpoch = epochs.get(id);
|
return epochService.delete(id);
|
||||||
if (existingEpoch != null) {
|
|
||||||
if (updates.containsKey("name")) {
|
|
||||||
existingEpoch.setName((String) updates.get("name"));
|
|
||||||
}
|
|
||||||
epochs.put(id, existingEpoch);
|
|
||||||
return existingEpoch;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@DeleteMapping("/epochs/{id}")
|
|
||||||
public boolean deleteEpoch(@PathVariable Integer id) {
|
|
||||||
return epochs.remove(id) != null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
demo/src/main/java/com/example/demo/dto/ArtistRq.java
Normal file
47
demo/src/main/java/com/example/demo/dto/ArtistRq.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
50
demo/src/main/java/com/example/demo/dto/ArtistRs.java
Normal file
50
demo/src/main/java/com/example/demo/dto/ArtistRs.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
demo/src/main/java/com/example/demo/dto/CountryRq.java
Normal file
17
demo/src/main/java/com/example/demo/dto/CountryRq.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
demo/src/main/java/com/example/demo/dto/CountryRs.java
Normal file
23
demo/src/main/java/com/example/demo/dto/CountryRs.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
demo/src/main/java/com/example/demo/dto/EpochRq.java
Normal file
17
demo/src/main/java/com/example/demo/dto/EpochRq.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
demo/src/main/java/com/example/demo/dto/EpochRs.java
Normal file
23
demo/src/main/java/com/example/demo/dto/EpochRs.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
53
demo/src/main/java/com/example/demo/entity/ArtistEntity.java
Normal file
53
demo/src/main/java/com/example/demo/entity/ArtistEntity.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
public class ArtistEntity extends BaseEntity {
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private EpochEntity epoch;
|
||||||
|
private CountryEntity country;
|
||||||
|
|
||||||
|
public ArtistEntity() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArtistEntity(String name, String description, EpochEntity epoch, CountryEntity country) {
|
||||||
|
this();
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.epoch = epoch;
|
||||||
|
this.country = 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) {
|
||||||
|
this.epoch = epoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryEntity getCountry() {
|
||||||
|
return country;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCountry(CountryEntity country) {
|
||||||
|
this.country = country;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
demo/src/main/java/com/example/demo/entity/BaseEntity.java
Normal file
17
demo/src/main/java/com/example/demo/entity/BaseEntity.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
public abstract class BaseEntity {
|
||||||
|
protected Long id;
|
||||||
|
|
||||||
|
protected BaseEntity() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
public class CountryEntity extends BaseEntity {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public CountryEntity() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryEntity(String name) {
|
||||||
|
this();
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
demo/src/main/java/com/example/demo/entity/EpochEntity.java
Normal file
23
demo/src/main/java/com/example/demo/entity/EpochEntity.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package com.example.demo.entity;
|
||||||
|
|
||||||
|
public class EpochEntity extends BaseEntity {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public EpochEntity() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochEntity(String name) {
|
||||||
|
this();
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
51
demo/src/main/java/com/example/demo/mapper/ArtistMapper.java
Normal file
51
demo/src/main/java/com/example/demo/mapper/ArtistMapper.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
33
demo/src/main/java/com/example/demo/mapper/EpochMapper.java
Normal file
33
demo/src/main/java/com/example/demo/mapper/EpochMapper.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.ArtistEntity;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class ArtistRepository extends MapRepository<ArtistEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface CommonRepository<E, T> {
|
||||||
|
Iterable<E> findAll();
|
||||||
|
|
||||||
|
Optional<E> findById(T id);
|
||||||
|
|
||||||
|
E save(E entity);
|
||||||
|
|
||||||
|
void delete(E entity);
|
||||||
|
|
||||||
|
void deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.CountryEntity;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class CountryRepository extends MapRepository<CountryEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import com.example.demo.entity.EpochEntity;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class EpochRepository extends MapRepository<EpochEntity> {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.example.demo.repository;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ConcurrentNavigableMap;
|
||||||
|
import java.util.concurrent.ConcurrentSkipListMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
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<>();
|
||||||
|
private final AtomicLong idGenerator = new AtomicLong(0L);
|
||||||
|
|
||||||
|
protected MapRepository() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isNew(E entity) {
|
||||||
|
return Objects.isNull(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private E create(E entity) {
|
||||||
|
final Long lastId = idGenerator.incrementAndGet();
|
||||||
|
entity.setId(lastId);
|
||||||
|
entities.put(lastId, entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private E update(E entity) {
|
||||||
|
if (findById(entity.getId()).isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
entities.put(entity.getId(), entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterable<E> findAll() {
|
||||||
|
return entities.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<E> findById(Long id) {
|
||||||
|
return Optional.ofNullable(entities.get(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public E save(E entity) {
|
||||||
|
if (isNew(entity)) {
|
||||||
|
return create(entity);
|
||||||
|
}
|
||||||
|
return update(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(E entity) {
|
||||||
|
if (findById(entity.getId()).isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entities.remove(entity.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteAll() {
|
||||||
|
entities.clear();
|
||||||
|
idGenerator.set(0L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArtistEntity getEntity(Long id) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException(ArtistEntity.class, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ArtistRs> getAll() {
|
||||||
|
return mapper.toRsDtoList(repository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArtistRs get(Long id) {
|
||||||
|
final ArtistEntity entity = getEntity(id);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArtistRs delete(Long id) {
|
||||||
|
final ArtistEntity entity = getEntity(id);
|
||||||
|
repository.delete(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryEntity getEntity(Long id) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException(CountryEntity.class, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CountryRs> getAll() {
|
||||||
|
return mapper.toRsDtoList(repository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryRs get(Long id) {
|
||||||
|
final CountryEntity entity = getEntity(id);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryRs create(CountryRq dto) {
|
||||||
|
CountryEntity entity = new CountryEntity(dto.getName());
|
||||||
|
entity = repository.save(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryRs update(Long id, CountryRq dto) {
|
||||||
|
CountryEntity entity = getEntity(id);
|
||||||
|
entity.setName(dto.getName());
|
||||||
|
entity = repository.save(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CountryRs delete(Long id) {
|
||||||
|
final CountryEntity entity = getEntity(id);
|
||||||
|
repository.delete(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochEntity getEntity(Long id) {
|
||||||
|
return repository.findById(id)
|
||||||
|
.orElseThrow(() -> new NotFoundException(EpochEntity.class, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EpochRs> getAll() {
|
||||||
|
return mapper.toRsDtoList(repository.findAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochRs get(Long id) {
|
||||||
|
final EpochEntity entity = getEntity(id);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochRs create(EpochRq dto) {
|
||||||
|
EpochEntity entity = new EpochEntity(dto.getName());
|
||||||
|
entity = repository.save(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochRs update(Long id, EpochRq dto) {
|
||||||
|
EpochEntity entity = getEntity(id);
|
||||||
|
entity.setName(dto.getName());
|
||||||
|
entity = repository.save(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EpochRs delete(Long id) {
|
||||||
|
final EpochEntity entity = getEntity(id);
|
||||||
|
repository.delete(entity);
|
||||||
|
return mapper.toRsDto(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
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 com.example.demo.dto.ArtistRs;
|
||||||
|
import com.example.demo.error.NotFoundException;
|
||||||
|
import com.example.demo.mapper.ArtistMapper;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@TestMethodOrder(OrderAnnotation.class)
|
||||||
|
public class ArtistServiceTests {
|
||||||
|
@Autowired
|
||||||
|
private ArtistService service;
|
||||||
|
@Autowired
|
||||||
|
private EpochService epochService;
|
||||||
|
@Autowired
|
||||||
|
private CountryService countryService;
|
||||||
|
@Autowired
|
||||||
|
private ArtistMapper mapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTest() {
|
||||||
|
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void createTest() {
|
||||||
|
// Создаем необходимые зависимости
|
||||||
|
final var epochRq1 = new com.example.demo.dto.EpochRq();
|
||||||
|
epochRq1.setName("1980-е");
|
||||||
|
final var epoch1 = epochService.create(epochRq1);
|
||||||
|
|
||||||
|
final var epochRq2 = new com.example.demo.dto.EpochRq();
|
||||||
|
epochRq2.setName("1990-е");
|
||||||
|
final var epoch2 = epochService.create(epochRq2);
|
||||||
|
|
||||||
|
final var countryRq1 = new com.example.demo.dto.CountryRq();
|
||||||
|
countryRq1.setName("Россия");
|
||||||
|
final var country1 = countryService.create(countryRq1);
|
||||||
|
|
||||||
|
final var countryRq2 = new com.example.demo.dto.CountryRq();
|
||||||
|
countryRq2.setName("США");
|
||||||
|
final var country2 = countryService.create(countryRq2);
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
final ArtistRs cmpEntity = service.get(3L);
|
||||||
|
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
void updateTest() {
|
||||||
|
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 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(3L);
|
||||||
|
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void deleteTest() {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
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 com.example.demo.dto.CountryRs;
|
||||||
|
import com.example.demo.error.NotFoundException;
|
||||||
|
import com.example.demo.mapper.CountryMapper;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@TestMethodOrder(OrderAnnotation.class)
|
||||||
|
public class CountryServiceTests {
|
||||||
|
@Autowired
|
||||||
|
private CountryService service;
|
||||||
|
@Autowired
|
||||||
|
private CountryMapper mapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTest() {
|
||||||
|
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void createTest() {
|
||||||
|
service.create(mapper.toRqDto("Россия"));
|
||||||
|
service.create(mapper.toRqDto("США"));
|
||||||
|
final CountryRs last = service.create(mapper.toRqDto("Тайга"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(3, service.getAll().size());
|
||||||
|
|
||||||
|
final CountryRs cmpEntity = service.get(3L);
|
||||||
|
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
void updateTest() {
|
||||||
|
final String test = "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(3L);
|
||||||
|
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void deleteTest() {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.example.demo.service;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
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 com.example.demo.dto.EpochRs;
|
||||||
|
import com.example.demo.error.NotFoundException;
|
||||||
|
import com.example.demo.mapper.EpochMapper;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@TestMethodOrder(OrderAnnotation.class)
|
||||||
|
public class EpochServiceTests {
|
||||||
|
@Autowired
|
||||||
|
private EpochService service;
|
||||||
|
@Autowired
|
||||||
|
private EpochMapper mapper;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getTest() {
|
||||||
|
Assertions.assertThrows(NotFoundException.class, () -> service.get(0L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1)
|
||||||
|
void createTest() {
|
||||||
|
service.create(mapper.toRqDto("1980-е"));
|
||||||
|
service.create(mapper.toRqDto("1990-е"));
|
||||||
|
final EpochRs last = service.create(mapper.toRqDto("2000-е"));
|
||||||
|
|
||||||
|
Assertions.assertEquals(3, service.getAll().size());
|
||||||
|
|
||||||
|
final EpochRs cmpEntity = service.get(3L);
|
||||||
|
Assertions.assertEquals(last.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(last.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
void updateTest() {
|
||||||
|
final String test = "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(3L);
|
||||||
|
Assertions.assertEquals(newEntity.getId(), cmpEntity.getId());
|
||||||
|
Assertions.assertEquals(newEntity.getName(), cmpEntity.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(3)
|
||||||
|
void deleteTest() {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,8 +15,8 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
|||||||
setFormData({
|
setFormData({
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
description: artist.description,
|
description: artist.description,
|
||||||
epochId: artist.epoch?.id || artist.epochId || '',
|
epochId: artist.epoch?.id || '',
|
||||||
countryId: artist.country?.id || artist.countryId || ''
|
countryId: artist.country?.id || ''
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -45,8 +45,8 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
|||||||
onSubmit({
|
onSubmit({
|
||||||
name: formData.name.trim(),
|
name: formData.name.trim(),
|
||||||
description: formData.description.trim(),
|
description: formData.description.trim(),
|
||||||
epochId: parseInt(formData.epochId),
|
epochId: Number(formData.epochId),
|
||||||
countryId: parseInt(formData.countryId)
|
countryId: Number(formData.countryId)
|
||||||
});
|
});
|
||||||
if (!artist) {
|
if (!artist) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|||||||
@@ -25,14 +25,8 @@ const PunkRockPage = () => {
|
|||||||
console.log('Fetched Countries:', countriesData);
|
console.log('Fetched Countries:', countriesData);
|
||||||
console.log('Fetched Epochs:', epochsData);
|
console.log('Fetched Epochs:', epochsData);
|
||||||
setArtists(artistsData);
|
setArtists(artistsData);
|
||||||
setCountries(countriesData.map(country => ({
|
setCountries(countriesData);
|
||||||
...country,
|
setEpochs(epochsData);
|
||||||
id: parseInt(country.id)
|
|
||||||
})));
|
|
||||||
setEpochs(epochsData.map(epoch => ({
|
|
||||||
...epoch,
|
|
||||||
id: parseInt(epoch.id)
|
|
||||||
})));
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching data:', error);
|
console.error('Error fetching data:', error);
|
||||||
@@ -54,12 +48,7 @@ const PunkRockPage = () => {
|
|||||||
const newArtist = await createArtist(artistData);
|
const newArtist = await createArtist(artistData);
|
||||||
console.log('Added Artist:', newArtist);
|
console.log('Added Artist:', newArtist);
|
||||||
setArtists(prevArtists => {
|
setArtists(prevArtists => {
|
||||||
const enrichedArtist = {
|
return [...prevArtists, newArtist].sort((a, b) =>
|
||||||
...newArtist,
|
|
||||||
epoch: epochs.find(epoch => epoch.id === parseInt(newArtist.epochId)),
|
|
||||||
country: countries.find(country => country.id === parseInt(newArtist.countryId))
|
|
||||||
};
|
|
||||||
return [...prevArtists, enrichedArtist].sort((a, b) =>
|
|
||||||
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -83,12 +72,7 @@ const PunkRockPage = () => {
|
|||||||
const updatedArtist = await updateArtist(editingArtist.id, artistData);
|
const updatedArtist = await updateArtist(editingArtist.id, artistData);
|
||||||
console.log('Updated Artist:', updatedArtist);
|
console.log('Updated Artist:', updatedArtist);
|
||||||
setArtists(prevArtists => {
|
setArtists(prevArtists => {
|
||||||
const enrichedArtist = {
|
return prevArtists.map(a => (a.id === updatedArtist.id ? updatedArtist : a))
|
||||||
...updatedArtist,
|
|
||||||
epoch: epochs.find(epoch => epoch.id === parseInt(updatedArtist.epochId)),
|
|
||||||
country: countries.find(country => country.id === parseInt(updatedArtist.countryId))
|
|
||||||
};
|
|
||||||
return prevArtists.map(a => (a.id === updatedArtist.id ? enrichedArtist : a))
|
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||||
);
|
);
|
||||||
@@ -121,11 +105,7 @@ const PunkRockPage = () => {
|
|||||||
return <div className="text-center text-punk my-5">Загрузка...</div>;
|
return <div className="text-center text-punk my-5">Загрузка...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrichedArtists = artists.map(artist => ({
|
const sortedArtists = artists.sort((a, b) =>
|
||||||
...artist,
|
|
||||||
epoch: epochs.find(epoch => epoch.id === parseInt(artist.epochId)),
|
|
||||||
country: countries.find(country => country.id === parseInt(artist.countryId))
|
|
||||||
})).sort((a, b) =>
|
|
||||||
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -149,7 +129,7 @@ const PunkRockPage = () => {
|
|||||||
Сортировать {sortDirection === 'asc' ? 'А-Я' : 'Я-А'}
|
Сортировать {sortDirection === 'asc' ? 'А-Я' : 'Я-А'}
|
||||||
</button>
|
</button>
|
||||||
<ArtistList
|
<ArtistList
|
||||||
artists={enrichedArtists}
|
artists={sortedArtists}
|
||||||
onEdit={setEditingArtist}
|
onEdit={setEditingArtist}
|
||||||
onDelete={handleDeleteArtist}
|
onDelete={handleDeleteArtist}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const createArtist = async (artist) => {
|
|||||||
|
|
||||||
export const updateArtist = async (id, artist) => {
|
export const updateArtist = async (id, artist) => {
|
||||||
const response = await fetch(`${API_URL}/artists/${id}`, {
|
const response = await fetch(`${API_URL}/artists/${id}`, {
|
||||||
method: 'PATCH',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(artist)
|
body: JSON.stringify(artist)
|
||||||
});
|
});
|
||||||
|
|||||||
73
Лекция 2 (3)/.gitignore
vendored
Normal file
73
Лекция 2 (3)/.gitignore
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
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/
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
out/
|
||||||
|
!**/src/main/**/out/
|
||||||
|
!**/src/test/**/out/
|
||||||
|
.idea/
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.history/*
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
|
||||||
|
# Compiled output
|
||||||
|
/dist
|
||||||
|
/tmp
|
||||||
|
/out-tsc
|
||||||
|
/bazel-out
|
||||||
|
|
||||||
|
# Node
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Miscellaneous
|
||||||
|
/.angular/cache
|
||||||
|
.sass-cache/
|
||||||
|
/connect.lock
|
||||||
|
/coverage
|
||||||
|
/libpeerconnection.log
|
||||||
|
testem.log
|
||||||
|
/typings
|
||||||
|
|
||||||
|
# System files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
12
Лекция 2 (3)/.vscode/extensions.json
vendored
Normal file
12
Лекция 2 (3)/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
// fronted
|
||||||
|
"AndersEAndersen.html-class-suggestions",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
// backend
|
||||||
|
"vscjava.vscode-java-pack",
|
||||||
|
"vmware.vscode-boot-dev-pack",
|
||||||
|
"vscjava.vscode-gradle",
|
||||||
|
"redhat.vscode-xml"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
Лекция 2 (3)/.vscode/launch.json
vendored
Normal file
14
Лекция 2 (3)/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "java",
|
||||||
|
"name": "Server",
|
||||||
|
"request": "launch",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"mainClass": "ru.ulstu.is.server.ServerApplication",
|
||||||
|
"projectName": "lec2",
|
||||||
|
"envFile": "${workspaceFolder}/.env",
|
||||||
|
"args": "--populate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
54
Лекция 2 (3)/.vscode/settings.json
vendored
Normal file
54
Лекция 2 (3)/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"files.eol": "\n",
|
||||||
|
"editor.detectIndentation": false,
|
||||||
|
"editor.formatOnType": false,
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.tabSize": 4,
|
||||||
|
"editor.insertSpaces": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit",
|
||||||
|
"source.sortImports": "explicit"
|
||||||
|
},
|
||||||
|
"editor.snippetSuggestions": "bottom",
|
||||||
|
"debug.toolBarLocation": "commandCenter",
|
||||||
|
"debug.showVariableTypes": true,
|
||||||
|
"errorLens.gutterIconsEnabled": true,
|
||||||
|
"errorLens.messageEnabled": false,
|
||||||
|
"prettier.tabWidth": 4,
|
||||||
|
"prettier.singleQuote": false,
|
||||||
|
"prettier.printWidth": 120,
|
||||||
|
"prettier.trailingComma": "es5",
|
||||||
|
"prettier.useTabs": false,
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[css]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
|
"java.configuration.updateBuildConfiguration": "automatic",
|
||||||
|
"[java]": {
|
||||||
|
"editor.pasteAs.enabled": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "never",
|
||||||
|
"source.sortImports": "explicit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gradle.nestedProjects": true,
|
||||||
|
"java.saveActions.organizeImports": true,
|
||||||
|
"java.dependency.packagePresentation": "hierarchical",
|
||||||
|
"java.codeGeneration.hashCodeEquals.useJava7Objects": true,
|
||||||
|
"spring-boot.ls.problem.boot2.JAVA_CONSTRUCTOR_PARAMETER_INJECTION": "WARNING",
|
||||||
|
"spring.initializr.defaultLanguage": "Java"
|
||||||
|
}
|
||||||
5
Лекция 2 (3)/README.md
Normal file
5
Лекция 2 (3)/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Swagger UI URL:
|
||||||
|
http://localhost:8080/swagger-ui/index.html
|
||||||
|
|
||||||
|
MVN Repository:
|
||||||
|
https://mvnrepository.com/
|
||||||
60
Лекция 2 (3)/build.gradle
Normal file
60
Лекция 2 (3)/build.gradle
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
plugins {
|
||||||
|
id "java"
|
||||||
|
id "org.springframework.boot" version "3.5.5"
|
||||||
|
id "io.spring.dependency-management" version "1.1.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "ru.ulstu.is"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
description = "My demo server app"
|
||||||
|
def jdkVersion = "21"
|
||||||
|
|
||||||
|
defaultTasks "bootRun"
|
||||||
|
|
||||||
|
jar {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
bootJar {
|
||||||
|
archiveFileName = String.format("%s-%s.jar", rootProject.name, version)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert System.properties["java.specification.version"] == jdkVersion
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(jdkVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
springdocVersion = "2.8.11"
|
||||||
|
mockitoVersion = "5.19.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
mockitoAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
testImplementation "org.springframework.boot:spring-boot-starter-test"
|
||||||
|
testRuntimeOnly "org.junit.platform:junit-platform-launcher"
|
||||||
|
|
||||||
|
testImplementation "org.mockito:mockito-core:${mockitoVersion}"
|
||||||
|
mockitoAgent("org.mockito:mockito-core:${mockitoVersion}") {
|
||||||
|
transitive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("test") {
|
||||||
|
useJUnitPlatform()
|
||||||
|
jvmArgs += "-Xshare:off"
|
||||||
|
jvmArgs += "-javaagent:${configurations.mockitoAgent.asPath}"
|
||||||
|
}
|
||||||
17
Лекция 2 (3)/front/README.md
Normal file
17
Лекция 2 (3)/front/README.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Установка зависимостей
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск в режиме разработки
|
||||||
|
|
||||||
|
```
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Запуск для использования в продуктовой среде
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run prod
|
||||||
|
```
|
||||||
52
Лекция 2 (3)/front/eslint.config.js
Normal file
52
Лекция 2 (3)/front/eslint.config.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
13
Лекция 2 (3)/front/index.html
Normal file
13
Лекция 2 (3)/front/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!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>
|
||||||
18
Лекция 2 (3)/front/jsconfig.json
Normal file
18
Лекция 2 (3)/front/jsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"checkJs": true,
|
||||||
|
"paths": {
|
||||||
|
"@entities/*": ["./src/entities/*"],
|
||||||
|
"@pages/*": ["./src/pages/*"],
|
||||||
|
"@shared/*": ["./src/shared/*"],
|
||||||
|
"@widgets/*": ["./src/widgets/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "**/node_modules/*"]
|
||||||
|
}
|
||||||
6037
Лекция 2 (3)/front/package-lock.json
generated
Normal file
6037
Лекция 2 (3)/front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
Лекция 2 (3)/front/package.json
Normal file
40
Лекция 2 (3)/front/package.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Лекция 2 (3)/front/public/icon.svg
Normal file
25
Лекция 2 (3)/front/public/icon.svg
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?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>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
BIN
Лекция 2 (3)/front/public/images/200.png
Normal file
BIN
Лекция 2 (3)/front/public/images/200.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
Лекция 2 (3)/front/public/images/banner1.png
Normal file
BIN
Лекция 2 (3)/front/public/images/banner1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 640 KiB |
BIN
Лекция 2 (3)/front/public/images/banner2.png
Normal file
BIN
Лекция 2 (3)/front/public/images/banner2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 812 KiB |
BIN
Лекция 2 (3)/front/public/images/banner3.png
Normal file
BIN
Лекция 2 (3)/front/public/images/banner3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 767 KiB |
BIN
Лекция 2 (3)/front/public/images/logo.png
Normal file
BIN
Лекция 2 (3)/front/public/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
30
Лекция 2 (3)/front/src/app/index.jsx
Normal file
30
Лекция 2 (3)/front/src/app/index.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FifthEditPage, FifthPage, FourthPage, MainPage, NotFoundPage, SecondPage, ThirdPage } from "@pages/index";
|
||||||
|
import { ModalContainer, ModalProvider } from "@shared/modal";
|
||||||
|
import { ToastProvider } from "@shared/toast";
|
||||||
|
import { ToastContainer } from "@shared/toast/ui";
|
||||||
|
import { BrowserRouter, Route, Routes } from "react-router-dom";
|
||||||
|
import { MainLayout } from "./layouts";
|
||||||
|
|
||||||
|
export const App = () => {
|
||||||
|
return (
|
||||||
|
<ModalProvider>
|
||||||
|
<ToastProvider>
|
||||||
|
<ModalContainer />
|
||||||
|
<ToastContainer />
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route element={<MainLayout />}>
|
||||||
|
<Route path="/" element={<MainPage />} />
|
||||||
|
<Route path="/page2" element={<SecondPage />} />
|
||||||
|
<Route path="/page3" element={<ThirdPage />} />
|
||||||
|
<Route path="/page4" element={<FourthPage />} />
|
||||||
|
<Route path="/page5" element={<FifthPage />} />
|
||||||
|
<Route path="/page5/edit/:studentId?" element={<FifthEditPage />} />
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Route>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</ToastProvider>
|
||||||
|
</ModalProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
Лекция 2 (3)/front/src/app/layouts/index.js
Normal file
1
Лекция 2 (3)/front/src/app/layouts/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./main-layout";
|
||||||
14
Лекция 2 (3)/front/src/app/layouts/main-layout/index.jsx
Normal file
14
Лекция 2 (3)/front/src/app/layouts/main-layout/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
Лекция 2 (3)/front/src/entities/student/hooks/index.js
Normal file
3
Лекция 2 (3)/front/src/entities/student/hooks/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./student-group-hook";
|
||||||
|
export * from "./student-hook";
|
||||||
|
export * from "./students-hook";
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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 };
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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 };
|
||||||
|
};
|
||||||
4
Лекция 2 (3)/front/src/entities/student/index.js
Normal file
4
Лекция 2 (3)/front/src/entities/student/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/* eslint-disable import/export */
|
||||||
|
export * from "./hooks";
|
||||||
|
export * from "./model";
|
||||||
|
export * from "./ui";
|
||||||
7
Лекция 2 (3)/front/src/entities/student/model/index.js
Normal file
7
Лекция 2 (3)/front/src/entities/student/model/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const studentModel = {
|
||||||
|
last_name: ["Фамилия", "text"],
|
||||||
|
first_name: ["Имя", "text"],
|
||||||
|
email: ["Почта", "email"],
|
||||||
|
phone: ["Телефон", "tel"],
|
||||||
|
bdate: ["Дата рождения", "date"],
|
||||||
|
};
|
||||||
3
Лекция 2 (3)/front/src/entities/student/ui/index.js
Normal file
3
Лекция 2 (3)/front/src/entities/student/ui/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./students";
|
||||||
|
export * from "./students-form";
|
||||||
|
export * from "./students-table";
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.image-preview {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
11
Лекция 2 (3)/front/src/index.jsx
Normal file
11
Лекция 2 (3)/front/src/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
22
Лекция 2 (3)/front/src/pages/fifth-edit-page/index.jsx
Normal file
22
Лекция 2 (3)/front/src/pages/fifth-edit-page/index.jsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
5
Лекция 2 (3)/front/src/pages/fifth-page/index.jsx
Normal file
5
Лекция 2 (3)/front/src/pages/fifth-page/index.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Students } from "@entities/student";
|
||||||
|
|
||||||
|
export const FifthPage = () => {
|
||||||
|
return <Students />;
|
||||||
|
};
|
||||||
11
Лекция 2 (3)/front/src/pages/fourth-page/index.jsx
Normal file
11
Лекция 2 (3)/front/src/pages/fourth-page/index.jsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Calc, Paragraphs, Separator } from "@widgets/index";
|
||||||
|
|
||||||
|
export const FourthPage = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Calc />
|
||||||
|
<Separator />
|
||||||
|
<Paragraphs />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
7
Лекция 2 (3)/front/src/pages/index.js
Normal file
7
Лекция 2 (3)/front/src/pages/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
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";
|
||||||
81
Лекция 2 (3)/front/src/pages/main-page/index.jsx
Normal file
81
Лекция 2 (3)/front/src/pages/main-page/index.jsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
19
Лекция 2 (3)/front/src/pages/main-page/styles.css
Normal file
19
Лекция 2 (3)/front/src/pages/main-page/styles.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
.illustration {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.illustration {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.illustration {
|
||||||
|
width: 25% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Лекция 2 (3)/front/src/pages/not-found-page/index.jsx
Normal file
14
Лекция 2 (3)/front/src/pages/not-found-page/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
34
Лекция 2 (3)/front/src/pages/second-page/index.jsx
Normal file
34
Лекция 2 (3)/front/src/pages/second-page/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
103
Лекция 2 (3)/front/src/pages/third-page/index.jsx
Normal file
103
Лекция 2 (3)/front/src/pages/third-page/index.jsx
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
13
Лекция 2 (3)/front/src/shared/base64/index.js
Normal file
13
Лекция 2 (3)/front/src/shared/base64/index.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
};
|
||||||
36
Лекция 2 (3)/front/src/shared/client/index.js
Normal file
36
Лекция 2 (3)/front/src/shared/client/index.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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");
|
||||||
21
Лекция 2 (3)/front/src/shared/form/index.jsx
Normal file
21
Лекция 2 (3)/front/src/shared/form/index.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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 };
|
||||||
|
};
|
||||||
4
Лекция 2 (3)/front/src/shared/index.js
Normal file
4
Лекция 2 (3)/front/src/shared/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./base64";
|
||||||
|
export * from "./client";
|
||||||
|
export * from "./form";
|
||||||
|
export * from "./storage";
|
||||||
36
Лекция 2 (3)/front/src/shared/modal/context/index.jsx
Normal file
36
Лекция 2 (3)/front/src/shared/modal/context/index.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
};
|
||||||
2
Лекция 2 (3)/front/src/shared/modal/index.js
Normal file
2
Лекция 2 (3)/front/src/shared/modal/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./context";
|
||||||
|
export * from "./ui";
|
||||||
29
Лекция 2 (3)/front/src/shared/modal/ui/index.jsx
Normal file
29
Лекция 2 (3)/front/src/shared/modal/ui/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
5
Лекция 2 (3)/front/src/shared/storage/index.js
Normal file
5
Лекция 2 (3)/front/src/shared/storage/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const lsSave = (key, value) => {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lsReadArray = (key) => JSON.parse(localStorage.getItem(key)) || [];
|
||||||
84
Лекция 2 (3)/front/src/shared/toast/context/index.jsx
Normal file
84
Лекция 2 (3)/front/src/shared/toast/context/index.jsx
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { lsReadArray, lsSave } from "@shared/storage";
|
||||||
|
import { createContext, useContext, useEffect, useReducer } from "react";
|
||||||
|
|
||||||
|
const KEY = "toasts";
|
||||||
|
|
||||||
|
const ToastContext = createContext(null);
|
||||||
|
const ToastDispatcherContext = createContext(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const TOAST_ACTION = {
|
||||||
|
add: "add",
|
||||||
|
hide: "hide",
|
||||||
|
delete: "delete",
|
||||||
|
deleteAll: "delete-all",
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDate = () => {
|
||||||
|
const zeroPad = (num) => String(num).padStart(2, "0");
|
||||||
|
const date = new Date();
|
||||||
|
const day = zeroPad(date.getDate());
|
||||||
|
const month = zeroPad(date.getMonth() + 1);
|
||||||
|
const hours = zeroPad(date.getHours());
|
||||||
|
const minutes = zeroPad(date.getMinutes());
|
||||||
|
return `${day}.${month} ${hours}:${minutes}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toastsReducer = (toasts, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case TOAST_ACTION.add: {
|
||||||
|
return [
|
||||||
|
...toasts,
|
||||||
|
{
|
||||||
|
id: crypto.randomUUID(),
|
||||||
|
title: action.title,
|
||||||
|
text: action.text,
|
||||||
|
date: getDate(),
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
case TOAST_ACTION.hide: {
|
||||||
|
return toasts.map((toast) => {
|
||||||
|
if (toast.id === action.id) {
|
||||||
|
return { ...toast, show: false };
|
||||||
|
} else {
|
||||||
|
return toast;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
case TOAST_ACTION.delete: {
|
||||||
|
return toasts.filter((toast) => toast.id !== action.id);
|
||||||
|
}
|
||||||
|
case TOAST_ACTION.deleteAll: {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
throw Error("Unknown action: " + action.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToastProvider = ({ children }) => {
|
||||||
|
const [toasts, dispatch] = useReducer(toastsReducer, [], () => lsReadArray(KEY));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
lsSave(KEY, toasts);
|
||||||
|
}, [toasts]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={toasts}>
|
||||||
|
<ToastDispatcherContext.Provider value={dispatch}>{children}</ToastDispatcherContext.Provider>
|
||||||
|
</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const useToasts = () => {
|
||||||
|
return useContext(ToastContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-refresh/only-export-components
|
||||||
|
export const useToastsDispatch = () => {
|
||||||
|
return useContext(ToastDispatcherContext);
|
||||||
|
};
|
||||||
1
Лекция 2 (3)/front/src/shared/toast/index.js
Normal file
1
Лекция 2 (3)/front/src/shared/toast/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./context";
|
||||||
34
Лекция 2 (3)/front/src/shared/toast/ui/container/index.jsx
Normal file
34
Лекция 2 (3)/front/src/shared/toast/ui/container/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
2
Лекция 2 (3)/front/src/shared/toast/ui/index.js
Normal file
2
Лекция 2 (3)/front/src/shared/toast/ui/index.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./container";
|
||||||
|
export * from "./menu";
|
||||||
56
Лекция 2 (3)/front/src/shared/toast/ui/menu/index.jsx
Normal file
56
Лекция 2 (3)/front/src/shared/toast/ui/menu/index.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { TOAST_ACTION, useToasts, useToastsDispatch } from "@shared/toast/context";
|
||||||
|
import { Dropdown } from "react-bootstrap";
|
||||||
|
import { BellFill, BellSlashFill } from "react-bootstrap-icons";
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export const ToastMenu = () => {
|
||||||
|
const toasts = useToasts();
|
||||||
|
const toast = useToastsDispatch();
|
||||||
|
|
||||||
|
const data = toasts.filter((toast) => !toast.show);
|
||||||
|
const isEmpty = data.length === 0;
|
||||||
|
const icon = isEmpty ? <BellSlashFill /> : <BellFill />;
|
||||||
|
const variant = isEmpty ? "secondary" : "primary";
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
toast({
|
||||||
|
type: TOAST_ACTION.delete,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAll = () => {
|
||||||
|
toast({
|
||||||
|
type: TOAST_ACTION.deleteAll,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const content = data.map((toast) => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item key={toast.id} className="toast-menu-item" onClick={() => handleDelete(toast.id)}>
|
||||||
|
<span className="fw-bold">{toast.date}</span>
|
||||||
|
|
||||||
|
<span>{toast.text}</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown>
|
||||||
|
<Dropdown.Toggle className="toast-toggle" variant={variant}>
|
||||||
|
{icon}
|
||||||
|
</Dropdown.Toggle>
|
||||||
|
|
||||||
|
<Dropdown.Menu className="toast-menu" align="end">
|
||||||
|
{content}
|
||||||
|
<Dropdown.Item
|
||||||
|
disabled={isEmpty}
|
||||||
|
className="toast-menu-item text-center text-body-tertiary"
|
||||||
|
onClick={handleDeleteAll}
|
||||||
|
>
|
||||||
|
Очистить
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
15
Лекция 2 (3)/front/src/shared/toast/ui/menu/styles.css
Normal file
15
Лекция 2 (3)/front/src/shared/toast/ui/menu/styles.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
27
Лекция 2 (3)/front/src/styles.css
Normal file
27
Лекция 2 (3)/front/src/styles.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
: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;
|
||||||
|
}
|
||||||
38
Лекция 2 (3)/front/src/widgets/banner/index.jsx
Normal file
38
Лекция 2 (3)/front/src/widgets/banner/index.jsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
Лекция 2 (3)/front/src/widgets/banner/styles.css
Normal file
9
Лекция 2 (3)/front/src/widgets/banner/styles.css
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
.carousel-caption {
|
||||||
|
background-color: var(--my-footer-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-control-next,
|
||||||
|
.carousel-control-prev {
|
||||||
|
filter: invert(80%);
|
||||||
|
}
|
||||||
86
Лекция 2 (3)/front/src/widgets/calc/index.jsx
Normal file
86
Лекция 2 (3)/front/src/widgets/calc/index.jsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
9
Лекция 2 (3)/front/src/widgets/footer/index.jsx
Normal file
9
Лекция 2 (3)/front/src/widgets/footer/index.jsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
export const Footer = () => {
|
||||||
|
return (
|
||||||
|
<footer className="d-flex flex-shrink-0 align-items-center justify-content-center">
|
||||||
|
Автор, {new Date().getFullYear()}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
5
Лекция 2 (3)/front/src/widgets/footer/styles.css
Normal file
5
Лекция 2 (3)/front/src/widgets/footer/styles.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
footer {
|
||||||
|
background-color: var(--my-footer-color);
|
||||||
|
height: 48px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
41
Лекция 2 (3)/front/src/widgets/header/index.jsx
Normal file
41
Лекция 2 (3)/front/src/widgets/header/index.jsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
7
Лекция 2 (3)/front/src/widgets/header/styles.css
Normal file
7
Лекция 2 (3)/front/src/widgets/header/styles.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
header nav {
|
||||||
|
background-color: var(--my-navbar-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
header nav a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user