proximity search for book cases

This commit is contained in:
Benjamin Takats
2022-12-07 18:16:07 +01:00
parent ce8f87db6f
commit c8bd4a63c5
14 changed files with 331 additions and 115 deletions

View File

@@ -35,8 +35,8 @@ class SyncOpenBooks extends Command
],
[
'title' => $case['title'],
'lat' => (float)$case['lat'],
'lon' => (float)$case['lon'],
'latitude' => (float)$case['lat'],
'longitude' => (float)$case['lon'],
'address' => $case['address'],
'type' => $case['type'],
'open' => $case['open'],

View File

@@ -4,9 +4,14 @@ namespace App\Http\Livewire\Frontend;
use App\Models\BookCase;
use Livewire\Component;
use Livewire\WithFileUploads;
class CommentBookCase extends Component
{
use WithFileUploads;
public $photo;
public string $c = 'de';
public BookCase $bookCase;
@@ -16,28 +21,26 @@ class CommentBookCase extends Component
return view('livewire.frontend.comment-book-case');
}
public function save()
{
$this->validate([
'photo' => 'image|max:4096', // 4MB Max
]);
$this->bookCase
->addMedia($this->photo)
->toMediaCollection('images');
return to_route('comment.bookcase', ['bookCase' => $this->bookCase->id]);
}
protected function url_to_absolute($url)
{
// Determine request protocol
$request_protocol = $request_protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
// If dealing with a Protocol Relative URL
if (stripos($url, '//') === 0) {
if (str($url)->contains('http')) {
return $url;
}
// If dealing with a Root-Relative URL
if (stripos($url, '/') === 0) {
return $request_protocol.'://'.$_SERVER['HTTP_HOST'].$url;
if (!str($url)->contains('http')) {
return str($url)->prepend('https://');
}
// If dealing with an Absolute URL, just return it as-is
if (stripos($url, 'http') === 0) {
return $url;
}
// If dealing with a relative URL,
// and attempt to handle double dot notation ".."
do {
$url = preg_replace('/[^\/]+\/\.\.\//', '', $url, 1, $count);
} while ($count);
// Return the absolute version of a Relative URL
return $request_protocol.'://'.$_SERVER['HTTP_HOST'].'/'.$url;
}
}

View File

@@ -3,16 +3,23 @@
namespace App\Http\Livewire\Tables;
use App\Models\BookCase;
use App\Models\OrangePill;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
use Rappasoft\LaravelLivewireTables\Views\Filters\TextFilter;
use WireUi\Traits\Actions;
class BookCaseTable extends DataTableComponent
{
use Actions;
public bool $viewingModal = false;
public $currentModal;
public array $orangepill = [
'amount' => 1,
'date' => null,
'amount' => 1,
'date' => null,
'comment' => '',
];
protected $model = BookCase::class;
@@ -37,12 +44,27 @@ class BookCaseTable extends DataTableComponent
->setPerPage(50);
}
public function filters(): array
{
return [
TextFilter::make('By IDs', 'byids')
->filter(function (Builder $builder, string $value) {
$builder->whereIn('id', str($value)->explode(','));
}),
];
}
public function columns(): array
{
return [
Column::make("Name", "title")
->sortable()
->searchable(),
->searchable(
function (Builder $query, $searchTerm) {
$query->where('title', 'ilike', '%'.$searchTerm.'%');
}
),
Column::make("Adresse", "address")
->sortable()
->searchable(),
@@ -61,27 +83,20 @@ class BookCaseTable extends DataTableComponent
private function url_to_absolute($url)
{
// Determine request protocol
$request_protocol = $request_protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
// If dealing with a Protocol Relative URL
if (stripos($url, '//') === 0) {
if (str($url)->contains('http')) {
return $url;
}
// If dealing with a Root-Relative URL
if (stripos($url, '/') === 0) {
return $request_protocol.'://'.$_SERVER['HTTP_HOST'].$url;
if (!str($url)->contains('http')) {
return str($url)->prepend('https://');
}
// If dealing with an Absolute URL, just return it as-is
if (stripos($url, 'http') === 0) {
return $url;
}
// If dealing with a relative URL,
// and attempt to handle double dot notation ".."
do {
$url = preg_replace('/[^\/]+\/\.\.\//', '', $url, 1, $count);
} while ($count);
// Return the absolute version of a Relative URL
return $request_protocol.'://'.$_SERVER['HTTP_HOST'].'/'.$url;
}
public function builder(): Builder
{
return BookCase::query()
->withCount([
'orangePills',
]);
}
public function viewHistoryModal($modelId): void
@@ -90,6 +105,25 @@ class BookCaseTable extends DataTableComponent
$this->currentModal = BookCase::findOrFail($modelId);
}
public function submit(): void
{
$this->validate([
'orangepill.amount' => 'required|numeric',
'orangepill.date' => 'required|date',
]);
$orangePill = OrangePill::create([
'user_id' => auth()->id(),
'book_case_id' => $this->currentModal->id,
'amount' => $this->orangepill['amount'],
'date' => $this->orangepill['date'],
]);
if ($this->orangepill['comment']) {
$this->currentModal->comment($this->orangepill['comment']);
}
$this->resetModal();
$this->emit('refreshDatatable');
}
public function resetModal(): void
{
$this->reset('viewingModal', 'currentModal');

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Tables;
use App\Models\BookCase;
use App\Models\City;
use Illuminate\Database\Eloquent\Builder;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
@@ -87,4 +88,21 @@ class CityTable extends DataTableComponent
]
]);
}
public function proximitySearchForBookCases($id)
{
$city = City::query()
->find($id);
$query = BookCase::radius($city->latitude, $city->longitude, 5);
return to_route('search.bookcases', [
'#table',
'country' => $this->country,
'table' => [
'filters' => [
'byids' => $query->pluck('id')
->implode(',')
],
]
]);
}
}

View File

@@ -2,35 +2,63 @@
namespace App\Models;
use Akuechler\Geoly;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Comments\Models\Concerns\HasComments;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
class BookCase extends Model
class BookCase extends Model implements HasMedia
{
use HasFactory;
use HasComments;
use InteractsWithMedia;
use Geoly;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'id' => 'integer',
'lat' => 'double',
'lon' => 'array',
'digital' => 'boolean',
'id' => 'integer',
'lat' => 'double',
'lon' => 'array',
'digital' => 'boolean',
'deactivated' => 'boolean',
];
public function registerMediaConversions(Media $media = null): void
{
$this
->addMediaConversion('preview')
->fit(Manipulations::FIT_CROP, 300, 300)
->nonQueued();
$this->addMediaConversion('thumb')
->fit(Manipulations::FIT_CROP, 130, 130)
->width(130)
->height(130);
}
public function registerMediaCollections(): void
{
$this->addMediaCollection('images');
}
public function orangePills(): HasMany
{
return $this->hasMany(OrangePill::class);
}
/*
* This string will be used in notifications on what a new comment
* was made.

View File

@@ -14,13 +14,9 @@ class CreateBookCasesTable extends Migration
{
Schema::create('book_cases', function (Blueprint $table) {
$table->id();
$table->boolean('orange_pilled')
->default(false);
$table->unsignedInteger('orange_pilled_amount')
->default(0);
$table->string('title');
$table->double('lat');
$table->double('lon');
$table->double('latitude');
$table->double('longitude');
$table->text('address')
->nullable();
$table->string('type');

View File

@@ -1,5 +1,5 @@
{
"Orange Pill Book Case": "Bücher-Schrank wurde orange pilled.",
"Orange Pill Book Case": "Wie viele Bitcoin-Bücher hast du hinzu gefügt?",
"Book": "Buch",
"Article": "Artikel",
"Markdown Article": "Interner Artikel",

View File

@@ -1,14 +1,14 @@
<div class="flex flex-col space-y-1">
@auth
@if($row->orange_pilled)
@if($row->orange_pills_count > 0)
<img class="aspect-auto max-h-12" src="{{ asset('img/social_credit_plus.webp') }}" alt="">
@endif
@if(!$row->orange_pilled)
@if($row->orange_pills_count < 1)
<img class="aspect-auto max-h-12" src="{{ asset('img/social_credit_minus.webp') }}" alt="">
@endif
<div class="flex items-center space-x-1">
<x-button wire:click="viewHistoryModal({{ $row->id }})">💊 Orange Pill Now</x-button>
<x-button :href="route('comment.bookcase', ['bookCase' => $row->id])">Kommentare</x-button>
<x-button primary class="text-21gray" wire:click="viewHistoryModal({{ $row->id }})">💊 Orange Pill Now</x-button>
<x-button :href="route('comment.bookcase', ['bookCase' => $row->id])">Details</x-button>
</div>
@else
<div>

View File

@@ -1 +1,14 @@
<x-button amber wire:click="proximitySearch({{ $row->id }})">Umkreis-Suche {{ $row->name }} (100km)</x-button>
<div class="flex flex-col space-y-1">
<div>
<x-button amber wire:click="proximitySearch({{ $row->id }})" class="text-21gray">
<i class="fa fa-thin fa-person-chalkboard mr-2"></i>
Umkreis-Suche Kurs-Termin {{ $row->name }}(100km)
</x-button>
</div>
<div>
<x-button amber wire:click="proximitySearchForBookCases({{ $row->id }})" class="text-21gray">
<i class="fa fa-thin fa-book mr-2"></i>
Umkreis-Suche Bücher-Schrank {{ $row->name }} (5km)
</x-button>
</div>
</div>

View File

@@ -9,6 +9,7 @@
@googlefonts
<!-- Scripts -->
<script src="https://kit.fontawesome.com/03bc14bd1e.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.js"></script>
@mapscripts
<wireui:scripts/>
<x-comments::scripts />

View File

@@ -9,6 +9,7 @@
@googlefonts
<!-- Scripts -->
<script src="https://kit.fontawesome.com/03bc14bd1e.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/smoothscroll-polyfill@0.4.4/dist/smoothscroll.js"></script>
<wireui:scripts/>
<x-comments::scripts />
@vite(['resources/css/app.css', 'resources/js/app.js'])

View File

@@ -74,12 +74,12 @@
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div
class="relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:border-gray-400">
class="relative flex items-center space-x-3 rounded-lg border border-gray-300 bg-white px-6 py-5 shadow-sm">
{{--<div class="flex-shrink-0">
<img class="h-10 w-10 rounded-full" src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" alt="">
</div>--}}
<div class="min-w-0 flex-1">
<div class="focus:outline-none">
<div class="focus:outline-none space-y-2">
<p class="text-sm font-medium text-gray-900">Name</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->title }}</p>
<p class="text-sm font-medium text-gray-900">Link</p>
@@ -89,20 +89,165 @@
</p>
<p class="text-sm font-medium text-gray-900">Adresse</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->address }}</p>
<p class="text-sm font-medium text-gray-900">Art</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->type }}</p>
<p class="text-sm font-medium text-gray-900">Geöffnet</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->open }}</p>
<p class="text-sm font-medium text-gray-900">Kontakt</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->contact }}</p>
<p class="text-sm font-medium text-gray-900">Information</p>
<p class="truncate text-sm text-gray-500">{{ $bookCase->comment }}</p>
<p class="text-sm font-medium text-gray-900">Neues Foto hochladen</p>
<form wire:submit.prevent="save">
<div class="text-sm text-gray-500">
<input type="file" wire:model="photo">
@error('photo') <span class="error">{{ $message }}</span> @enderror
<x-button xs secondary type="submit">Hochladen</x-button>
</div>
</form>
@if($bookCase->getMedia('images')->count() > 0)
<div
x-data="{
skip: 3,
atBeginning: false,
atEnd: false,
next() {
this.to((current, offset) => current + (offset * this.skip))
},
prev() {
this.to((current, offset) => current - (offset * this.skip))
},
to(strategy) {
let slider = this.$refs.slider
let current = slider.scrollLeft
let offset = slider.firstElementChild.getBoundingClientRect().width
slider.scrollTo({ left: strategy(current, offset), behavior: 'smooth' })
},
focusableWhenVisible: {
'x-intersect:enter'() {
this.$el.removeAttribute('tabindex')
},
'x-intersect:leave'() {
this.$el.setAttribute('tabindex', '-1')
},
},
disableNextAndPreviousButtons: {
'x-intersect:enter.threshold.05'() {
let slideEls = this.$el.parentElement.children
// If this is the first slide.
if (slideEls[0] === this.$el) {
this.atBeginning = true
// If this is the last slide.
} else if (slideEls[slideEls.length-1] === this.$el) {
this.atEnd = true
}
},
'x-intersect:leave.threshold.05'() {
let slideEls = this.$el.parentElement.children
// If this is the first slide.
if (slideEls[0] === this.$el) {
this.atBeginning = false
// If this is the last slide.
} else if (slideEls[slideEls.length-1] === this.$el) {
this.atEnd = false
}
},
},
}"
class="flex w-full flex-col"
>
<div
x-on:keydown.right="next"
x-on:keydown.left="prev"
tabindex="0"
role="region"
aria-labelledby="carousel-label"
class="flex space-x-6"
>
<h2 id="carousel-label" class="sr-only" hidden>Carousel</h2>
<!-- Prev Button -->
<button
x-on:click="prev"
class="text-6xl"
:aria-disabled="atBeginning"
:tabindex="atEnd ? -1 : 0"
:class="{ 'opacity-50 cursor-not-allowed': atBeginning }"
>
<span aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3"><path
stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
</span>
<span class="sr-only">Skip to previous slide page</span>
</button>
<span id="carousel-content-label" class="sr-only" hidden>Carousel</span>
<ul
x-ref="slider"
tabindex="0"
role="listbox"
aria-labelledby="carousel-content-label"
class="flex w-full snap-x snap-mandatory overflow-x-scroll"
>
@foreach($bookCase->getMedia('images') as $image)
<li x-bind="disableNextAndPreviousButtons"
class="flex w-1/3 shrink-0 snap-start flex-col items-center justify-center p-2"
role="option">
<a href="{{ $image->getUrl() }}" target="_blank">
<img class="mt-2 w-full" src="{{ $image->getUrl() }}"
alt="placeholder image">
</a>
<button x-bind="focusableWhenVisible" class="px-4 py-2 text-sm">
#{{ $loop->iteration }} Bild
</button>
</li>
@endforeach
</ul>
<!-- Next Button -->
<button
x-on:click="next"
class="text-6xl"
:aria-disabled="atEnd"
:tabindex="atEnd ? -1 : 0"
:class="{ 'opacity-50 cursor-not-allowed': atEnd }"
>
<span aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-600"
fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="3"><path stroke-linecap="round"
stroke-linejoin="round"
d="M9 5l7 7-7 7"/></svg>
</span>
<span class="sr-only">Skip to next slide page</span>
</button>
</div>
</div>
@endif
</div>
</div>
</div>
<div class="rounded">
<div class="rounded" wire:ignore>
@map([
'lat' => $bookCase->lat,
'lng' => $bookCase->lon,
'lat' => $bookCase->latitude,
'lng' => $bookCase->longitude,
'zoom' => 24,
'markers' => [
[
'title' => $bookCase->title,
'lat' => $bookCase->lat,
'lng' => $bookCase->lon,
'lat' => $bookCase->latitude,
'lng' => $bookCase->longitude,
'url' => 'https://gonoware.com',
'icon' => asset('img/btc-logo-6219386_1280.png'),
'icon_size' => [42, 42],

View File

@@ -1,15 +1,4 @@
<div class="bg-21gray flex flex-col h-screen justify-between">
<script src="{{ asset('earth/miniature.earth.js') }}"></script>
<style>
.earth-container::after {
content: "";
position: absolute;
height: 22%;
bottom: 4%;
left: 13%;
right: 13%;
}
</style>
{{-- HEADER --}}
<div>
<section class="w-full">
@@ -58,7 +47,7 @@
</div>
@endauth
</div>
<div class="flex lg:flex-row flex-col pt-4 md:pt-4 lg:pt-4">
<div class="flex lg:flex-row flex-col pt-4 md:pt-4 lg:pt-4 mt-12">
<div
class="w-full lg:w-1/2 flex lg:px-0 px-5 flex-col md:items-center lg:items-start justify-center -mt-12">
@@ -75,41 +64,6 @@
</a>
<p class="text-gray-400 font-normal mt-4">{{-- TEXT --}}</p>
</div>
<div
x-data="{
earth: null,
init() {
this.earth = new Earth(this.$refs.myearth, {
location : {lat: {{ $bookCases->first()->lat }}, lng: {{ $bookCases->first()->lon }}},
zoom: 1,
light: 'sun',
polarLimit: 0.6,
transparent : true,
mapSeaColor : 'RGBA(34, 34, 34,0.76)',
mapLandColor : '#F7931A',
mapBorderColor : '#5D5D5D',
mapBorderWidth : 0.25,
mapHitTest : true,
autoRotate: true,
autoRotateSpeed: 0.7,
autoRotateDelay: 500,
});
this.earth.addEventListener('ready', function() {
@foreach($bookCases as $city)
this.addMarker( {
mesh : ['Needle'],
location : { lat: {{ $city->lat }}, lng: {{ $city->lon }} },
});
@endforeach
});
}
}" class="hidden sm:inline-block w-1/2">
{{--<img src="https://cdn.devdojo.com/images/march2022/mesh-gradient1.png"
class="absolute lg:max-w-none max-w-3xl mx-auto mt-32 w-full h-full inset-0">--}}
<div x-ref="myearth" class="earth-container"></div>
</div>
</div>
</div>
</section>

View File

@@ -6,7 +6,27 @@
</x-slot>
<x-slot name="content">
<div class="space-y-4 mt-16 flex flex-col justify-center">
<div class="space-y-4 mt-16 flex flex-col justify-center min-h-[600px]">
<div class="my-4">
<div class="border-b border-gray-200 pb-5">
<h3 class="text-lg font-medium leading-6 text-gray-200">Bisher waren hier</h3>
</div>
<ul role="list" class="divide-y divide-gray-200">
@foreach($currentModal?->orangePills ?? [] as $orangePill)
<li class="flex py-4">
<img class="h-10 w-10 rounded-full" src="{{ $orangePill->user->profile_photo_url }}" alt="">
<div class="ml-3">
<p class="text-sm text-gray-200">
{{ $orangePill->user->name }} hat am {{ $orangePill->date->asDateTime() }} {{ $orangePill->amount }} Bitcoin-Bücher hinzugefügt
</p>
</div>
</li>
@endforeach
</ul>
</div>
<div class="col-span-6 sm:col-span-4">
<x-input
min="1"
@@ -36,6 +56,9 @@
<x-slot name="footer">
<x-jet-secondary-button wire:click="resetModal" wire:loading.attr="disabled">
@lang('Close')
</x-jet-secondary-button>
<x-jet-secondary-button wire:click="submit" wire:loading.attr="disabled">
💊 @lang('Orange Pill Now')
</x-jet-secondary-button>
</x-slot>