слава богу она сделалась
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.2.0'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
id 'org.springframework.boot' version '3.2.5'
|
||||
id 'io.spring.dependency-management' version '1.1.5'
|
||||
}
|
||||
|
||||
group = 'com.example'
|
||||
@@ -20,6 +20,7 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
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;
|
||||
|
||||
import com.example.demo.dto.ArtistDto;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.ArtistRq;
|
||||
import com.example.demo.dto.ArtistRs;
|
||||
import com.example.demo.service.ArtistService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@CrossOrigin(origins = "*")
|
||||
@RequestMapping(Constants.API_URL + ArtistController.URL)
|
||||
public class ArtistController {
|
||||
|
||||
private final Map<Integer, ArtistDto> artists = new LinkedHashMap<>();
|
||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
||||
public static final String URL = "/artists";
|
||||
private final ArtistService artistService;
|
||||
|
||||
public ArtistController() {}
|
||||
|
||||
@GetMapping("/artists")
|
||||
public List<ArtistDto> getAllArtists() {
|
||||
return new ArrayList<>(artists.values());
|
||||
public ArtistController(ArtistService artistService) {
|
||||
this.artistService = artistService;
|
||||
}
|
||||
|
||||
@GetMapping("/artists/{id}")
|
||||
public ArtistDto getArtistById(@PathVariable Integer id) {
|
||||
return artists.get(id);
|
||||
@GetMapping
|
||||
public List<ArtistRs> getAll() {
|
||||
return artistService.getAll();
|
||||
}
|
||||
|
||||
@PostMapping("/artists")
|
||||
public ArtistDto createArtist(@RequestBody ArtistDto artist) {
|
||||
Integer newId = idCounter.getAndIncrement();
|
||||
artist.setId(newId);
|
||||
artists.put(newId, artist);
|
||||
return artist;
|
||||
@GetMapping("/{id}")
|
||||
public ArtistRs get(@PathVariable Long id) {
|
||||
return artistService.get(id);
|
||||
}
|
||||
|
||||
@PutMapping("/artists/{id}")
|
||||
public ArtistDto updateArtist(@PathVariable Integer id, @RequestBody ArtistDto artist) {
|
||||
if (artists.containsKey(id)) {
|
||||
artist.setId(id);
|
||||
artists.put(id, artist);
|
||||
return artist;
|
||||
}
|
||||
return null;
|
||||
@PostMapping
|
||||
public ArtistRs create(@RequestBody @Valid ArtistRq dto) {
|
||||
return artistService.create(dto);
|
||||
}
|
||||
|
||||
@PatchMapping("/artists/{id}")
|
||||
public ArtistDto patchArtist(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
||||
ArtistDto existingArtist = artists.get(id);
|
||||
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;
|
||||
@PutMapping("/{id}")
|
||||
public ArtistRs update(@PathVariable Long id, @RequestBody @Valid ArtistRq dto) {
|
||||
return artistService.update(id, dto);
|
||||
}
|
||||
|
||||
@DeleteMapping("/artists/{id}")
|
||||
public boolean deleteArtist(@PathVariable Integer id) {
|
||||
return artists.remove(id) != null;
|
||||
@DeleteMapping("/{id}")
|
||||
public ArtistRs delete(@PathVariable Long id) {
|
||||
return artistService.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +1,54 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.dto.CountryDto;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.CountryRq;
|
||||
import com.example.demo.dto.CountryRs;
|
||||
import com.example.demo.service.CountryService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@CrossOrigin(origins = "*")
|
||||
@RequestMapping(Constants.API_URL + CountryController.URL)
|
||||
public class CountryController {
|
||||
|
||||
private final Map<Integer, CountryDto> countries = new LinkedHashMap<>();
|
||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
||||
public static final String URL = "/countries";
|
||||
private final CountryService countryService;
|
||||
|
||||
public CountryController() {
|
||||
|
||||
countries.put(1, new CountryDto(1, "Россия"));
|
||||
countries.put(2, new CountryDto(2, "США"));
|
||||
countries.put(3, new CountryDto(3, "Тайга"));
|
||||
public CountryController(CountryService countryService) {
|
||||
this.countryService = countryService;
|
||||
}
|
||||
|
||||
@GetMapping("/countries")
|
||||
public List<CountryDto> getAllCountries() {
|
||||
return new ArrayList<>(countries.values());
|
||||
@GetMapping
|
||||
public List<CountryRs> getAll() {
|
||||
return countryService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/countries/{id}")
|
||||
public CountryDto getCountryById(@PathVariable Integer id) {
|
||||
return countries.get(id);
|
||||
@GetMapping("/{id}")
|
||||
public CountryRs get(@PathVariable Long id) {
|
||||
return countryService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping("/countries")
|
||||
public CountryDto createCountry(@RequestBody CountryDto country) {
|
||||
Integer newId = idCounter.getAndIncrement();
|
||||
country.setId(newId);
|
||||
countries.put(newId, country);
|
||||
return country;
|
||||
@PostMapping
|
||||
public CountryRs create(@RequestBody @Valid CountryRq dto) {
|
||||
return countryService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/countries/{id}")
|
||||
public CountryDto updateCountry(@PathVariable Integer id, @RequestBody CountryDto country) {
|
||||
if (countries.containsKey(id)) {
|
||||
country.setId(id);
|
||||
countries.put(id, country);
|
||||
return country;
|
||||
}
|
||||
return null;
|
||||
@PutMapping("/{id}")
|
||||
public CountryRs update(@PathVariable Long id, @RequestBody @Valid CountryRq dto) {
|
||||
return countryService.update(id, dto);
|
||||
}
|
||||
|
||||
@PatchMapping("/countries/{id}")
|
||||
public CountryDto patchCountry(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
||||
CountryDto existingCountry = countries.get(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;
|
||||
@DeleteMapping("/{id}")
|
||||
public CountryRs delete(@PathVariable Long id) {
|
||||
return countryService.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +1,54 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.dto.EpochDto;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import com.example.demo.configuration.Constants;
|
||||
import com.example.demo.dto.EpochRq;
|
||||
import com.example.demo.dto.EpochRs;
|
||||
import com.example.demo.service.EpochService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@CrossOrigin(origins = "*")
|
||||
@RequestMapping(Constants.API_URL + EpochController.URL)
|
||||
public class EpochController {
|
||||
|
||||
private final Map<Integer, EpochDto> epochs = new LinkedHashMap<>();
|
||||
private final AtomicInteger idCounter = new AtomicInteger(1);
|
||||
public static final String URL = "/epochs";
|
||||
private final EpochService epochService;
|
||||
|
||||
public EpochController() {
|
||||
epochs.put(1, new EpochDto(1, "1980-е"));
|
||||
epochs.put(2, new EpochDto(2, "1990-е"));
|
||||
public EpochController(EpochService epochService) {
|
||||
this.epochService = epochService;
|
||||
}
|
||||
|
||||
@GetMapping("/epochs")
|
||||
public List<EpochDto> getAllEpochs() {
|
||||
return new ArrayList<>(epochs.values());
|
||||
@GetMapping
|
||||
public List<EpochRs> getAll() {
|
||||
return epochService.getAll();
|
||||
}
|
||||
|
||||
@GetMapping("/epochs/{id}")
|
||||
public EpochDto getEpochById(@PathVariable Integer id) {
|
||||
return epochs.get(id);
|
||||
@GetMapping("/{id}")
|
||||
public EpochRs get(@PathVariable Long id) {
|
||||
return epochService.get(id);
|
||||
}
|
||||
|
||||
@PostMapping("/epochs")
|
||||
public EpochDto createEpoch(@RequestBody EpochDto epoch) {
|
||||
Integer newId = idCounter.getAndIncrement();
|
||||
epoch.setId(newId);
|
||||
epochs.put(newId, epoch);
|
||||
return epoch;
|
||||
@PostMapping
|
||||
public EpochRs create(@RequestBody @Valid EpochRq dto) {
|
||||
return epochService.create(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/epochs/{id}")
|
||||
public EpochDto updateEpoch(@PathVariable Integer id, @RequestBody EpochDto epoch) {
|
||||
if (epochs.containsKey(id)) {
|
||||
epoch.setId(id);
|
||||
epochs.put(id, epoch);
|
||||
return epoch;
|
||||
}
|
||||
return null;
|
||||
@PutMapping("/{id}")
|
||||
public EpochRs update(@PathVariable Long id, @RequestBody @Valid EpochRq dto) {
|
||||
return epochService.update(id, dto);
|
||||
}
|
||||
|
||||
@PatchMapping("/epochs/{id}")
|
||||
public EpochDto patchEpoch(@PathVariable Integer id, @RequestBody Map<String, Object> updates) {
|
||||
EpochDto existingEpoch = epochs.get(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;
|
||||
@DeleteMapping("/{id}")
|
||||
public EpochRs delete(@PathVariable Long id) {
|
||||
return epochService.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
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({
|
||||
name: artist.name,
|
||||
description: artist.description,
|
||||
epochId: artist.epoch?.id || artist.epochId || '',
|
||||
countryId: artist.country?.id || artist.countryId || ''
|
||||
epochId: artist.epoch?.id || '',
|
||||
countryId: artist.country?.id || ''
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
@@ -45,8 +45,8 @@ const ArtistForm = ({ countries = [], epochs = [], onSubmit, artist }) => {
|
||||
onSubmit({
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim(),
|
||||
epochId: parseInt(formData.epochId),
|
||||
countryId: parseInt(formData.countryId)
|
||||
epochId: Number(formData.epochId),
|
||||
countryId: Number(formData.countryId)
|
||||
});
|
||||
if (!artist) {
|
||||
setFormData({
|
||||
|
||||
@@ -25,14 +25,8 @@ const PunkRockPage = () => {
|
||||
console.log('Fetched Countries:', countriesData);
|
||||
console.log('Fetched Epochs:', epochsData);
|
||||
setArtists(artistsData);
|
||||
setCountries(countriesData.map(country => ({
|
||||
...country,
|
||||
id: parseInt(country.id)
|
||||
})));
|
||||
setEpochs(epochsData.map(epoch => ({
|
||||
...epoch,
|
||||
id: parseInt(epoch.id)
|
||||
})));
|
||||
setCountries(countriesData);
|
||||
setEpochs(epochsData);
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
@@ -54,12 +48,7 @@ const PunkRockPage = () => {
|
||||
const newArtist = await createArtist(artistData);
|
||||
console.log('Added Artist:', newArtist);
|
||||
setArtists(prevArtists => {
|
||||
const enrichedArtist = {
|
||||
...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) =>
|
||||
return [...prevArtists, newArtist].sort((a, b) =>
|
||||
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);
|
||||
console.log('Updated Artist:', updatedArtist);
|
||||
setArtists(prevArtists => {
|
||||
const enrichedArtist = {
|
||||
...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))
|
||||
return prevArtists.map(a => (a.id === updatedArtist.id ? updatedArtist : a))
|
||||
.sort((a, b) =>
|
||||
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>;
|
||||
}
|
||||
|
||||
const enrichedArtists = artists.map(artist => ({
|
||||
...artist,
|
||||
epoch: epochs.find(epoch => epoch.id === parseInt(artist.epochId)),
|
||||
country: countries.find(country => country.id === parseInt(artist.countryId))
|
||||
})).sort((a, b) =>
|
||||
const sortedArtists = artists.sort((a, b) =>
|
||||
sortDirection === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||
);
|
||||
|
||||
@@ -149,7 +129,7 @@ const PunkRockPage = () => {
|
||||
Сортировать {sortDirection === 'asc' ? 'А-Я' : 'Я-А'}
|
||||
</button>
|
||||
<ArtistList
|
||||
artists={enrichedArtists}
|
||||
artists={sortedArtists}
|
||||
onEdit={setEditingArtist}
|
||||
onDelete={handleDeleteArtist}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const createArtist = async (artist) => {
|
||||
|
||||
export const updateArtist = async (id, artist) => {
|
||||
const response = await fetch(`${API_URL}/artists/${id}`, {
|
||||
method: 'PATCH',
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
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