Add attachments, more pages
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Tyler 2020-12-26 19:34:07 -05:00
parent d086b29bcc
commit a1ac81a73a
29 changed files with 2349 additions and 17899 deletions

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers;
use App\Models\Book;
use Illuminate\Http\Request;
class BookController extends Controller {
@ -40,7 +41,13 @@ class BookController extends Controller {
* @return \Illuminate\Http\Response
*/
public function show($id) {
//
$book = Book::find($id);
if (!$book) {
abort(404);
}
return view('books.show', [ 'book' => $book ]);
}
/**
@ -71,6 +78,16 @@ class BookController extends Controller {
* @return \Illuminate\Http\Response
*/
public function destroy($id) {
//
$book = Book::find($id);
if (!$book) {
abort(404);
}
$book->delete();
return response()->json([
'success' => true
]);
}
}

View File

@ -12,7 +12,7 @@ class LocationController extends Controller {
* @return \Illuminate\Http\Response
*/
public function index() {
//
return view('locations.index', [ 'locations' => Location::paginate(15) ]);
}
/**
@ -47,7 +47,7 @@ class LocationController extends Controller {
abort(404);
}
return view('location', [ 'location' => $location ]);
return view('locations.show', [ 'location' => $location, 'rows' => $location->books()->paginate(15) ]);
}
/**

View File

@ -2,20 +2,31 @@
namespace App\Http\Controllers;
use App\Models\Book;
use App\Services\BookInformation\BookLookupService;
use Cache;
use App\Services\BookInformation\GoogleBooks;
use Symfony\Component\HttpKernel\Exception\HttpException;
class LookupController {
public function lookup($isbn) {
if (!preg_match('/^(\d+)$/', $isbn)) {
throw new HttpException(400);
public function lookup(BookLookupService $service, $isbn) {
$result = $service->lookup($isbn);
$arr = [
'success' => false
];
if ($result) {
$arr = array_merge($arr, [
'success' => true,
'data' => $result
]);
}
return Cache::remember('isbn_' . $isbn, 86400, function() use ($isbn) {
$google_books = new GoogleBooks();
$book = Book::where('barcode', $isbn)->first();
return $google_books->lookup($isbn);
});
if ($book) {
$arr['warning'] = 'Item already exists.<br />Location: ' . $book->location->name;
}
return response()->json($arr);
}
}

View File

@ -5,6 +5,9 @@ namespace App\Http\Controllers;
use App\Models\Author;
use App\Models\Book;
use App\Models\Location;
use App\Services\BookInformation\BookLookupService;
use App\Services\BookInformation\GoogleBooks;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
@ -16,24 +19,21 @@ class MainController extends Controller {
}
public function search(Request $request) {
$this->validate($request, [
'query' => [ 'required' ]
]);
$rows = Book::where('name', 'LIKE', '%' . $request->get('query') . '%')->paginate(15);
$rows = Book::search($request->get('query'))->paginate(15);
return view('index', [ 'rows' => $rows ]);
}
public function add(Request $request) {
return view('add', [
'locations' => Location::get(),
'old' => array_filter($request->old('books', []), function($item) {
return !empty($item['barcode']);
})
]);
}
public function save(Request $request) {
public function save(Request $request, BookLookupService $service) {
$this->validate($request, [
'location' => [ 'required' ]
]);
@ -69,6 +69,39 @@ class MainController extends Controller {
$authors = array_map(function($author) { return $author->id; }, $authors);
$book->authors()->attach($authors);
// Lookup info from cache
$res = $service->lookup($item['barcode']);
if (!empty($res)) {
if ($thumbnail = data_get('images.thumbnail', $res)) {
$file = $this->downloadFile($thumbnail);
$book->attach('thumbnail', $file);
}
}
}
}
/**
* Download a file and return the local temp path.
*
* @param $url
* @return string|null
*/
private function downloadFile($url) {
$client = new Client();
$path = tempnam(storage_path('app/temp'), 'image');
$res = $client->get($url, [
'sink' => $path
]);
if ($res->getStatusCode() != 200) {
return null;
}
return $path;
}
}

View File

@ -3,12 +3,13 @@
namespace App\Models;
use App\Services\Search\BookConfigurator;
use Bnb\Laravel\Attachments\HasAttachment;
use Illuminate\Database\Eloquent\Model;
use ScoutElastic\Searchable;
class Book extends Model {
use Searchable;
use Searchable, HasAttachment;
protected $indexConfigurator = BookConfigurator::class;

View File

@ -14,4 +14,8 @@ class Location extends Model {
protected $fillable = [
'name',
];
public function books() {
return $this->hasMany(Book::class);
}
}

View File

@ -2,6 +2,9 @@
namespace App\Providers;
use App\Services\BookInformation\BookLookupService;
use App\Services\BookInformation\CachedService;
use App\Services\BookInformation\GoogleBooks;
use Illuminate\Pagination\Paginator;
use Schema;
use Illuminate\Support\ServiceProvider;
@ -23,6 +26,8 @@ class AppServiceProvider extends ServiceProvider {
* @return void
*/
public function register() {
//
$this->app->singleton(BookLookupService::class, function() {
return new CachedService(new GoogleBooks());
});
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Services\BookInformation;
use Cache;
class CachedService implements BookLookupService {
private $service;
public function __construct($service) {
$this->service = $service;
}
public function lookup($isbn) {
return Cache::remember('isbn_' . $isbn, 86400, function() use ($isbn) {
return $this->service->lookup($isbn);
});
}
}

View File

@ -22,9 +22,10 @@ class GoogleBooks implements BookLookupService {
*/
$volume = Arr::first($results->getItems())->getVolumeInfo();
return [
return (object) [
'title' => $volume->getTitle(),
'authors' => $volume->getAuthors()
'authors' => $volume->getAuthors(),
'images' => $volume->getImageLinks(),
];
}
}

View File

@ -7,6 +7,7 @@
"require": {
"php": "^7.3|^8.0",
"babenkoivan/scout-elasticsearch-driver": "^4.2",
"bnbwebexpertise/laravel-attachments": "^1.0",
"fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0",
"google/apiclient": "^2.2",

396
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "10a519557c1fd55683da44313588c40e",
"content-hash": "7921640c91682ddbd453b7e9bb3f8e2c",
"packages": [
{
"name": "asm89/stack-cors",
@ -119,6 +119,107 @@
],
"time": "2020-08-13T17:57:09+00:00"
},
{
"name": "bnbwebexpertise/laravel-attachments",
"version": "1.0.22",
"source": {
"type": "git",
"url": "https://github.com/bnbwebexpertise/laravel-attachments.git",
"reference": "d3f4aa024449ad938d564aede64d05e19bfc5ff5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bnbwebexpertise/laravel-attachments/zipball/d3f4aa024449ad938d564aede64d05e19bfc5ff5",
"reference": "d3f4aa024449ad938d564aede64d05e19bfc5ff5",
"shasum": ""
},
"require": {
"bnbwebexpertise/php-uuid": ">=0.0.2",
"doctrine/dbal": "~2.5",
"illuminate/console": ">=5.5",
"illuminate/database": ">=5.5",
"illuminate/encryption": ">=5.5",
"illuminate/routing": ">=5.5",
"illuminate/support": ">=5.5",
"nesbot/carbon": "^1.20 || ^2.0",
"php": ">=7.0"
},
"require-dev": {
"laravel/framework": ">=5.5"
},
"type": "package",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev",
"dev-L5.4": "0.x-dev"
},
"laravel": {
"providers": [
"Bnb\\Laravel\\Attachments\\AttachmentsServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Bnb\\Laravel\\Attachments\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "B&B Web Expertise",
"email": "support@bnb.re"
}
],
"description": "Attach files to your models, retrievable by key, group name or using the Eloquent relationship.",
"time": "2020-06-24T15:19:20+00:00"
},
{
"name": "bnbwebexpertise/php-uuid",
"version": "0.0.3",
"source": {
"type": "git",
"url": "https://github.com/bnbwebexpertise/php-uuid.git",
"reference": "d60bf8054db1d062f2fc79c43af0ff499a49fbfa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bnbwebexpertise/php-uuid/zipball/d60bf8054db1d062f2fc79c43af0ff499a49fbfa",
"reference": "d60bf8054db1d062f2fc79c43af0ff499a49fbfa",
"shasum": ""
},
"require": {
"ext-bcmath": "*",
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.0"
},
"type": "package",
"autoload": {
"psr-4": {
"Bnb\\Uuid\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "B&B Web Expertise",
"email": "support@bnb.re"
}
],
"description": "UUID helpers",
"time": "2017-11-18T13:18:41+00:00"
},
{
"name": "brick/math",
"version": "0.9.1",
@ -204,6 +305,299 @@
"description": "implementation of xdg base directory specification for php",
"time": "2019-12-04T15:06:13+00:00"
},
{
"name": "doctrine/cache",
"version": "1.10.2",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "13e3381b25847283a91948d04640543941309727"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727",
"reference": "13e3381b25847283a91948d04640543941309727",
"shasum": ""
},
"require": {
"php": "~7.1 || ^8.0"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"alcaeus/mongo-php-adapter": "^1.1",
"doctrine/coding-standard": "^6.0",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^7.0",
"predis/predis": "~1.0"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
}
],
"description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.",
"homepage": "https://www.doctrine-project.org/projects/cache.html",
"keywords": [
"abstraction",
"apcu",
"cache",
"caching",
"couchdb",
"memcached",
"php",
"redis",
"xcache"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache",
"type": "tidelift"
}
],
"time": "2020-07-07T18:54:01+00:00"
},
{
"name": "doctrine/dbal",
"version": "2.12.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "adce7a954a1c2f14f85e94aed90c8489af204086"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/adce7a954a1c2f14f85e94aed90c8489af204086",
"reference": "adce7a954a1c2f14f85e94aed90c8489af204086",
"shasum": ""
},
"require": {
"doctrine/cache": "^1.0",
"doctrine/event-manager": "^1.0",
"ext-pdo": "*",
"php": "^7.3 || ^8"
},
"require-dev": {
"doctrine/coding-standard": "^8.1",
"jetbrains/phpstorm-stubs": "^2019.1",
"phpstan/phpstan": "^0.12.40",
"phpunit/phpunit": "^9.4",
"psalm/plugin-phpunit": "^0.10.0",
"symfony/console": "^2.0.5|^3.0|^4.0|^5.0",
"vimeo/psalm": "^3.17.2"
},
"suggest": {
"symfony/console": "For helpful console commands such as SQL execution and import of files."
},
"bin": [
"bin/doctrine-dbal"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\DBAL\\": "lib/Doctrine/DBAL"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
}
],
"description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.",
"homepage": "https://www.doctrine-project.org/projects/dbal.html",
"keywords": [
"abstraction",
"database",
"db2",
"dbal",
"mariadb",
"mssql",
"mysql",
"oci8",
"oracle",
"pdo",
"pgsql",
"postgresql",
"queryobject",
"sasql",
"sql",
"sqlanywhere",
"sqlite",
"sqlserver",
"sqlsrv"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal",
"type": "tidelift"
}
],
"time": "2020-11-14T20:26:58+00:00"
},
{
"name": "doctrine/event-manager",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/event-manager.git",
"reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/event-manager/zipball/41370af6a30faa9dc0368c4a6814d596e81aba7f",
"reference": "41370af6a30faa9dc0368c4a6814d596e81aba7f",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"conflict": {
"doctrine/common": "<2.9@dev"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\": "lib/Doctrine/Common"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Johannes Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.",
"homepage": "https://www.doctrine-project.org/projects/event-manager.html",
"keywords": [
"event",
"event dispatcher",
"event manager",
"event system",
"events"
],
"funding": [
{
"url": "https://www.doctrine-project.org/sponsorship.html",
"type": "custom"
},
{
"url": "https://www.patreon.com/phpdoctrine",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager",
"type": "tidelift"
}
],
"time": "2020-05-29T18:28:51+00:00"
},
{
"name": "doctrine/inflector",
"version": "2.0.3",

98
config/attachments.php Normal file
View File

@ -0,0 +1,98 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Routes
|--------------------------------------------------------------------------
|
| Determine whether or not to automatically define attachments routes.
| Used for local storage only as other storage should define their public URL.
|
*/
'routes' => [
'publish' => true,
'prefix' => 'attachments',
'middleware' => 'web',
'pattern' => '/{id}/{name}',
'shared_pattern' => '/shared/{token}',
'dropzone' => [
'upload_pattern' => '/dropzone',
'delete_pattern' => '/dropzone/{id}',
]
],
/*
|--------------------------------------------------------------------------
| Model
|--------------------------------------------------------------------------
|
| Attachment model used
|
*/
'attachment_model' => env('ATTACHMENTS_MODEL', \Bnb\Laravel\Attachments\Attachment::class),
/*
|--------------------------------------------------------------------------
| Uuid
|--------------------------------------------------------------------------
|
| Default attachment model uses an UUID column. You can define your own UUID
| generator here : a global function name or a static class method in the form :
| App\Namespace\ClassName@method
|
*/
'uuid_provider' => 'uuid_v4_base36',
/*
|--------------------------------------------------------------------------
| Behaviors
|--------------------------------------------------------------------------
|
| Configurable behaviors :
| - Concrete files can be delete when the database entry is deleted
| - Dropzone delete can check for CSRF token match (set on upload)
|
*/
'behaviors' => [
'cascade_delete' => env('ATTACHMENTS_CASCADE_DELETE', true),
'dropzone_check_csrf' => env('ATTACHMENTS_DROPZONE_CHECK_CSRF', true),
],
/*
|--------------------------------------------------------------------------
| Declare the attachment model attributes
|--------------------------------------------------------------------------
|
| This allow to extend the attachment model with new columns
| `dropzone_attributes` holds the public fields returned after a successful upload via DropzoneController
|
*/
'attributes' => ['title', 'description', 'key', 'disk', 'filepath', 'group'],
'dropzone_attributes' => ['uuid', 'url', 'url_inline', 'filename', 'filetype', 'filesize', 'title', 'description', 'key', 'group'],
/*
|--------------------------------------------------------------------------
| Attachment Storage Directory
|--------------------------------------------------------------------------
|
| Defines the directory prefix where new attachment files are stored
|
*/
'storage_directory' => [
'prefix' => rtrim(env('ATTACHMENTS_STORAGE_DIRECTORY_PREFIX', 'attachments'), '/'),
],
/*
|--------------------------------------------------------------------------
| Database configuration
|--------------------------------------------------------------------------
|
| Allows to set the database connection name for the module's models
|
*/
'database' => [
'connection' => env('ATTACHMENTS_DATABASE_CONNECTION'),
],
];

106
config/scout.php Normal file
View File

@ -0,0 +1,106 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Search Engine
|--------------------------------------------------------------------------
|
| This option controls the default search connection that gets used while
| using Laravel Scout. This connection is used when syncing all models
| to the search service. You should adjust this based on your needs.
|
| Supported: "algolia", "null"
|
*/
'driver' => env('SCOUT_DRIVER', 'elastic'),
/*
|--------------------------------------------------------------------------
| Index Prefix
|--------------------------------------------------------------------------
|
| Here you may specify a prefix that will be applied to all search index
| names used by Scout. This prefix may be useful if you have multiple
| "tenants" or applications sharing the same search infrastructure.
|
*/
'prefix' => env('SCOUT_PREFIX', ''),
/*
|--------------------------------------------------------------------------
| Queue Data Syncing
|--------------------------------------------------------------------------
|
| This option allows you to control if the operations that sync your data
| with your search engines are queued. When this is set to "true" then
| all automatic data syncing will get queued for better performance.
|
*/
'queue' => env('SCOUT_QUEUE', false),
/*
|--------------------------------------------------------------------------
| Chunk Sizes
|--------------------------------------------------------------------------
|
| These options allow you to control the maximum chunk size when you are
| mass importing data into the search engine. This allows you to fine
| tune each of these chunk sizes based on the power of the servers.
|
*/
'chunk' => [
'searchable' => 500,
'unsearchable' => 500,
],
/*
|--------------------------------------------------------------------------
| Soft Deletes
|--------------------------------------------------------------------------
|
| This option allows to control whether to keep soft deleted records in
| the search indexes. Maintaining soft deleted records can be useful
| if your application still needs to search for the records later.
|
*/
'soft_delete' => false,
/*
|--------------------------------------------------------------------------
| Identify User
|--------------------------------------------------------------------------
|
| This option allows you to control whether to notify the search engine
| of the user performing the search. This is sometimes useful if the
| engine supports any analytics based on this application's users.
|
| Supported engines: "algolia"
|
*/
'identify' => env('SCOUT_IDENTIFY', false),
/*
|--------------------------------------------------------------------------
| Algolia Configuration
|--------------------------------------------------------------------------
|
| Here you may configure your Algolia settings. Algolia is a cloud hosted
| search engine which works great with Scout out of the box. Just plug
| in your application ID and admin API key to get started searching.
|
*/
'algolia' => [
'id' => env('ALGOLIA_APP_ID', ''),
'secret' => env('ALGOLIA_SECRET', ''),
],
];

