A ton of progress, search start
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Tyler 2020-12-26 05:46:23 -05:00
parent bed34d5350
commit d086b29bcc
47 changed files with 87115 additions and 7581 deletions

View File

@ -4,15 +4,13 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class BookController extends Controller class BookController extends Controller {
{
/** /**
* Display a listing of the resource. * Display a listing of the resource.
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function index() public function index() {
{
// //
} }
@ -21,64 +19,58 @@ class BookController extends Controller
* *
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function create() public function create() {
{
// //
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function store(Request $request) public function store(Request $request) {
{
// //
} }
/** /**
* Display the specified resource. * Display the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function show($id) public function show($id) {
{
// //
} }
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function edit($id) public function edit($id) {
{
// //
} }
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
* *
* @param \Illuminate\Http\Request $request * @param \Illuminate\Http\Request $request
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function update(Request $request, $id) public function update(Request $request, $id) {
{
// //
} }
/** /**
* Remove the specified resource from storage. * Remove the specified resource from storage.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return \Illuminate\Http\Response
*/ */
public function destroy($id) public function destroy($id) {
{
// //
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Location;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class LocationController extends Controller { class LocationController extends Controller {
@ -37,10 +38,16 @@ class LocationController extends Controller {
* Display the specified resource. * Display the specified resource.
* *
* @param int $id * @param int $id
* @return \Illuminate\Http\Response * @return mixed
*/ */
public function show($id) { public function show($id) {
// $location = Location::find($id);
if (!$location) {
abort(404);
}
return view('location', [ 'location' => $location ]);
} }
/** /**

View File

@ -2,12 +2,73 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Author;
use App\Models\Book;
use App\Models\Location;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
class MainController extends Controller { class MainController extends Controller {
public function index() { public function index() {
return view('welcome'); $rows = Book::paginate(15);
return view('index', [ 'rows' => $rows ]);
} }
public function add() { public function search(Request $request) {
return view('add'); $this->validate($request, [
'query' => [ 'required' ]
]);
$rows = Book::where('name', 'LIKE', '%' . $request->get('query') . '%')->paginate(15);
return view('index', [ 'rows' => $rows ]);
}
public function add(Request $request) {
return view('add', [
'old' => array_filter($request->old('books', []), function($item) {
return !empty($item['barcode']);
})
]);
}
public function save(Request $request) {
$this->validate($request, [
'location' => [ 'required' ]
]);
$input = array_filter($request->get('books'), function($item) {
return !empty($item['barcode']) && !empty($item['name']);
});
$this->getValidationFactory()->make($input, [
'*.barcode' => [ 'required' ],
'*.name' => [ 'required' ]
]);
$location = Location::firstOrCreate([
'name' => trim($request->get('location'))
]);
foreach ($input as $item) {
$book = Book::create([
'location_id' => $location->id,
'barcode' => $item['barcode'],
'name' => $item['name']
]);
$authors = [];
foreach (Arr::get($item, 'authors') as $author) {
$authors[] = Author::firstOrCreate([
'name' => $author
]);
}
$authors = array_map(function($author) { return $author->id; }, $authors);
$book->authors()->attach($authors);
}
} }
} }

View File

@ -2,9 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\Model;
class Author extends Authenticatable { class Author extends Model {
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View File

@ -2,9 +2,15 @@
namespace App\Models; namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable; use App\Services\Search\BookConfigurator;
use Illuminate\Database\Eloquent\Model;
use ScoutElastic\Searchable;
class Book extends Authenticatable { class Book extends Model {
use Searchable;
protected $indexConfigurator = BookConfigurator::class;
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
@ -12,14 +18,28 @@ class Book extends Authenticatable {
* @var array * @var array
*/ */
protected $fillable = [ protected $fillable = [
'name', 'location_id', 'barcode', 'name',
]; ];
public function author() { public function authors() {
return $this->hasMany(Author::class); return $this->belongsToMany(Author::class, 'book_authors');
} }
public function location() { public function location() {
return $this->belongsTo(Location::class); return $this->belongsTo(Location::class);
} }
protected $mapping = [
'properties' => [
'name' => [
'type' => 'text',
// Also you can configure multi-fields, more details you can find here https://www.elastic.co/guide/en/elasticsearch/reference/current/multi-fields.html
'fields' => [
'raw' => [
'type' => 'keyword',
]
]
],
]
];
} }

View File

@ -2,9 +2,9 @@
namespace App\Models; namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\Model;
class Location extends Authenticatable { class Location extends Model {
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.

View File

@ -0,0 +1,22 @@
<?php
namespace App\Services\Search;
use ScoutElastic\IndexConfigurator;
class BookConfigurator extends IndexConfigurator
{
// It's not obligatory to determine name. By default it'll be a snaked class name without `IndexConfigurator` part.
protected $name = 'books';
// You can specify any settings you want, for example, analyzers.
protected $settings = [
'analysis' => [
'analyzer' => [
'es_std' => [
'type' => 'standard',
'stopwords' => '_spanish_'
]
]
]
];
}

View File

@ -6,15 +6,16 @@
"type": "project", "type": "project",
"require": { "require": {
"php": "^7.3|^8.0", "php": "^7.3|^8.0",
"babenkoivan/scout-elasticsearch-driver": "^4.2",
"fideloper/proxy": "^4.4", "fideloper/proxy": "^4.4",
"fruitcake/laravel-cors": "^2.0", "fruitcake/laravel-cors": "^2.0",
"google/apiclient": "^2.2", "google/apiclient": "^2.2",
"guzzlehttp/guzzle": "^7.0.1", "guzzlehttp/guzzle": "^7.0.1",
"laravel/framework": "^8.12", "laravel/framework": "^8.12",
"laravel/scout": "^8.5",
"laravel/tinker": "^2.5", "laravel/tinker": "^2.5",
"laravel/ui": "^3.0", "laravel/ui": "^3.0",
"sunra/php-simple-html-dom-parser": "^1.5", "sunra/php-simple-html-dom-parser": "^1.5"
"yajra/laravel-datatables-oracle": "^9.0"
}, },
"require-dev": { "require-dev": {
"facade/ignition": "^2.5", "facade/ignition": "^2.5",

1461
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -2,20 +2,32 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "npm run development", "dev": "npm run development",
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --config=node_modules/laravel-mix/setup/webpack.config.js",
"watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", "watch": "npm run development -- --watch",
"watch-poll": "npm run watch -- --watch-poll", "watch-poll": "npm run watch -- --watch-poll",
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
"prod": "npm run production", "prod": "npm run production",
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --config=node_modules/laravel-mix/setup/webpack.config.js"
}, },
"devDependencies": { "devDependencies": {
"axios": "^0.17", "@ttskch/select2-bootstrap4-theme": "^1.3.4",
"bootstrap-sass": "^3.3.7", "axios": "^0.19",
"cross-env": "^5.1", "bootstrap": "^4.0.0",
"cross-env": "^7.0",
"datatables.net-bs4": "^1.10.23",
"datatables.net-buttons-bs4": "^1.6.5",
"jquery": "^3.2", "jquery": "^3.2",
"laravel-mix": "^1.0", "laravel-mix": "^5.0.1",
"lodash": "^4.17.4", "lodash": "^4.17.19",
"vue": "^2.5.7" "popper.js": "^1.12",
"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"
} }
} }

11293
public/css/app.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
/*! /*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)
* Copyright 2011-2017 The Bootstrap Authors * Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc. * Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/ */
*, *,
@ -15,22 +15,16 @@ html {
font-family: sans-serif; font-family: sans-serif;
line-height: 1.15; line-height: 1.15;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-ms-overflow-style: scrollbar;
-webkit-tap-highlight-color: transparent;
} }
@-ms-viewport { article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
width: device-width;
}
article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section {
display: block; display: block;
} }
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem; font-size: 1rem;
font-weight: 400; font-weight: 400;
line-height: 1.5; line-height: 1.5;
@ -39,8 +33,8 @@ body {
background-color: #fff; background-color: #fff;
} }
[tabindex="-1"]:focus { [tabindex="-1"]:focus:not(:focus-visible) {
outline: none !important; outline: 0 !important;
} }
hr { hr {
@ -63,9 +57,11 @@ abbr[title],
abbr[data-original-title] { abbr[data-original-title] {
text-decoration: underline; text-decoration: underline;
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; text-decoration: underline dotted;
cursor: help; cursor: help;
border-bottom: 0; border-bottom: 0;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
} }
address { address {
@ -101,10 +97,6 @@ blockquote {
margin: 0 0 1rem; margin: 0 0 1rem;
} }
dfn {
font-style: italic;
}
b, b,
strong { strong {
font-weight: bolder; font-weight: bolder;
@ -134,7 +126,6 @@ a {
color: #007bff; color: #007bff;
text-decoration: none; text-decoration: none;
background-color: transparent; background-color: transparent;
-webkit-text-decoration-skip: objects;
} }
a:hover { a:hover {
@ -142,25 +133,21 @@ a:hover {
text-decoration: underline; text-decoration: underline;
} }
a:not([href]):not([tabindex]) { a:not([href]):not([class]) {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { a:not([href]):not([class]):hover {
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
} }
a:not([href]):not([tabindex]):focus {
outline: 0;
}
pre, pre,
code, code,
kbd, kbd,
samp { samp {
font-family: monospace, monospace; font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em; font-size: 1em;
} }
@ -180,21 +167,9 @@ img {
border-style: none; border-style: none;
} }
svg:not(:root) { svg {
overflow: hidden; overflow: hidden;
} vertical-align: middle;
a,
area,
button,
[role="button"],
input:not([type="range"]),
label,
select,
summary,
textarea {
-ms-touch-action: manipulation;
touch-action: manipulation;
} }
table { table {
@ -204,18 +179,19 @@ table {
caption { caption {
padding-top: 0.75rem; padding-top: 0.75rem;
padding-bottom: 0.75rem; padding-bottom: 0.75rem;
color: #868e96; color: #6c757d;
text-align: left; text-align: left;
caption-side: bottom; caption-side: bottom;
} }
th { th {
text-align: inherit; text-align: inherit;
text-align: -webkit-match-parent;
} }
label { label {
display: inline-block; display: inline-block;
margin-bottom: .5rem; margin-bottom: 0.5rem;
} }
button { button {
@ -248,13 +224,28 @@ select {
text-transform: none; text-transform: none;
} }
[role="button"] {
cursor: pointer;
}
select {
word-wrap: normal;
}
button, button,
html [type="button"], [type="button"],
[type="reset"], [type="reset"],
[type="submit"] { [type="submit"] {
-webkit-appearance: button; -webkit-appearance: button;
} }
button:not(:disabled),
[type="button"]:not(:disabled),
[type="reset"]:not(:disabled),
[type="submit"]:not(:disabled) {
cursor: pointer;
}
button::-moz-focus-inner, button::-moz-focus-inner,
[type="button"]::-moz-focus-inner, [type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner, [type="reset"]::-moz-focus-inner,
@ -269,13 +260,6 @@ input[type="checkbox"] {
padding: 0; padding: 0;
} }
input[type="date"],
input[type="time"],
input[type="datetime-local"],
input[type="month"] {
-webkit-appearance: listbox;
}
textarea { textarea {
overflow: auto; overflow: auto;
resize: vertical; resize: vertical;
@ -314,7 +298,6 @@ progress {
-webkit-appearance: none; -webkit-appearance: none;
} }
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration { [type="search"]::-webkit-search-decoration {
-webkit-appearance: none; -webkit-appearance: none;
} }
@ -330,6 +313,7 @@ output {
summary { summary {
display: list-item; display: list-item;
cursor: pointer;
} }
template { template {

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
/*! /*!
* Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) * Bootstrap Reboot v4.5.3 (https://getbootstrap.com/)
* Copyright 2011-2017 The Bootstrap Authors * Copyright 2011-2020 The Bootstrap Authors
* Copyright 2011-2017 Twitter, Inc. * Copyright 2011-2020 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([class]){color:inherit;text-decoration:none}a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit;text-align:-webkit-match-parent}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */ /*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

5245
public/css/bootstrap.css vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

61357
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3362
public/js/bootstrap.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap');
window.Vue = require('vue');
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('example-component', require('./components/ExampleComponent.vue'));
const app = new Vue({
el: '#app'
});

View File

@ -1,23 +0,0 @@
<template>
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">Example Component</div>
<div class="panel-body">
I'm an example component!
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
}
}
</script>

View File

@ -1,38 +0,0 @@
// Body
$body-bg: #f5f8fa;
// Borders
$laravel-border-color: darken($body-bg, 10%);
$list-group-border: $laravel-border-color;
$navbar-default-border: $laravel-border-color;
$panel-default-border: $laravel-border-color;
$panel-inner-border: $laravel-border-color;
// Brands
$brand-primary: #3097D1;
$brand-info: #8eb4cb;
$brand-success: #2ab27b;
$brand-warning: #cbb956;
$brand-danger: #bf5329;
// Typography
$icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/";
$font-family-sans-serif: "Raleway", sans-serif;
$font-size-base: 14px;
$line-height-base: 1.6;
$text-color: #636b6f;
// Navbar
$navbar-default-bg: #fff;
// Buttons
$btn-default-color: $text-color;
// Inputs
$input-border: lighten($text-color, 40%);
$input-border-focus: lighten($brand-primary, 25%);
$input-color-placeholder: lighten($text-color, 30%);
// Panels
$panel-default-heading-bg: #fff;

View File

@ -1,9 +0,0 @@
// Fonts
@import url("https://fonts.googleapis.com/css?family=Raleway:300,400,600");
// Variables
@import "variables";
// Bootstrap
@import "~bootstrap-sass/assets/stylesheets/bootstrap";

133
resources/js/app.js vendored Normal file
View File

@ -0,0 +1,133 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
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,
ajax: {
url: '/authors/search',
dataType: 'json'
}
};
var locationOptions = {
placeholder: 'Location',
tags: true
};
$('#add-form .select2-author').select2(authorOptions);
$('#add-form .select2-location').select2(locationOptions);
$('#add-form').on('click', '.remove-row', function(e) {
e.preventDefault();
$(this).closest('.form-row').remove();
});
$('#add-form').on('keydown', '.barcode_input', function(e) {
if (e.keyCode === 13) {
e.preventDefault();
}
});
$('#add-form').on('keyup', '.barcode_input', function (e) {
if (e.keyCode === 13) {
// Do something
e.preventDefault();
var $this = $(this),
$row = $this.closest('.form-row'),
barcodeValue = $this.val();
if (barcodeValue === '') {
return;
}
$.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'}));
}
$authors.trigger('change');
}, 'json');
var count = emptyRowCount();
console.log('Empty rows:', count);
if (count < 1) {
var firstIndex = 0,
$container = $row.closest('.row-container');
for (var i = 0; i < 200; i++) {
if ($container.find('.form-row[data-index=' + i + ']').length < 1) {
firstIndex = i;
break;
}
}
var $template = $('#add-template'),
$clone = $template.clone();
$clone.attr('data-index', firstIndex);
$clone.find('input, select').each(function() {
$(this).attr('name', $(this).attr('name').replace('__INDEX__', firstIndex));
});
$clone.removeAttr('id');
$clone.find('input[type=text]').val('');
$clone.removeClass('invisible').addClass('form-row');
$container.append($clone);
setTimeout(function() {
$clone.find('.barcode_input').focus();
$clone.find('.select2-author').select2(authorOptions);
}, 150);
}
}
});
});
function emptyRowCount() {
var $barcodes = $('#add-form .barcode_input');
var count = 0;
$barcodes.each(function() {
if ($(this).val() == '') {
count++;
}
});
return count;
}

View File

@ -1,4 +1,3 @@
window._ = require('lodash'); window._ = require('lodash');
/** /**
@ -8,9 +7,13 @@ window._ = require('lodash');
*/ */
try { try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery'); window.$ = window.jQuery = require('jquery');
require('bootstrap-sass'); require('bootstrap');
require('select2');
require('datatables.net-bs4');
require('datatables.net-buttons-bs4');
} catch (e) {} } catch (e) {}
/** /**
@ -23,31 +26,19 @@ window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/** /**
* Echo exposes an expressive API for subscribing to channels and listening * Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting * for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications. * allows your team to easily build robust real-time web applications.
*/ */
// import Echo from 'laravel-echo' // import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js'); // window.Pusher = require('pusher-js');
// window.Echo = new Echo({ // window.Echo = new Echo({
// broadcaster: 'pusher', // broadcaster: 'pusher',
// key: 'your-pusher-key' // key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// }); // });

14
resources/sass/app.scss vendored Normal file
View File

@ -0,0 +1,14 @@
// Bootstrap
@import "../../node_modules/bootstrap/scss/bootstrap";
// DataTables
@import "../../node_modules/datatables.net-bs4/css/dataTables.bootstrap4.css";
@import "../../node_modules/datatables.net-buttons-bs4/css/buttons.bootstrap4.css";
body {
padding-top: 5rem;
}
.form-row {
padding-bottom: 6px;
}

View File

@ -1,7 +1,8 @@
@extends('layouts.main') @extends('layouts.main')
@section('content') @section('content')
<form id="add-form"> <form id="add-form" method="POST" action="{{ route('save') }}">
{{ csrf_field() }}
<div class="form-row justify-content-md-center"> <div class="form-row justify-content-md-center">
<div class="col-4"> <div class="col-4">
<select name="location" class="form-control select2-location"> <select name="location" class="form-control select2-location">
@ -9,29 +10,21 @@
</div> </div>
</div> </div>
<br /> <br />
<div class="form-row"> <div class="row-container">
<div class="col"> @if (!empty($old))
<input name="books[][barcode]" type="text" class="form-control barcode_input" placeholder="Barcode"> @foreach ($old as $index => $item)
</div> @include('partials/row', [ 'index' => $index, 'item' => $item ])
<div class="col-4"> @endforeach
<input name="books[][name]" type="text" class="form-control" placeholder="Name"> @include('partials/row', [ 'index' => count($old), 'item' => [] ])
</div> @else
<div class="col"> @include('partials/row', [ 'index' => 0])
<select name="books[][authors]" class="form-control select2-author" multiple="multiple"> @endif
</select> </div>
</div> <div class="row">
<button class="btn btn-primary" name="save" type="submit">
Save
</button>
</div> </div>
</form> </form>
<div id="add-template" class="invisible"> @include('partials/row', [ 'id' => 'add-template', 'htmlClass' => 'invisible' ])
<div class="col">
<input name="books[][barcode]" type="text" class="form-control barcode_input" placeholder="Barcode">
</div>
<div class="col-4">
<input name="books[][name]" type="text" class="form-control" placeholder="Name">
</div>
<div class="col">
<select name="books[][authors]" class="form-control select2-author" multiple="multiple">
</select>
</div>
</div>
@endsection @endsection

View File

@ -0,0 +1,21 @@
@extends('layouts.main')
@section('content')
<table class="table table-compact">
<thead>
<th>Name</th>
<th>Authors</th>
<th>Location</th>
</thead>
<tbody>
@foreach ($rows as $row)
<tr>
<td>{{ $row->name }}</td>
<td>{{ $row->authors->pluck('name')->join(', ') }}</td>
<td>{{ $row->location->name }}</td>
</tr>
@endforeach
</tbody>
</table>
{{ $rows->links() }}
@endsection

View File

@ -8,49 +8,20 @@
<title>{{ config('app.name') }}</title> <title>{{ config('app.name') }}</title>
<!-- Bootstrap core CSS --> <link href="{{ mix('css/app.css') }}" rel="stylesheet">
<link href="{{ url('css/bootstrap.min.css') }}" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" /> <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/css/select2.min.css" rel="stylesheet" />
<link href="{{ url('css/select2-bs4.css') }}" rel="stylesheet">
<link href="{{ url('css/app.css') }}" rel="stylesheet">
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top"> @include('partials/navbar')
<a class="navbar-brand" href="#">{{ config('app.name') }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<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>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
<main role="main" class="container"> <main role="main" class="container">
@yield('content') @yield('content')
</main><!-- /.container --> </main><!-- /.container -->
<script <script src="{{ mix('js/app.js') }}"></script>
src="https://code.jquery.com/jquery-3.2.1.min.js" <script src="{{ asset('vendor/datatables/buttons.server-side.js') }}"></script>
integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" @stack('scripts')
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js" integrity="sha384-vFJXuSJphROIrBnz7yo7oB41mKfc8JzQZiCq4NCceLEaO4IHwicKwpJf9c9IpFgh" crossorigin="anonymous"></script>
<script src="{{ url('js/bootstrap.min.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
<script src="{{ url('js/app.js') }}"></script>
</body> </body>
</html> </html>

View File

@ -0,0 +1,22 @@
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<a class="navbar-brand" href="#">{{ config('app.name') }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault"
aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<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>
</li>
</ul>
<form class="form-inline my-2 my-lg-0" action="{{ route('search') }}">
<input name="query" class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search" />
<button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>

View File

@ -0,0 +1,20 @@
<div {!! !empty($id) ? 'id="' . $id . '" ' : '' !!}class="form-row {{ $htmlClass ?? '' }}" data-index="{{ $index ?? '__INDEX__' }}">
<div class="col">
<input name="books[{{ $index ?? '__INDEX__' }}][barcode]" type="text" class="form-control barcode_input" placeholder="Barcode" value="{{ $item['barcode'] ?? '' }}">
</div>
<div class="col-4">
<input name="books[{{ $index ?? '__INDEX__' }}][name]" type="text" class="form-control" placeholder="Name" value="{{ $item['name'] ?? '' }}">
</div>
<div class="col">
<select name="books[{{ $index ?? '__INDEX__' }}][authors][]" class="form-control select2-author" multiple="multiple">
@if(!empty($item['authors']))
@foreach($item['authors'] as $author)
<option value="{{ $author }}" selected="selected">{{ $author }}</option>
@endforeach
@endif
</select>
</div>
<div class="col-1">
<button class="remove-row btn btn-danger">Remove</button>
</div>
</div>

View File

@ -1 +0,0 @@
@extends('layouts.main')

View File

@ -13,6 +13,8 @@
Route::get('/', 'MainController@index'); Route::get('/', 'MainController@index');
Route::get('/add', 'MainController@add'); Route::get('/add', 'MainController@add');
Route::post('/save', 'MainController@save')->name('save');
Route::get('/search', 'MainController@search')->name('search');
Route::get('/lookup/{isbn}', 'LookupController@lookup'); Route::get('/lookup/{isbn}', 'LookupController@lookup');

6
webpack.mix.js vendored
View File

@ -1,4 +1,4 @@
let mix = require('laravel-mix'); const mix = require('laravel-mix');
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -11,5 +11,5 @@ let mix = require('laravel-mix');
| |
*/ */
mix.js('resources/assets/js/app.js', 'public/js') mix.js('resources/js/app.js', 'public/js')
.sass('resources/assets/sass/app.scss', 'public/css'); .sass('resources/sass/app.scss', 'public/css');