Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/gradle/wrapper/
|
||||||
|
/build/
|
||||||
|
/.idea/
|
||||||
|
/.gradle/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 droideparanoico
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Album catalog web application
|
||||||
|
**Spring Boot** application using **Thymeleaf** view to build an album catalog capable of:
|
||||||
|
|
||||||
|
- Create a new artist
|
||||||
|
- Delete an artist
|
||||||
|
- Add a new album to an artist
|
||||||
|
- Delete an album from a artist
|
||||||
|
|
||||||
|
# Running the application
|
||||||
|
Download the project and extract the contents to a folder.
|
||||||
|
Once Java is installed, please run the following command to start the application:
|
||||||
|
|
||||||
|
**For Linux/Macintosh**
|
||||||
|
```
|
||||||
|
cd <extracted-folder>
|
||||||
|
./gradlew clean build bootRun
|
||||||
|
```
|
||||||
|
**For Windows**
|
||||||
|
```
|
||||||
|
cd <extracted-folder>
|
||||||
|
gradlew clean build bootRun
|
||||||
|
```
|
||||||
|
For deploying the application onto another web server, we can build the project using the following command and host the `albumcatalog-0.0.1-SNAPSHOT.war` that gets generated after building under `build/libs`.
|
||||||
|
|
||||||
|
**For Linux/Macintosh**
|
||||||
|
```
|
||||||
|
cd <extracted-folder>
|
||||||
|
./gradlew clean build
|
||||||
|
```
|
||||||
|
**For Windows**
|
||||||
|
```
|
||||||
|
cd <extracted-folder>
|
||||||
|
gradlew clean build
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the application will run on `8080`, which can be changed using the property `server.port` in `src/main/resources/application.properties`. To access the application UI, open [http://localhost:8080](http://localhost:8080/) on any browser.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The following form will be displayed to add a new artist when clicking on the “+” button at the bottom:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The following form will be displayed to add an album to an artist when clicking on the “+” button next to the album:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Using Swagger
|
||||||
|
To access Swagger UI, click on the Swagger icon at the top right corner. The following page will be opened where we can try out and execute the APIs.
|
||||||
|
|
||||||
|

|
||||||
53
STRUCTURE.md
Normal file
53
STRUCTURE.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Project structure and files
|
||||||
|
|
||||||
|
- **build.gradle** – This contains the dependencies needed for the project.
|
||||||
|
- **src/main**
|
||||||
|
* **java**
|
||||||
|
* ```com.droideparanoico.albumcatalog```
|
||||||
|
* **ServletInitializer.java** – this class extends SpringBootServletInitializer to make it eligible to deploy as a traditional WAR application.
|
||||||
|
* **SpringBootAlbumCatalog.java** – This class is annotated with @SpringBootApplication which does the auto-configuration of the application.
|
||||||
|
* ```com.droideparanoico.albumcatalog.config```
|
||||||
|
* **SwaggerConfiguration.java** – This is a @Configuration annotated class that contains the beans that initialize Swagger for this application.
|
||||||
|
* ```com.droideparanoico.albumcatalog.controller```
|
||||||
|
* **ArtistController.java** – This is a @Controller annotated classes that contains the request handler method for the following endpoints:
|
||||||
|
GET / – The request handler method listArtistAndAlbums(Model) is called when we land on the root application.
|
||||||
|
* ```com.droideparanoico.albumcatalog.controller.data```
|
||||||
|
* **ArtistRestController.java** – This is a @RestController annotated class that contains the request handler method for the following endpoints:
|
||||||
|
|
||||||
|
|HTTP Method|Endpoint|Request Handling Method|Description|
|
||||||
|
|:---:|:---:|:---:|---|
|
||||||
|
|```GET```|```/artist/```|```root()```|returns **“application is working!”** when called|
|
||||||
|
|```GET```|```/artist/all```|```getAllArtists()```|returns ```Iterable<Artist>``` when called|
|
||||||
|
|```GET```|```/artist/{id}```|```getArtistById(BigInteger)```|returns the ```Artist``` with the given id when called|
|
||||||
|
|```POST```|```/artist/{name}```|```createArtist(String)```|for creating a new **Artist** with the given name and returns the same after creation|
|
||||||
|
|```DELETE```|```/artist/{id}```|```deleteArtist(BigInteger)```|for deleting an **Artist** by id|
|
||||||
|
|```GET```|```/artist/{id}/albums```|```getAlbumsByArtist(BigInteger)```|for fetching all the albums of the **Artist** with the given id|
|
||||||
|
|```DELETE```|```/artist/{id}/albums```|```deleteAllAlbumsFromArtist(BigInteger)```|for deleting all the albums of the **Artist** with the given id|
|
||||||
|
|```POST```|```/artist/{id}/add```|```addAlbumToArtist(BigInteger, Album)```|for adding the given **Album** to the **Artist** with the given id and returns the created album|
|
||||||
|
|```GET```|```/artist/albums```|```getAllAlbums()```|returns ```List<Album>``` that fetches all the albums from all the artists|
|
||||||
|
|```PUT```|```/artist/albums/{id}/move```|```moveAlbumToDifferentArtist(BigInteger, BigInteger)```|for moving an album from one artist to another|
|
||||||
|
|```DELETE```|```/artist/{id}/albums/{album_id}```|```deleteFromArtist(BigInteger, BigInteger)```|for deleting an album from an artist|
|
||||||
|
|
||||||
|
> **ArtistRestController** uses **ArtistService** for business logic and interacting with persistence layer.
|
||||||
|
* ```com.droideparanoico.albumcatalog.database```
|
||||||
|
* **ArtistRepository.java** – this interface extends JpaRepository for interacting with artist database table.
|
||||||
|
* **AlbumsRepository.java** – this interface extends JpaRepository for interacting with album database table.
|
||||||
|
* ```com.droideparanoico.albumcatalog.exception```
|
||||||
|
* **ExceptionController.java** – this class is annotated with @ControllerAdvice and contains @ExceptionHandler annotated method to handle exceptions.
|
||||||
|
* **ArtistNotFoundException.java** – a custom exception class for managing an invalid artist.
|
||||||
|
* **AlbumNotFoundException.java** – a custom exception class for managing an invalid album.
|
||||||
|
* ```com.droideparanoico.albumcatalog.model```
|
||||||
|
* **ErrorCodes.java** – and enum that holds the error codes associated with our custom exceptions.
|
||||||
|
* **Artist.java** – a POJO class for saving the artist information.
|
||||||
|
* **Album.java** – a POJO class for saving the album information.
|
||||||
|
* ```com.droideparanoico.albumcatalog.service```
|
||||||
|
* **ArtistService.java** – a Business Facade Service that controllers use to interact with the database repositories and contains the business logic to manipulate the data that is transferred between the controller and persistence layer.
|
||||||
|
* resources
|
||||||
|
* ```templates```
|
||||||
|
* **index.html** – the HTML template with embedded Thymeleaf constructs for rendering the server-side response.
|
||||||
|
* ```static```
|
||||||
|
* **custom.css** – a cascading stylesheet for our application.
|
||||||
|
* **scripts.js** – a Javascript file containing the method to interact with our backend RESTful APIs that we have built.
|
||||||
|
* **application.properties** – contains the Spring Boot configurations.
|
||||||
|
* **application-dev.properties** – contains the Spring Boot configurations for ```dev``` profile.
|
||||||
|
* **data.sql** – initializing database script that is executed at the time of application boot up.
|
||||||
48
build.gradle
Normal file
48
build.gradle
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
plugins {
|
||||||
|
id 'org.springframework.boot' version '2.3.5.RELEASE'
|
||||||
|
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
|
||||||
|
id 'java'
|
||||||
|
id 'war'
|
||||||
|
id 'eclipse'
|
||||||
|
id 'idea'
|
||||||
|
}
|
||||||
|
|
||||||
|
group = 'com.droideparanoico'
|
||||||
|
version = '0.0.1-SNAPSHOT'
|
||||||
|
sourceCompatibility = '1.8'
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom annotationProcessor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
|
implementation 'com.google.guava:guava:30.0-jre'
|
||||||
|
implementation 'io.springfox:springfox-spring-webmvc:2.10.5'
|
||||||
|
implementation 'io.springfox:springfox-swagger2:2.10.5'
|
||||||
|
implementation 'io.springfox:springfox-swagger-ui:2.10.5'
|
||||||
|
|
||||||
|
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml'
|
||||||
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
runtimeOnly 'com.h2database:h2'
|
||||||
|
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
|
||||||
|
|
||||||
|
testImplementation('org.springframework.boot:spring-boot-starter-test') {
|
||||||
|
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'albumcatalog'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.droideparanoico.albumcatalog;
|
||||||
|
|
||||||
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||||
|
|
||||||
|
public class ServletInitializer extends SpringBootServletInitializer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||||
|
return application.sources(SpringBootAlbumCatalog.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.droideparanoico.albumcatalog;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class SpringBootAlbumCatalog {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(SpringBootAlbumCatalog.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
import springfox.documentation.builders.PathSelectors;
|
||||||
|
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||||
|
import springfox.documentation.spi.DocumentationType;
|
||||||
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
|
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableSwagger2WebMvc
|
||||||
|
public class SwaggerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Docket api() {
|
||||||
|
return new Docket(DocumentationType.SWAGGER_2)
|
||||||
|
.select()
|
||||||
|
.apis(RequestHandlerSelectors.any())
|
||||||
|
.paths(PathSelectors.any())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcConfigurer configureSwagger() {
|
||||||
|
|
||||||
|
return new WebMvcConfigurer() {
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("swagger-ui.html")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/");
|
||||||
|
|
||||||
|
registry.addResourceHandler("/webjars/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.controller;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.service.ArtistService;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class ArtistController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArtistService artistService;
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String listArtistAndAlbums(Model model) {
|
||||||
|
model.addAttribute("artists", artistService.getAllArtists());
|
||||||
|
return "index";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.controller.data;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.model.Artist;
|
||||||
|
import com.droideparanoico.albumcatalog.model.Album;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.service.ArtistService;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/artist")
|
||||||
|
public class ArtistRestController {
|
||||||
|
|
||||||
|
public ArtistService service;
|
||||||
|
|
||||||
|
@Qualifier("artistService")
|
||||||
|
@Autowired
|
||||||
|
public void setService(ArtistService service) {
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
public String root() {
|
||||||
|
return "application is runnning!";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/all")
|
||||||
|
public Iterable<Artist> getAllArtists() {
|
||||||
|
return service.getAllArtists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public Artist getArtistById(final @PathVariable("id") BigInteger artistId) {
|
||||||
|
return service.getAlbumsById(artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{name}")
|
||||||
|
public Optional<Artist> createArtist(final @PathVariable String name) {
|
||||||
|
return service.createArtist(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public void deleteArtist(final @PathVariable("id") BigInteger artistId) {
|
||||||
|
service.deleteArtist(artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/albums")
|
||||||
|
public Iterable<Album> getAlbumsByArtist(@PathVariable("id") BigInteger artistId) {
|
||||||
|
return service.getAlbums(artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/albums")
|
||||||
|
public void deleteAllAlbumsFromArtist(final @PathVariable("id") BigInteger artistId) {
|
||||||
|
service.deleteAlbums(artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/add")
|
||||||
|
public Album addAlbumToArtist(final @PathVariable("id") BigInteger artistId,
|
||||||
|
final @RequestBody Album album) {
|
||||||
|
return service.addAlbum(artistId, album);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/albums")
|
||||||
|
public Iterable<Album> getAllAlbums() {
|
||||||
|
return service.getAlbums(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/albums/{id}/move")
|
||||||
|
public boolean moveAlbumToDifferentArtist(@PathVariable("id") BigInteger albumId,
|
||||||
|
@RequestParam("to_artist") BigInteger toArtistId) {
|
||||||
|
return service.moveAlbum(albumId, toArtistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/albums/{album_id}")
|
||||||
|
public void deleteFromArtist(final @PathVariable("id") BigInteger artistId,
|
||||||
|
final @PathVariable("album_id") BigInteger albumId) {
|
||||||
|
service.deleteAlbum(artistId, albumId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.database;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.model.Album;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Modifying;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
public interface AlbumsRepository extends JpaRepository<Album, BigInteger> {
|
||||||
|
|
||||||
|
public Optional<Album> findByName(String name);
|
||||||
|
|
||||||
|
Collection<Album> findByArtistId(BigInteger artistId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Modifying(clearAutomatically = true)
|
||||||
|
@Query("delete from Album s where s.artistId = ?1")
|
||||||
|
void deleteByArtistId(BigInteger artistId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Modifying(clearAutomatically = true)
|
||||||
|
@Query("delete from Album s where s.artistId = ?1 and s.id = ?2")
|
||||||
|
public void delete(BigInteger artistId, BigInteger albumId);
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Modifying(clearAutomatically = true)
|
||||||
|
@Query("update Album s set s.artistId = ?2 where s.id = ?1")
|
||||||
|
public int updateArtist(BigInteger albumId, BigInteger artistId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.database;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.model.Artist;
|
||||||
|
import com.droideparanoico.albumcatalog.model.Album;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
|
||||||
|
public interface ArtistRepository extends JpaRepository<Artist, BigInteger> {
|
||||||
|
|
||||||
|
public Optional<Artist> findByName(String name);
|
||||||
|
|
||||||
|
@Query("select a from Album a where a.artistId = ?1")
|
||||||
|
public Collection<Album> getAlbums(BigInteger artistId);
|
||||||
|
|
||||||
|
@Query(value = "select name from Album where artist_id = ?", nativeQuery = true)
|
||||||
|
public List<String> getAlbumsUsingNativeQuery(BigInteger albumId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.exception;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class AlbumNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public AlbumNotFoundException(final BigInteger id) {
|
||||||
|
super(String.format("album with id '%s' not found", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.exception;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
@SuppressWarnings("serial")
|
||||||
|
public class ArtistNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
public ArtistNotFoundException(final BigInteger id) {
|
||||||
|
super(String.format("album with id '%s' not found", id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.exception;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.model.ErrorCodes;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class ExceptionController {
|
||||||
|
|
||||||
|
@ExceptionHandler
|
||||||
|
public ResponseEntity<?> artistNotFound(ArtistNotFoundException ex) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseStatusError(ErrorCodes.ARTIST_NOT_FOUND.code(), ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler
|
||||||
|
public ResponseEntity<?> albumNotFound(AlbumNotFoundException ex) {
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(new ResponseStatusError(ErrorCodes.ALBUM_NOT_FOUND.code(), ex.getMessage()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
class ResponseStatusError {
|
||||||
|
|
||||||
|
private int status;
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.model;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.NamedNativeQuery;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "album")
|
||||||
|
@NamedNativeQuery(name = "albumsByArtistId", query = "select id, name, artist_id, cover_url, created_on from album a where a.artist_id = ?", resultClass = Album.class)
|
||||||
|
public class Album {
|
||||||
|
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
@Id
|
||||||
|
private BigInteger id;
|
||||||
|
|
||||||
|
@Column(name = "artist_id")
|
||||||
|
@JsonProperty("artist_id")
|
||||||
|
private BigInteger artistId;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "cover_url")
|
||||||
|
@JsonProperty("cover_url")
|
||||||
|
private String coverUrl;
|
||||||
|
|
||||||
|
@Column(name = "created_on")
|
||||||
|
@JsonProperty("created_on")
|
||||||
|
private Date createdOn;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.model;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.persistence.CascadeType;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.ElementCollection;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.OneToMany;
|
||||||
|
import javax.persistence.Table;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "artist")
|
||||||
|
public class Artist {
|
||||||
|
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
@Id
|
||||||
|
private BigInteger id;
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Column(name = "created_on")
|
||||||
|
@JsonProperty("created_on")
|
||||||
|
private Date createdOn;
|
||||||
|
|
||||||
|
@ElementCollection(targetClass = java.util.HashSet.class)
|
||||||
|
@OneToMany(cascade = CascadeType.ALL)
|
||||||
|
@JoinColumn
|
||||||
|
private Collection<Album> albums;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.model;
|
||||||
|
|
||||||
|
public enum ErrorCodes {
|
||||||
|
|
||||||
|
ARTIST_NOT_FOUND(1001),
|
||||||
|
ALBUM_NOT_FOUND(1002);
|
||||||
|
|
||||||
|
private int code;
|
||||||
|
|
||||||
|
ErrorCodes(int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int code() {
|
||||||
|
return this.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.droideparanoico.albumcatalog.service;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.database.AlbumsRepository;
|
||||||
|
import com.droideparanoico.albumcatalog.exception.ArtistNotFoundException;
|
||||||
|
import com.droideparanoico.albumcatalog.exception.AlbumNotFoundException;
|
||||||
|
import com.droideparanoico.albumcatalog.model.Album;
|
||||||
|
import com.droideparanoico.albumcatalog.model.Artist;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.droideparanoico.albumcatalog.database.ArtistRepository;
|
||||||
|
|
||||||
|
@Service("artistService")
|
||||||
|
public class ArtistService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ArtistRepository artistRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AlbumsRepository albumsRepo;
|
||||||
|
|
||||||
|
public Iterable<Artist> getAllArtists() {
|
||||||
|
return artistRepo.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Artist getAlbumsById(BigInteger artistId) {
|
||||||
|
return getArtist(artistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<Artist> createArtist(String name) {
|
||||||
|
Artist artist = new Artist();
|
||||||
|
artist.setName(name);
|
||||||
|
artist.setCreatedOn(new Date());
|
||||||
|
return Optional.of(artistRepo.save(artist));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteArtist(BigInteger artistId) {
|
||||||
|
Artist artist = getArtist(artistId);
|
||||||
|
artist.setId(artistId);
|
||||||
|
artistRepo.delete(artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Iterable<Album> getAlbums(BigInteger artistId) {
|
||||||
|
if (artistId == null) {
|
||||||
|
return albumsRepo.findAll();
|
||||||
|
}
|
||||||
|
artistRepo.getAlbums(artistId);
|
||||||
|
Artist artist = getArtist(artistId);
|
||||||
|
return artistRepo.getAlbums(artist.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAlbums(BigInteger artistId) {
|
||||||
|
Artist artist = getArtist(artistId);
|
||||||
|
albumsRepo.deleteByArtistId(artist.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Album addAlbum(BigInteger artistId, Album album) {
|
||||||
|
Artist artist = getArtist(artistId);
|
||||||
|
album.setArtistId(artist.getId());
|
||||||
|
album.setCreatedOn(new Date());
|
||||||
|
return albumsRepo.save(album);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean moveAlbum(BigInteger albumId, BigInteger toArtistId) {
|
||||||
|
Album album = getAlbum(albumId);
|
||||||
|
Artist artist = getArtist(toArtistId);
|
||||||
|
return 1 == albumsRepo.updateArtist(album.getId(), artist.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAlbum(BigInteger artistId, BigInteger albumId) {
|
||||||
|
Album album = getAlbum(albumId);
|
||||||
|
albumsRepo.delete(artistId, album.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Artist getArtist(final BigInteger artistId) {
|
||||||
|
return artistRepo.findById(artistId)
|
||||||
|
.orElseThrow(() -> new ArtistNotFoundException(artistId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Album getAlbum(final BigInteger albumId) {
|
||||||
|
return albumsRepo.findById(albumId).orElseThrow(() -> new AlbumNotFoundException(albumId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
2
src/main/resources/application-dev.properties
Normal file
2
src/main/resources/application-dev.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
server.port = 9090
|
||||||
|
spring.h2.console.enabled = false
|
||||||
2
src/main/resources/application.properties
Normal file
2
src/main/resources/application.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
server.port=8080
|
||||||
|
spring.h2.console.enabled = false
|
||||||
5
src/main/resources/data.sql
Normal file
5
src/main/resources/data.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
create table if not exists artist(id bigint auto_increment primary key, name VARCHAR(250) NOT NULL, created_on DATE default CURRENT_TIMESTAMP );
|
||||||
|
create table if not exists album(id bigint auto_increment PRIMARY KEY, album_id bigint NOT NULL, name VARCHAR(250) NOT NULL, cover_url VARCHAR(250) NOT NULL, created_on DATE default CURRENT_TIMESTAMP, FOREIGN KEY(artist_id) REFERENCES artist(id) ON UPDATE CASCADE);
|
||||||
|
|
||||||
|
insert into artist(id, name, created_on) values(1, 'Radiohead', CURRENT_TIMESTAMP);
|
||||||
|
insert into album(id, name, artist_id, cover_url, created_on) values(2, 'OK Computer', 1, 'https://upload.wikimedia.org/wikipedia/en/b/ba/Radioheadokcomputer.png', CURRENT_TIMESTAMP);
|
||||||
BIN
src/main/resources/static/app_screen_1.png
Normal file
BIN
src/main/resources/static/app_screen_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
src/main/resources/static/app_screen_2.png
Normal file
BIN
src/main/resources/static/app_screen_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
BIN
src/main/resources/static/app_screen_3.png
Normal file
BIN
src/main/resources/static/app_screen_3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/main/resources/static/app_screen_4.png
Normal file
BIN
src/main/resources/static/app_screen_4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
55
src/main/resources/static/custom.css
Normal file
55
src/main/resources/static/custom.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-3x {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
margin: 2em
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
font-weight: bolder;
|
||||||
|
text-align: center
|
||||||
|
}
|
||||||
|
|
||||||
|
.table>tbody>tr>td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.royal_blue {
|
||||||
|
background-color: #0074B7;
|
||||||
|
color: white;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fa-trash:hover {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.fa-plus:hover {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-url {
|
||||||
|
max-width: 200px;
|
||||||
|
max-height: 120px;
|
||||||
|
box-shadow: 2px 2px 1px rgba(50, 50, 50, 0.75);
|
||||||
|
border: 1px solid grey;
|
||||||
|
border-radius: 5px 5px 5px 5px;
|
||||||
|
}
|
||||||
BIN
src/main/resources/static/favicon.png
Normal file
BIN
src/main/resources/static/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
101
src/main/resources/static/scripts.js
Normal file
101
src/main/resources/static/scripts.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
$(document).ready(function () {
|
||||||
|
$("#artist > tbody").append(
|
||||||
|
'<tr style="background-color: #0074B7" class="royal_blue"><td colspan="3"><span data-toggle="modal" data-target="#add-artist" style="color: white" title="Create artist" class="fa fa-plus fa-3x" aria-hidden="true"> </span></td></tr>'
|
||||||
|
);
|
||||||
|
|
||||||
|
function getId(obj, prefix) {
|
||||||
|
return $(obj)
|
||||||
|
.attr("class")
|
||||||
|
.match(prefix + "-[0-9]+")[0]
|
||||||
|
.split("-")[1]
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(".remove-album").on("click", function (evt) {
|
||||||
|
var result = confirm("Sure you want to delete?");
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let albumId = getId(this, "album");
|
||||||
|
let artistId = getId(this, "artist");
|
||||||
|
$.ajax({
|
||||||
|
url: `artist/${artistId}/albums/${albumId}`,
|
||||||
|
type: "DELETE",
|
||||||
|
success: function (data) {
|
||||||
|
alert("successfully deleted the album from artist");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$(".delete_artist").on("click", function (evt) {
|
||||||
|
var result = confirm("Sure you want to delete?");
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let artistId = getId(this, "artist");
|
||||||
|
$.ajax({
|
||||||
|
url: `artist/${artistId}`,
|
||||||
|
type: "DELETE",
|
||||||
|
success: function (data) {
|
||||||
|
alert("successfully deleted the artist");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$("form.add-artist").on("submit", function (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
let name = $("#artist-name").val().trim();
|
||||||
|
|
||||||
|
if (name == null || name.length == 0 || name.length < 3) {
|
||||||
|
alert("name cannot be null/empty/length less than 3");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$.ajax({
|
||||||
|
url: `artist/${name}`,
|
||||||
|
type: "POST",
|
||||||
|
success: function (data) {
|
||||||
|
alert("successfully created the artist");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
complete: function () {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$("span[data-id]").on("click", function (evt) {
|
||||||
|
$("#artist-id").val($(this).attr("data-id"));
|
||||||
|
});
|
||||||
|
|
||||||
|
$("form.add-album").on("submit", function (evt) {
|
||||||
|
evt.preventDefault();
|
||||||
|
let name = $("#album-name").val().trim();
|
||||||
|
if (name == null || name.length == 0 || name.length < 3) {
|
||||||
|
alert("name cannot be null/empty/length less than 3");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let url = $("#cover-url").val().trim();
|
||||||
|
let match = url.match("http.*(jpg|jpeg|gif|png)");
|
||||||
|
if (match == null || match < 0) {
|
||||||
|
alert("enter valid image url");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let artistId = $("#artist-id").val().split("-")[1].trim();
|
||||||
|
$.ajax({
|
||||||
|
url: `artist/${artistId}/add`,
|
||||||
|
dataType: "json",
|
||||||
|
contentType: "application/json",
|
||||||
|
type: "POST",
|
||||||
|
data: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
cover_url: url,
|
||||||
|
}),
|
||||||
|
success: function (data) {
|
||||||
|
alert("successfully added album to artist");
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$("#artist-id").val("");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
148
src/main/resources/templates/index.html
Normal file
148
src/main/resources/templates/index.html
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Album catalog app</title>
|
||||||
|
<link rel="shortcut icon" th:href="@{/favicon.png}" type="image/x-icon">
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"/>
|
||||||
|
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
|
||||||
|
<script
|
||||||
|
src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
|
||||||
|
<script th:src="@{/scripts.js}"></script>
|
||||||
|
<link rel="stylesheet" th:href="@{/custom.css}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="centered">
|
||||||
|
<div class="royal_blue" ; style="margin: 0.1em">
|
||||||
|
<span style="margin: 0.1em; float: center; font-size: 2em">
|
||||||
|
Album catalog application </span> <span
|
||||||
|
style="margin: 0.25em; float: right"> <a
|
||||||
|
title="Click to Open Swagger UI" href="swagger-ui.html"><img
|
||||||
|
width="30px" height="30px"
|
||||||
|
src="https://upload.wikimedia.org/wikipedia/commons/a/ab/Swagger-logo.png"/></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<table id="artist"
|
||||||
|
class="table table-striped table-bordered table-responsive">
|
||||||
|
<thead class="royal_blue" style="color: white; font-size: 1.5em;">
|
||||||
|
<tr>
|
||||||
|
<td>Artist</td>
|
||||||
|
<td>Albums</td>
|
||||||
|
<td style="width: 15%"></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="artist: ${artists}">
|
||||||
|
<td class="album_id" style="display: none"
|
||||||
|
th:text="${artist.id}"></td>
|
||||||
|
<td th:text="${artist.name}"></td>
|
||||||
|
<td>
|
||||||
|
<table
|
||||||
|
class="table table-striped table-bordered table-responsive">
|
||||||
|
<tr class="table" th:each="album: ${artist.albums}">
|
||||||
|
<td class="album_id" style="display: none" th:text="${album.id}"></td>
|
||||||
|
<td th:text="${album.name}"></td>
|
||||||
|
<td><img class="cover-url" th:src="${album.coverUrl}" />
|
||||||
|
<td><span title="Remove Album"
|
||||||
|
th:classappend="${'artist-' + artist.id + ' album-' + album.id}"
|
||||||
|
class="fa fa-trash fa-3x remove-album" aria-hidden="true">
|
||||||
|
</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div style="margin: 0.25em; display: inline;">
|
||||||
|
<span data-toggle="modal" data-target="#add-album"
|
||||||
|
th:attr="data-id='artist-' + ${artist.id}"
|
||||||
|
style="padding: 0 0.25em" title="Add Album"
|
||||||
|
class="fa fa-plus fa-3x" aria-hidden="true"></span> <span
|
||||||
|
data-toggle="modal" title="Delete Artist"
|
||||||
|
style="padding: 0 0.25em"
|
||||||
|
th:classappend="${'artist-' + artist.id}"
|
||||||
|
class="fa fa-trash fa-3x delete_artist" aria-hidden="true">
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="add-artist" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header centered royal_blue">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h4 style="color: white;" class="modal-title">Add Artist</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class='add-artist'>
|
||||||
|
<div class="form-group">
|
||||||
|
<table
|
||||||
|
class="table table-striped table-bordered table-responsive">
|
||||||
|
<tr>
|
||||||
|
<td><label class="form-check-label" for="artist-name">Name</label></td>
|
||||||
|
<td><input name="artist-name" id="artist-name"
|
||||||
|
class="form-control" type="text"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-primary btn-lg add-artist">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="add-album" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header centered royal_blue">
|
||||||
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
|
<h4 style="color: white;" class="modal-title">Add Album</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class='add-album'>
|
||||||
|
<div class="form-group">
|
||||||
|
<table
|
||||||
|
class="table table-striped table-bordered table-responsive">
|
||||||
|
<tr style="display: none">
|
||||||
|
<td><label class="form-check-label" for="artist-id">Name</label></td>
|
||||||
|
<td><input name="artist-id" id="artist-id"
|
||||||
|
class="form-control" type="hidden"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label class="form-check-label" for="album-name">Name</label></td>
|
||||||
|
<td><input name="album-name" id="album-name"
|
||||||
|
class="form-control" type="text"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label class="form-check-label" for="cover-url">Cover
|
||||||
|
URL</label></td>
|
||||||
|
<td><input name="cover-url" id="cover-url"
|
||||||
|
class="form-control" type="text"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg">Add</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user