diff --git a/CHANGELOG.md b/CHANGELOG.md index 6619d3d1a..a0d68397c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file[^1]. ## [Unreleased] +### Added +- Featured Artworks back-end section +- new Tailwind component for back-end/admin + ### Changed - font loading (fix for Source Serif Pro) diff --git a/app/FeaturedArtwork.php b/app/FeaturedArtwork.php new file mode 100644 index 000000000..3b3a4bdbf --- /dev/null +++ b/app/FeaturedArtwork.php @@ -0,0 +1,77 @@ +belongsTo(Item::class); + } + + public function getIsPublishedAttribute() { + return !!$this->published_at; + } + + public function setIsPublishedAttribute(bool $isPublished) { + if ($this->is_published === $isPublished) return; + if (!$isPublished) return $this->attributes['published_at'] = null; + + $this->attributes['published_at'] = Carbon::now(); + } + + public function scopePublished($query) + { + return $query->where('published_at', '<=', Carbon::now()); + } + + public function getAuthorLinksAttribute() + { + if (!$this->item) return; + + return $this->item + ->authors_with_authorities + ->map(fn ($a) => (object) [ + 'label' => formatName($a->name), + 'url' => isset($a->authority) ? $a->authority->getUrl() : route('frontend.catalog.index', ['author' => $a->name]) + ]); + } + + public function getMetadataLinksAttribute() + { + if (!$this->item) return; + + $dating = (object) [ + 'label' => $this->item->getDatingFormated(), + 'url' => null, + ]; + + $techniques = collect($this->item->techniques) + ->map(fn ($t) => (object) [ + 'label' => $t, + 'url' => route('frontend.catalog.index', ['technique' => $t]), + ]); + + $media = collect($this->item->mediums) + ->map(fn ($m) => (object) [ + 'label' => $m, + 'url' => route('frontend.catalog.index', ['medium' => $m]), + ]); + + + return collect([$dating]) + ->concat($techniques) + ->concat($media); + } +} diff --git a/app/Http/Controllers/Admin/FeaturedArtworkController.php b/app/Http/Controllers/Admin/FeaturedArtworkController.php new file mode 100644 index 000000000..46fba9ee3 --- /dev/null +++ b/app/Http/Controllers/Admin/FeaturedArtworkController.php @@ -0,0 +1,91 @@ + 'required', + 'description' => 'required', + 'item_id' => 'required' + ]; + + public function index() + { + return view('featured_artworks.index', [ + 'artworks' => FeaturedArtwork::query() + ->with('item') + ->orderBy('id', 'desc') + ->get(), + 'lastPublishedId' => FeaturedArtwork::query() + ->published() + ->orderBy('published_at', 'desc') + ->value('id') + ]); + } + + public function create(Request $request) + { + $item = $request->query('itemId') ? Item::find($request->query('itemId')) : null; + + return view('featured_artworks.form', [ + 'artwork' => new FeaturedArtwork([ + 'title' => optional($item)->title, + 'description' => optional($item)->description, + 'item' => $item, + ]) + ]); + } + + public function edit(FeaturedArtwork $featuredArtwork) + { + return view('featured_artworks.form', [ + 'artwork' => $featuredArtwork, + ]); + } + + public function store(Request $request) + { + $request->validate(self::$rules); + $featuredArtwork = FeaturedArtwork::create(array_merge( + $request->input(), + [ + 'is_published' => $request->boolean('is_published') + ] + )); + + return redirect() + ->route('featured-artworks.index') + ->with('message', "Vybrané dielo \"{$featuredArtwork->title}\" bolo vytvorené"); + } + + public function update(Request $request, FeaturedArtwork $featuredArtwork) + { + $request->validate(self::$rules); + + $featuredArtwork->update(array_merge( + $request->input(), + [ + 'is_published' => $request->boolean('is_published') + ] + )); + + return redirect() + ->route('featured-artworks.index') + ->with('message', "Vybrané dielo \"{$featuredArtwork->title}\" bolo upravené"); + } + + public function destroy(FeaturedArtwork $featuredArtwork) + { + $featuredArtwork->delete(); + + return redirect() + ->route('featured-artworks.index') + ->with('message', 'Odporúčané dielo bolo zmazané'); + } +} diff --git a/app/Item.php b/app/Item.php index fd335f744..b4e657242 100644 --- a/app/Item.php +++ b/app/Item.php @@ -351,6 +351,24 @@ public function getAuthorsWithoutAuthority() ->keys(); } + public function getAuthorsWithAuthoritiesAttribute() + { + $authorities = $this + ->authorities + ->map(fn ($authority) => (object) [ + 'name' => $authority->name, + 'authority' => $authority + ]); + + $authors = $this + ->getAuthorsWithoutAuthority() + ->map(fn ($author) => (object) [ + 'name' => $author + ]); + + return $authorities->concat($authors); + } + public function getFirstAuthorAttribute($value) { $authors_array = $this->makeArray($this->author); diff --git a/app/View/Components/Admin/Form.php b/app/View/Components/Admin/Form.php new file mode 100644 index 000000000..7511f5af4 --- /dev/null +++ b/app/View/Components/Admin/Form.php @@ -0,0 +1,59 @@ +model = $model; + $this->url = $this->buildUrl($url); + $this->method = $this->buildMethod($method); + } + + private function buildUrl(?string $url): string { + if ($url) return $url; + + // Try to guess url from model class + $routeName = Str::of(class_basename($this->model))->plural()->kebab(); + + if ($this->model->exists) { + return route("$routeName.update", [$this->model]); + } + + return route("$routeName.store"); + } + + private function buildMethod(?string $method): string { + if ($method) return $method; + + if ($this->model->exists) return 'patch'; + + return 'post'; + } + + /** + * Get the view / contents that represent the component. + * + * @return \Illuminate\Contracts\View\View|\Closure|string + */ + public function render() + { + return view('components.admin.form'); + } +} diff --git a/database/migrations/2022_02_18_065826_create_featured_artworks_table.php b/database/migrations/2022_02_18_065826_create_featured_artworks_table.php new file mode 100644 index 000000000..4d96e926c --- /dev/null +++ b/database/migrations/2022_02_18_065826_create_featured_artworks_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('item_id'); + $table->foreign('item_id')->references('id')->on('items'); + $table->string('title'); + $table->text('description'); + $table->dateTime('published_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('featured_artworks'); + } +} diff --git a/package-lock.json b/package-lock.json index 6726be1f6..685cbee98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2912,6 +2912,14 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "corejs-typeahead": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/corejs-typeahead/-/corejs-typeahead-1.3.1.tgz", + "integrity": "sha512-fyNlBNWJNL6EQUnJyAunEzBzRcwR2cEHtZXBi2pndHPOJ/wpOf3wbS+/Oh+kYYS5sKowQcs0LFwMSl6Y2Xeqkw==", + "requires": { + "jquery": ">=1.11" + } + }, "cosmiconfig": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", @@ -4688,7 +4696,6 @@ "minipass": { "version": "2.9.0", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4893,8 +4900,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -4984,8 +4990,7 @@ }, "yallist": { "version": "3.1.1", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -6879,7 +6884,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, "optional": true, @@ -6889,7 +6894,7 @@ "dependencies": { "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true, "optional": true @@ -8315,7 +8320,7 @@ "dependencies": { "jsesc": { "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", "dev": true } diff --git a/package.json b/package.json index 4c4461933..6d5bff9dd 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "dependencies": { "bootstrap": "^3.4.1", "clipboard": "^2.0.6", + "corejs-typeahead": "^1.3.1", "date-fns": "^2.25.0", "debounce": "1.2.0", "flickity": "^2.2.1", diff --git a/resources/js/admin.js b/resources/js/admin.js index 1fbc4b3a2..110ae1b98 100644 --- a/resources/js/admin.js +++ b/resources/js/admin.js @@ -1,7 +1,9 @@ window.debounce = require('debounce'); window.Vue = require('vue').default; -Vue.component('linked-combos', require('./components/vue/linked-combos').default); Vue.component('admin-links-input', require('./components/admin/LinksInput.vue').default); +Vue.component('autocomplete', require('./components/Autocomplete.vue').default); +Vue.component('linked-combos', require('./components/vue/linked-combos').default); +Vue.component('query-string', require('./components/QueryString.vue').default); new Vue({ el: '#wrapper' }) diff --git a/resources/js/components/Autocomplete.vue b/resources/js/components/Autocomplete.vue new file mode 100644 index 000000000..61fecd240 --- /dev/null +++ b/resources/js/components/Autocomplete.vue @@ -0,0 +1,70 @@ + + + diff --git a/resources/js/components/QueryString.vue b/resources/js/components/QueryString.vue new file mode 100644 index 000000000..ae4a1c2eb --- /dev/null +++ b/resources/js/components/QueryString.vue @@ -0,0 +1,16 @@ + diff --git a/resources/views/components/admin/alert.blade.php b/resources/views/components/admin/alert.blade.php new file mode 100644 index 000000000..ab89dbeeb --- /dev/null +++ b/resources/views/components/admin/alert.blade.php @@ -0,0 +1,9 @@ +@props([ + 'danger' => false, + 'info' => false, +]) + +
class(['alert', 'alert-danger' => $danger, 'alert-info' => $info]) }}> + × + {{ $slot }} +
diff --git a/resources/views/components/admin/button.blade.php b/resources/views/components/admin/button.blade.php new file mode 100644 index 000000000..da27bc087 --- /dev/null +++ b/resources/views/components/admin/button.blade.php @@ -0,0 +1,60 @@ +@props([ + 'outline' => false, + 'primary' => false, + 'danger' => false, + 'sm' => false, + 'link' => null, +]) + +@php +$defaultColor = !$primary && !$danger; +$defaultSize = $sm == false; + +$class = [ + 'tw-font-medium tw-rounded active:tw-shadow-inner', + + // Set outlines on buttons, not on links + 'active:tw-outline active:tw-outline-black' => !$link, + 'tw-inline-block' => $link, +]; + +if ($sm) { + $class[] = 'tw-py-1 tw-px-2 tw-text-sm'; +} else { + $class[] = 'tw-py-2 tw-px-4'; +} + +if ($outline) { + $class[] = 'tw-border'; + if ($primary) { + $class[] = 'tw-text-[#3276b1] hover:tw-text-white tw-border-[#357ebd] hover:tw-bg-[#3276b1] hover:tw-border-[#285e8e]'; + } + if ($danger) { + $class[] = 'tw-text-[#d9534f] hover:tw-text-white tw-border-[#d43f3a] hover:tw-bg-[#d2322d] hover:tw-border-[#ac2925]'; + } + if ($defaultColor) { + $class[] = 'tw-border-gray-300 hover:tw-bg-gray-200'; + } +} else { + if ($primary) { + $class[] = 'tw-text-white tw-bg-[#428bca] tw-border-[#357ebd] hover:tw-bg-[#3276b1] hover:tw-border-[#285e8e]'; + } + if ($danger) { + $class[] = 'tw-text-white tw-bg-[#d9534f] hover:tw-text-white tw-border-[#d43f3a] hover:tw-bg-[#d2322d] hover:tw-border-[#ac2925]'; + } + if ($defaultColor) { + $class[] = 'tw-border-gray-300 tw-border hover:tw-bg-gray-200'; + } +} + +if ($sm) { +} +@endphp + +@if ($link) + class($class) }}>{{ $slot }} +@else + +@endif diff --git a/resources/views/components/admin/checkbox.blade.php b/resources/views/components/admin/checkbox.blade.php new file mode 100644 index 000000000..cd6721aa2 --- /dev/null +++ b/resources/views/components/admin/checkbox.blade.php @@ -0,0 +1,4 @@ +@props(['name']) + + + diff --git a/resources/views/components/admin/form.blade.php b/resources/views/components/admin/form.blade.php new file mode 100644 index 000000000..caa2bb797 --- /dev/null +++ b/resources/views/components/admin/form.blade.php @@ -0,0 +1,3 @@ +{!! Form::model($model, compact('url', 'method')) !!} + {{ $slot }} +{!! Form::close() !!} diff --git a/resources/views/components/admin/input.blade.php b/resources/views/components/admin/input.blade.php new file mode 100644 index 000000000..65dc5cb9f --- /dev/null +++ b/resources/views/components/admin/input.blade.php @@ -0,0 +1 @@ +merge(['class' => 'tw-block tw-w-full tw-py-2 tw-px-4 tw-text-base tw-shadow-inner focus:tw-shadow-[inset_0_1px_1px_rgb(0,0,0,0.2),0_0_8px_rgb(102,175,233,0.6)] focus:tw-outline-none tw-border tw-border-gray-300 focus:tw-border-sky-400 tw-rounded-md']) !!}> diff --git a/resources/views/components/admin/label.blade.php b/resources/views/components/admin/label.blade.php new file mode 100644 index 000000000..70390a4a1 --- /dev/null +++ b/resources/views/components/admin/label.blade.php @@ -0,0 +1,5 @@ +@props(['value']) + + diff --git a/resources/views/components/admin/link.blade.php b/resources/views/components/admin/link.blade.php new file mode 100644 index 000000000..f9ada2332 --- /dev/null +++ b/resources/views/components/admin/link.blade.php @@ -0,0 +1 @@ +merge(['class' => 'tw-text-[#428bca] hover:tw-underline']) !!}>{{ $slot }} \ No newline at end of file diff --git a/resources/views/components/item_author.blade.php b/resources/views/components/item_author.blade.php index 8fd7a990b..a57e72f3c 100644 --- a/resources/views/components/item_author.blade.php +++ b/resources/views/components/item_author.blade.php @@ -1,13 +1,13 @@ {{-- comments kept for code indentation without whitespace --}} -@if (is_string($author)) - {{ formatName($author) }}{{-- ---}}@elseif ($author->pivot->role === 'autor/author') +@empty ($author->authority) + {{ formatName($author->name) }}{{-- +--}}@elseif ($author->authority->pivot->role === 'autor/author') - - {{ formatName($author->name) }}{{-- + + {{ formatName($author->authority->name) }}{{-- --}}{{-- --}}{{-- --}}@else - {{ formatName($author->name) }} - – {{ $author::formatMultiAttribute($author->pivot->role) }}{{-- + {{ formatName($author->authority->name) }} + – {{ $author->authority::formatMultiAttribute($author->authority->pivot->role) }}{{-- --}}@endif \ No newline at end of file diff --git a/resources/views/dielo.blade.php b/resources/views/dielo.blade.php index d8d15d84b..8c1ebf188 100644 --- a/resources/views/dielo.blade.php +++ b/resources/views/dielo.blade.php @@ -51,13 +51,7 @@

{{ $item->title }}

- @php - $authors = $item->authorities - ->toBase() - ->merge($item->getAuthorsWithoutAuthority()); - @endphp - - @foreach($authors as $author) + @foreach($item->authors_with_authorities as $author) @include('components.item_author')@if (!$loop->last), @endif @endforeach

diff --git a/resources/views/featured_artworks/form.blade.php b/resources/views/featured_artworks/form.blade.php new file mode 100644 index 000000000..a670d0029 --- /dev/null +++ b/resources/views/featured_artworks/form.blade.php @@ -0,0 +1,85 @@ +@extends('layouts.admin') + +@section('content') +
+
+ @if ($errors->any()) + +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + + + + + + + + + @if ($artwork->item) +
+ +
+ + + + + @foreach ($artwork->authorLinks as $a) + {{ $a->label }} + {{ $loop->last ? '' : ', ' }} + @endforeach + + + @foreach ($artwork->metadataLinks as $m) + @if ($m->url) + {{ $m->label }}{{ $loop->last ? '' : ', ' }} + @else + {{ $m->label }}{{ $loop->last ? '' : ', ' }} + @endif + @endforeach +
+
+ + +
+
+ + + @if ($artwork->is_published) +

Publikované {{ $artwork->published_at }}

+ @endif +
+ + +
+
+ + Uložiť + + + Zrušiť + +
+ @endif +
+
+
+@endsection diff --git a/resources/views/featured_artworks/index.blade.php b/resources/views/featured_artworks/index.blade.php new file mode 100644 index 000000000..4262b87f6 --- /dev/null +++ b/resources/views/featured_artworks/index.blade.php @@ -0,0 +1,85 @@ +@extends('layouts.admin') + +@section('title') + @parent + - Vybrané diela +@endsection + +@section('content') +
+
+ + @if (session('message')) + + {{ session('message') }} + + @endif + +

Vybrané diela

+ +
+
+ + Vytvoriť + +
+
+ + + + + + + + + + + + @foreach ($artworks as $a) + + + + + + + + @endforeach + +
IDDieloVytvorenéPublikovanéAkcie
{{ $a->id }} + +
+

{{ $a->title }}

+

{{ $a->item->author }}

+
+
+ @datetime($a->created_at) + + @if ($a->is_published) + + @datetime($a->published_at) + + @if ($a->id == $lastPublishedId) +
+ Najnovšie + @endif + @endif +
+
+ + Upraviť + + + Zmazať + +
+
+
+
+
+
+@endsection diff --git a/resources/views/layouts/admin.blade.php b/resources/views/layouts/admin.blade.php index 994fac8c8..77c39c5d2 100644 --- a/resources/views/layouts/admin.blade.php +++ b/resources/views/layouts/admin.blade.php @@ -17,6 +17,7 @@ + {!! Html::style('css/sb-admin.css') !!} {!! Html::style('css/ladda-themeless.min.css') !!} {!! Html::style('css/bootstrap-wysihtml5.css') !!} @@ -109,8 +110,11 @@
  • Homepage
  • -
  • - Odporúčaný obsah +
  • + Odporúčaný obsah +
  • +
  • + Vybrané diela
  • @endcan @can('administer') diff --git a/routes/web.php b/routes/web.php index 12ac950ac..fda02279d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -533,6 +533,7 @@ function() Route::group(['middleware' => ['auth', 'can:administer']], function () { Route::resource('featured-pieces', 'Admin\FeaturedPieceController'); + Route::resource('featured-artworks', 'Admin\FeaturedArtworkController'); Route::get('harvests/launch/{id}', 'SpiceHarvesterController@launch'); Route::get('harvests/harvestFailed/{id}', 'SpiceHarvesterController@harvestFailed'); Route::get('harvests/orphaned/{id}', 'SpiceHarvesterController@orphaned'); diff --git a/tailwind.config.js b/tailwind.config.js index 713e9cf79..b47c5c199 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,6 +26,10 @@ module.exports = { 600: '#777', 800: '#333', }, + sky: { + 300: '#66ccf4', + 400: '#37bcf1' + } }, spacing: { '104': '26rem', diff --git a/tests/Models/ItemTest.php b/tests/Models/ItemTest.php index abef4ced0..bdc521dda 100644 --- a/tests/Models/ItemTest.php +++ b/tests/Models/ItemTest.php @@ -148,6 +148,27 @@ public function testMergedAuthorityNamesAndAuthors() $this->assertEquals('Philips Wouwerman', $data['author'][1]); } + public function testAuthorsWithAuthoritiesAttribute() + { + $item = factory(Item::class)->make([ + 'author' => 'Philips Wouwerman; Vladimír Boudník; Mikuláš Galanda' + ]); + $authority = factory(Authority::class)->create([ + 'name' => 'Boudník, Vladimír', + ]); + $item->authorities()->attach($authority); + + $data = $item->authors_with_authorities; + $this->assertCount(3, $data); + + $this->assertEquals('Boudník, Vladimír', $data[0]->name); + $this->assertInstanceOf(Authority::class, $data[0]->authority); + $this->assertEquals($authority->id, $data[0]->authority->id); + + $this->assertEquals('Philips Wouwerman', $data[1]->name); + $this->assertObjectNotHasAttribute('authority', $data[1]); + } + protected function createFreeItem() { return factory(Item::class)->make([ 'gallery' => 'Slovenská národná galéria, SNG',