12
config/scout_elastic.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'client' => [
'hosts' => [
env('SCOUT_ELASTIC_HOST', 'localhost:9200'),
],
],
'update_mapping' => env('SCOUT_ELASTIC_UPDATE_MAPPING', true),
'indexer' => env('SCOUT_ELASTIC_INDEXER', 'single'),
'document_refresh' => env('SCOUT_ELASTIC_DOCUMENT_REFRESH'),
];

View File

@ -12,10 +12,9 @@
"devDependencies": {
"@ttskch/select2-bootstrap4-theme": "^1.3.4",
"axios": "^0.19",
"bootbox": "^5.5.2",
"bootstrap": "^4.0.0",
"cross-env": "^7.0",
"datatables.net-bs4": "^1.10.23",
"datatables.net-buttons-bs4": "^1.6.5",
"jquery": "^3.2",
"laravel-mix": "^5.0.1",
"lodash": "^4.17.19",
@ -23,11 +22,6 @@
"resolve-url-loader": "^2.3.1",
"sass": "^1.20.1",
"sass-loader": "^8.0.0",
"select2": "^4.0.13",
"vue": "^2.5.17",
"vue-template-compiler": "^2.6.10"
},
"dependencies": {
"vue-router": "^3.4.9"
"select2": "^4.0.13"
}
}

