слава богу она сделалась

This commit is contained in:
maxim
2025-11-12 18:29:47 +04:00
parent 17014bcb67
commit c7dc979193
150 changed files with 10632 additions and 211 deletions

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
package com.example.demo.repository;
import org.springframework.stereotype.Repository;
import com.example.demo.entity.ArtistEntity;
@Repository
public class ArtistRepository extends MapRepository<ArtistEntity> {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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
View 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"
}
]
}

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

View File

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

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

View File

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

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

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

View 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

File diff suppressed because it is too large Load Diff

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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

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

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

View File

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

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

View 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)) || [];

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

View File

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

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

View File

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

View 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>
&nbsp;
<span>{toast.text}</span>
</Dropdown.Item>
);
});
return (
<Dropdown>
<Dropdown.Toggle className="toast-toggle" variant={variant}>
{icon}
</Dropdown.Toggle>
<Dropdown.Menu className="toast-menu" align="end">
{content}
<Dropdown.Item
disabled={isEmpty}
className="toast-menu-item text-center text-body-tertiary"
onClick={handleDeleteAll}
>
Очистить
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
};

View File

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

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

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

View 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%);
}

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

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

View File

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

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

View 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