recurring meetup events

This commit is contained in:
HolgerHatGarKeineNode
2023-02-07 15:18:47 +01:00
parent 5b475cc660
commit a93598ef2e
23 changed files with 502 additions and 36 deletions

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\meetup;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
class MeetupController extends Controller
{
/**
* Display a listing of the resource.
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
$myMeetupIds = User::query()->find($request->input('user_id'))->meetups->pluck('id');
return Meetup::query()
->select('id', 'name', 'city_id')
->with([
'city',
])
->whereIn('id', $myMeetupIds->toArray())
->orderBy('name')
->when(
$request->search,
fn(Builder $query) => $query
->where('name', 'like', "%{$request->search}%")
->orWhereHas('city',
fn(Builder $query) => $query->where('cities.name', 'ilike', "%{$request->search}%"))
)
->when(
$request->exists('selected'),
fn(Builder $query) => $query->whereIn('id', $request->input('selected', [])),
fn(Builder $query) => $query->limit(10)
)
->get()
->map(function (Meetup $meetup) {
$meetup->profile_image = $meetup->getFirstMediaUrl('logo', 'thumb');
return $meetup;
});
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
*
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param \App\Models\meetup $meetup
*
* @return \Illuminate\Http\Response
*/
public function show(meetup $meetup)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\meetup $meetup
*
* @return \Illuminate\Http\Response
*/
public function update(Request $request, meetup $meetup)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\meetup $meetup
*
* @return \Illuminate\Http\Response
*/
public function destroy(meetup $meetup)
{
//
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace App\Http\Livewire\Meetup\Form;
use App\Models\MeetupEvent;
use App\Support\Carbon;
use Livewire\Component;
use WireUi\Traits\Actions;
class MeetupEventForm extends Component
{
use Actions;
public string $country;
public ?MeetupEvent $meetupEvent = null;
public bool $recurring = false;
public int $repetitions = 52;
public array $series = [];
public function rules()
{
return [
'meetupEvent.meetup_id' => 'required',
'meetupEvent.start' => 'required',
'meetupEvent.location' => 'string|nullable',
'meetupEvent.description' => 'string|nullable',
'meetupEvent.link' => 'string|url|nullable',
'series.*.start' => 'required',
'recurring' => 'bool',
'repetitions' => 'numeric|min:1',
];
}
public function mount()
{
if (!$this->meetupEvent) {
$this->meetupEvent = new MeetupEvent(
[
'start' => now()
->startOfDay()
->addHours(17),
]
);
}
}
public function updatedMeetupEventStart($value)
{
if ($this->recurring) {
$this->updatedRecurring(true);
}
}
public function updatedRecurring($value)
{
if ($value && $this->meetupEvent->start) {
$this->series = [];
for ($i = 0; $i < $this->repetitions; $i++) {
$this->series[] = [
'start' => $this->meetupEvent->start->addWeeks($i + 1)
->toDateTimeString(),
];
}
}
}
public function updatedRepetitions($value)
{
if ($this->recurring) {
$this->updatedRecurring(true);
}
}
public function submit()
{
$this->validate();
if (!$this->meetupEvent->id) {
$hasAppointmentsOnThisDate = MeetupEvent::query()
->where('meetup_id', $this->meetupEvent->meetup_id)
->where('start', '>', Carbon::parse($this->meetupEvent->start)
->startOfDay())
->where('start', '<', Carbon::parse($this->meetupEvent->start)
->endOfDay())
->exists();
if ($hasAppointmentsOnThisDate) {
$this->notification()
->warning(__('There is already an event on this date. Please choose another date or delete the existing events.'));
return;
}
}
$this->meetupEvent->save();
if (!$this->meetupEvent->id && $this->recurring) {
foreach ($this->series as $event) {
$hasAppointmentsOnThisDate = MeetupEvent::query()
->where('meetup_id', $this->meetupEvent->meetup_id)
->where('start', '>', Carbon::parse($event['start'])
->startOfDay())
->where('start', '<', Carbon::parse($event['start'])
->endOfDay())
->exists();
if ($hasAppointmentsOnThisDate) {
continue;
}
$this->meetupEvent->replicate()
->fill($event)
->saveQuietly();
}
}
$this->notification()
->success(__('Event saved successfully.'));
return to_route('meetup.table.meetupEvent', ['country' => $this->country]);
}
public function render()
{
return view('livewire.meetup.form.meetup-event-form');
}
}

View File

@@ -32,7 +32,14 @@ class MeetupEventTable extends DataTableComponent
];
})
->setColumnSelectStatus(false)
->setPerPage(10);
->setPerPage(10)
->setConfigurableAreas([
'toolbar-left-end' => [
'columns.meetup_events.areas.toolbar-left-end', [
'country' => $this->country,
],
],
]);
}
public function filters(): array
@@ -53,7 +60,7 @@ class MeetupEventTable extends DataTableComponent
public function columns(): array
{
return [
$columns = [
Column::make(__('Meetup'), 'meetup.name')
->format(
fn($value, $row, Column $column) => view('columns.meetup_events.name')
@@ -72,12 +79,22 @@ class MeetupEventTable extends DataTableComponent
)
->sortable()
->collapseOnMobile(),
Column::make(__('Link'), 'link')
Column::make(__('Link'))
->label(
fn($row, Column $column) => view('columns.meetup_events.link')
->withRow($row)
),
];
$adminColumns = auth()->check() ? [
Column::make(__('Actions'))
->label(
fn($row, Column $column) => view('columns.meetup_events.manage')
->withRow($row)
),
] : [];
return array_merge($columns, $adminColumns);
}
public function builder(): Builder

View File

@@ -4,6 +4,7 @@ namespace App\Observers;
use App\Models\MeetupEvent;
use App\Traits\TwitterTrait;
use Illuminate\Support\Facades\Log;
class MeetupEventObserver
{
@@ -18,6 +19,7 @@ class MeetupEventObserver
*/
public function created(MeetupEvent $meetupEvent)
{
try {
if (config('feeds.services.twitterAccountId')) {
$this->setNewAccessToken(1);
@@ -36,6 +38,9 @@ class MeetupEventObserver
$this->postTweet($text);
}
} catch (\Exception $e) {
Log::error($e->getMessage());
}
}
/**

View File

@@ -18,6 +18,17 @@ class Carbon extends CarbonImmutable
->format('H:i');
}
public function asDayNameAndMonthName(): string
{
$dt = $this->timezone(config('app.user-timezone'))->locale('de');
return sprintf("%s, %s. week of %s [%s]",
$dt->dayName,
$dt->weekNumberInMonth,
$dt->monthName,
$dt->timezoneAbbreviatedName
);
}
public function asDateTime(): string
{
$dt = $this->timezone(config('app.user-timezone'))->locale('de');

View File

@@ -711,5 +711,12 @@
"Unfortunately I cannot come": "Leider kann ich doch nicht kommen",
"Link to participate": "Link zur Teilnahme",
"Copied!": "Kopiert!",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "Zum Beispiel ein Link zu einer Location auf Google Maps oder ein Link zu einer Website. (nicht dein Telegram-Gruppen-Link)"
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "Zum Beispiel ein Link zu einer Location auf Google Maps oder ein Link zu einer Website. (nicht dein Telegram-Gruppen-Link)",
"There is already an event on this date. Please choose another date or delete the existing events.": "Es gibt bereits einen Termin an diesem Datum. Bitte wähle ein anderes Datum oder lösche die bestehenden Termine.",
"Event saved successfully.": "Termin erfolgreich gespeichert.",
"no authorization": "keine Berechtigung",
"Recurring appointment": "Wiederkehrender Termin",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "Die Termine werden in der Datenbank als neue Einträge angelegt. Bitte sei vorsichtig mit dieser Funktion, sonst musst du alle Termine, ändern oder löschen, wenn du einen Fehler machst.",
"Number of repetitions": "Anzahl der Wiederholungen",
"Recurring appointments": "Wiederkehrende Termine"
}

View File

@@ -706,5 +706,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -706,5 +706,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -707,5 +707,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -681,5 +681,12 @@
"Unfortunately I cannot come": "",
"Link to participate": "",
"Copied!": "",
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": ""
"For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)": "",
"There is already an event on this date. Please choose another date or delete the existing events.": "",
"Event saved successfully.": "",
"no authorization": "",
"Recurring appointment": "",
"The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.": "",
"Number of repetitions": "",
"Recurring appointments": ""
}

View File

@@ -0,0 +1,6 @@
<div class="w-full mb-4 md:w-auto md:mb-0">
<x-button :href="route('meetup.event.form', ['country' => $country, 'meetupEvent' => null])">
<i class="fa fa-thin fa-plus"></i>
{{ __('Register Meetup date') }}
</x-button>
</div>

View File

@@ -0,0 +1,16 @@
<div class="flex flex-col space-y-1">
<div>
@if(auth()->user()->can('update', $row))
<x-button
primary
xs
:href="route('meetup.event.form', ['country' => $row->meetup->city->country, 'meetupEvent' => $row])"
>
<i class="fa fa-thin fa-edit mr-2"></i>
{{ __('Edit') }}
</x-button>
@else
<x-badge>{{ __('no authorization') }}</x-badge>
@endif
</div>
</div>

View File

@@ -5,5 +5,4 @@
{{ $row->name }}
</div>
</div>
</a>

View File

@@ -0,0 +1,13 @@
@props([
'for',
'label',
])
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-700 sm:pt-5">
<label for="{{ $for }}"
class="block text-sm font-medium text-gray-100 sm:mt-px sm:pt-2">
{{ $label }}
</label>
<div class="mt-1 sm:mt-0 sm:col-span-2">
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,105 @@
<div class="container p-4 mx-auto bg-21gray my-2">
<div class="pb-5 flex flex-row justify-between">
<h3 class="text-lg font-medium leading-6 text-gray-200">{{ __('Meetup Event') }}</h3>
<div>
<x-button :href="route('meetup.table.meetupEvent', ['country' => $country])">{{ __('Back') }}</x-button>
</div>
</div>
<form class="space-y-8 divide-y divide-gray-700 pb-24">
<div class="space-y-8 divide-y divide-gray-700 sm:space-y-5">
<div class="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<x-input.group :for="md5('meetup_id')" :label="__('Meetup')">
<x-select
autocomplete="off"
wire:model.debounce="meetupEvent.meetup_id"
:placeholder="__('Meetup')"
:async-data="[
'api' => route('api.meetup.index'),
'method' => 'GET', // default is GET
'params' => ['user_id' => auth()->id()], // default is []
]"
:template="[
'name' => 'user-option',
'config' => ['src' => 'profile_image']
]"
option-label="name"
option-value="id"
option-description="city.name"
/>
</x-input.group>
<x-input.group :for="md5('meetupEvent.start')" :label="__('Start')">
<x-datetime-picker
:clearable="false"
time-format="24"
timezone="UTC"
user-timezone="{{ config('app.user-timezone') }}"
autocomplete="off"
wire:model.debounce="meetupEvent.start"
display-format="DD-MM-YYYY HH:mm"
:placeholder="__('Start')"/>
</x-input.group>
@if(!$meetupEvent->id)
<x-input.group :for="md5('recurring')" :label="__('Recurring appointment')">
<x-toggle :label="__('Recurring appointment')" wire:model="recurring"/>
<p class="text-xs text-amber-400 py-2">{{ __('The recurring appointments are created in the database as new entries. Please be careful with this function, otherwise you will have to change or delete all the appointments you have created manually if you make an error.') }}</p>
</x-input.group>
@endif
@if($recurring)
<x-input.group :for="md5('repetitions')" :label="__('Number of repetitions')">
<x-input type="number" autocomplete="off" wire:model.debounce="repetitions"
:placeholder="__('Number of repetitions')"/>
</x-input.group>
@endif
<x-input.group :for="md5('meetupEvent.location')" :label="__('Location')">
<x-input autocomplete="off" wire:model.debounce="meetupEvent.location"
:placeholder="__('Location')"/>
</x-input.group>
<x-input.group :for="md5('meetupEvent.description')" :label="__('Description')">
<x-textarea autocomplete="off" wire:model.debounce="meetupEvent.description"
:placeholder="__('Description')"/>
</x-input.group>
<x-input.group :for="md5('meetupEvent.link')" :label="__('Link')">
<x-input type="url" autocomplete="off" wire:model.debounce="meetupEvent.link"
:placeholder="__('Link')"
:hint="__('For example, a link to a location on Google Maps or a link to a website. (not your Telegram group link)')"/>
</x-input.group>
<x-input.group :for="md5('grid')" :label="__('Recurring appointments')">
@if($meetupEvent->start && $recurring)
<div class="grid grid-cols-1 lg:grid-cols-3 gap-2">
@for($i = 0; $i < $repetitions; $i++)
<x-datetime-picker wire:key="event_{{ $i }}"
:label="\App\Support\Carbon::parse($series[$i]['start'])->asDayNameAndMonthName()"
:clearable="false"
time-format="24"
timezone="UTC"
user-timezone="{{ config('app.user-timezone') }}"
autocomplete="off"
wire:model.debounce="series.{{ $i }}.start"
display-format="DD-MM-YYYY HH:mm"
:placeholder="__('Start')"/>
@endfor
</div>
@endif
</x-input.group>
<x-input.group :for="md5('meetupEvent.link')" :label="__('Action')">
<x-button primary wire:click="submit">
<i class="fa fa-thin fa-save"></i>
{{ __('Save') }}
</x-button>
</x-input.group>
</div>
</div>
</form>
</div>

View File

@@ -30,7 +30,7 @@
</x-button>
</div>
<div>
<x-button xs amber href="/nova/resources/meetup-events" target="_blank">
<x-button xs amber :href="route('meetup.event.form', ['country' => $country ?? 'de'])">
<i class="fa fa-thin fa-plus"></i>
{{ __('Register Meetup date') }}
</x-button>

View File

@@ -27,11 +27,7 @@ Route::middleware([])
->as('api.')
->group(function () {
Route::resource('countries', \App\Http\Controllers\Api\CountryController::class);
});
Route::middleware([])
->as('api.')
->group(function () {
Route::resource('meetup', \App\Http\Controllers\Api\MeetupController::class);
Route::resource('languages', \App\Http\Controllers\Api\LanguageController::class);
Route::get('meetups', function () {
return \App\Models\Meetup::query()

View File

@@ -151,6 +151,10 @@ Route::middleware([])
Route::get('overview', \App\Http\Livewire\Meetup\MeetupTable::class)
->name('table.meetup');
Route::get('/meetup-events/form/{meetupEvent?}', \App\Http\Livewire\Meetup\Form\MeetupEventForm::class)
->name('event.form')
->middleware('needMeetup');
Route::get('/meetup-events/l/{meetupEvent}', \App\Http\Livewire\Meetup\LandingPageEvent::class)
->name('event.landing');