19309
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

66
resources/js/app.js vendored
View File

@ -8,18 +8,6 @@
require('./bootstrap');
$(document).ready(function(e) {
$.fn.dataTable.render.authorValue = function(_, context, book) {
let authors = [];
console.log(book);
for (let author of book.authors) {
authors.push(author.name);
}
return authors.join(', ');
};
var authorOptions = {
placeholder: 'Authors',
tags: true,
@ -64,23 +52,35 @@ $(document).ready(function(e) {
}
$.get('/lookup/' + barcodeValue, function(res) {
$row.find('input[name*=name]').val(res.title);
var $authors = $row.find('.select2-author');
$authors.children('option').remove();
for (var i = 0; i < res.authors.length; i++) {
$authors.append($('<option>', {value: res.authors[i], text: res.authors[i], selected: 'selected'}));
if (res.warning) {
bootbox.alert({
title: 'Warning',
message: res.warning,
callback: function() {
$('.barcode_input').filter(function() {
return !this.value;
}).focus();
}
});
}
$authors.trigger('change');
if (res.success) {
$row.find('input[name*=name]').val(res.data.title);
var $authors = $row.find('.select2-author');
$authors.children('option').remove();
for (var i = 0; i < res.data.authors.length; i++) {
$authors.append($('<option>', {value: res.data.authors[i], text: res.data.authors[i], selected: 'selected'}));
}
$authors.trigger('change');
}
}, 'json');
var count = emptyRowCount();
console.log('Empty rows:', count);
if (count < 1) {
var firstIndex = 0,
$container = $row.closest('.row-container');
@ -116,6 +116,26 @@ $(document).ready(function(e) {
}
}
});
$('.remove-item').click(function(e) {
e.preventDefault();
var $this = $(this),
$row = $this.closest('tr');
bootbox.confirm('Are you sure?', function(result) {
if (!result) {
return;
}
axios.delete($row.data('url')).then(function(res) {
if (res.data.success) {
$row.remove();
}
});
})
})
});
function emptyRowCount() {

View File

@ -12,8 +12,7 @@ try {
require('bootstrap');
require('select2');
require('datatables.net-bs4');
require('datatables.net-buttons-bs4');
window.bootbox = require('bootbox');
} catch (e) {}
/**
@ -24,21 +23,4 @@ try {
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// });
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -6,6 +6,9 @@
<div class="form-row justify-content-md-center">
<div class="col-4">
<select name="location" class="form-control select2-location">
@foreach ($locations as $location)
<option value="{{ $location->name }}">{{ $location->name }}</option>
@endforeach
</select>
</div>
</div>
@ -21,9 +24,11 @@
@endif
</div>
<div class="row">
<button class="btn btn-primary" name="save" type="submit">
Save
</button>
<div class="col-2">
<button class="btn btn-primary" name="save" type="submit" style="width:100%">
Save
</button>
</div>
</div>
</form>
@include('partials/row', [ 'id' => 'add-template', 'htmlClass' => 'invisible' ])

View File

@ -0,0 +1,18 @@
@extends('layouts.main')
@section('content')
<h3>{{ $book->name }}</h3>
<h5>By: {{ $book->authors->pluck('name')->join(', ') }}</h5>
<br />
@if ($thumbnail = $book->attachment('thumbnail'))
<h5>Image</h5>
<p>
<img src="{{ $thumbnail->url }}" />
</p>
@endif
<h5>Location:</h5>
<p>
{{ $book->location->name }}
</p>
@endsection

View File

View File

@ -1,11 +1,12 @@
@extends('layouts.main')
@section('content')
<table class="table table-compact">
<table class="table table-compact table-striped">
<thead>
<th>Name</th>
<th>Authors</th>
<th>Location</th>
<th>Actions</th>
</thead>
<tbody>
@foreach ($rows as $row)
@ -13,6 +14,9 @@
<td>{{ $row->name }}</td>
<td>{{ $row->authors->pluck('name')->join(', ') }}</td>
<td>{{ $row->location->name }}</td>
<td>
<a class="btn btn-info btn-sm" href="{{ route('books.show', $row->id) }}">View</a>
</td>
</tr>
@endforeach
</tbody>

View File

@ -21,7 +21,6 @@
</main><!-- /.container -->
<script src="{{ mix('js/app.js') }}"></script>
<script src="{{ asset('vendor/datatables/buttons.server-side.js') }}"></script>
@stack('scripts')
</body>
</html>

View File

@ -0,0 +1,21 @@
@extends('layouts.main')
@section('content')
<table class="table table-compact">
<thead>
<th>Name</th>
<th>Actions</th>
</thead>
<tbody>
@foreach ($locations as $row)
<tr>
<td>{{ $row->name }}</td>
<td>
<a href="{{ route('locations.show', $row->id) }}" class="btn btn-info btn-sm">View</a>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $locations->links() }}
@endsection

View File

@ -0,0 +1,24 @@
@extends('layouts.main')
@section('content')
<h3>{{ $location->name }}</h3>
<table class="table table-compact">
<thead>
<th>Name</th>
<th>Authors</th>
</thead>
<tbody>
@foreach ($rows as $row)
<tr data-id="{{ $row->id }}" data-url="{{ route('books.show', $row->id) }}">
<td>{{ $row->name }}</td>
<td>{{ $row->authors->pluck('name')->join(', ') }}</td>
<td>
<a class="btn btn-info btn-sm" href="{{ route('books.show', $row->id) }}">View</a>
<button class="remove-item btn btn-danger btn-sm">Remove</button>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $rows->links() }}
@endsection

View File

@ -11,7 +11,10 @@
<a class="nav-link" href="/">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/add">Add</a>
<a class="nav-link" href="{{ route('locations.index') }}">Locations</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('add') }}">Add</a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" action="{{ route('search') }}">

View File

@ -12,7 +12,7 @@
*/
Route::get('/', 'MainController@index');
Route::get('/add', 'MainController@add');
Route::get('/add', 'MainController@add')->name('add');
Route::post('/save', 'MainController@save')->name('save');
Route::get('/search', 'MainController@search')->name('search');

View File

@ -1,3 +1,4 @@
*
!public/
!temp/
!.gitignore

View File

@ -1,2 +1,2 @@
*
.gitignore
!.gitignore