Implement invites for spaces
ADD
app/Actions/CreateSpaceInviteAction.php
+<?php
+
+namespace App\Actions;
+
+use App\Exceptions\SpaceInviteAlreadyExistsException;
+use App\Exceptions\SpaceInviteInviteeAlreadyPresentException;
+use App\Models\SpaceInvite;
+use App\Models\User;
+
+class CreateSpaceInviteAction
+{
+ public function execute(int $spaceId, int $inviteeUserId, int $inviterUserId, string $role)
+ {
+ $inviteeUser = User::find($inviteeUserId);
+
+ if ($inviteeUser->spaces->contains($spaceId)) {
+ throw new SpaceInviteInviteeAlreadyPresentException();
+ }
+
+ $invite = SpaceInvite::firstOrNew([
+ 'space_id' => $spaceId,
+ 'invitee_user_id' => $inviteeUserId
+ ], [
+ 'space_id' => $spaceId,
+ 'invitee_user_id' => $inviteeUserId,
+ 'inviter_user_id' => $inviterUserId,
+ 'role' => $role
+ ]);
+
+ if ($invite->id) {
+ throw new SpaceInviteAlreadyExistsException();
+ }
+
+ $invite->save();
+
+ return $invite;
+ }
+}

ADD
app/Exceptions/SpaceInviteAlreadyExistsException.php
+<?php
+
+namespace App\Exceptions;
+
+use Exception;
+
+class SpaceInviteAlreadyExistsException extends Exception
+{
+ //
+}

ADD
app/Exceptions/SpaceInviteInviteeAlreadyPresentException.php
+<?php
+
+namespace App\Exceptions;
+
+use Exception;
+
+class SpaceInviteInviteeAlreadyPresentException extends Exception
+{
+ //
+}

MODIFY
app/Http/Controllers/SpaceController.php
app/Http/Controllers/SpaceController.php
namespace App\Http\Controllers;
+use App\Actions\CreateSpaceInviteAction;
+use App\Exceptions\SpaceInviteAlreadyExistsException;
+use App\Exceptions\SpaceInviteInviteeAlreadyPresentException;
use App\Models\Space;
+use App\Models\SpaceInvite;
+use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@@ -44,4 +49,43 @@ class SpaceController extends Controller
return redirect()->route('settings.spaces.index');
}
+
+ public function invite(Request $request, Space $space)
+ {
+ $authenticatedUser = Auth::user();
+
+ if ($authenticatedUser->cant('edit', $space)) {
+ return redirect()->route('settings.spaces.index');
+ }
+
+ $request->validate([
+ 'email' => 'required|exists:users,email',
+ 'role' => 'required|in:admin,regular'
+ ]);
+
+ $inviteeUser = User::where('email', $request->email)->first();
+
+ try {
+ (new CreateSpaceInviteAction())->execute(
+ $space->id,
+ $inviteeUser->id,
+ $authenticatedUser->id,
+ $request->role
+ );
+ } catch (SpaceInviteInviteeAlreadyPresentException $e) {
+ return redirect()
+ ->route('spaces.edit', ['space' => $space->id])
+ ->with('inviteStatus', 'present');
+ } catch (SpaceInviteAlreadyExistsException $e) {
+ return redirect()
+ ->route('spaces.edit', ['space' => $space->id])
+ ->with('inviteStatus', 'exists');
+ }
+
+ // TODO SEND MAIL
+
+ return redirect()
+ ->route('spaces.edit', ['space' => $space->id])
+ ->with('inviteStatus', 'success');
+ }
}

MODIFY
app/Models/Space.php
app/Models/Space.php
@@ -23,6 +23,11 @@ class Space extends Model
return $this->belongsToMany(User::class, 'user_space')->withPivot('role');
}
+ public function invites()
+ {
+ return $this->hasMany(SpaceInvite::class);
+ }
+
public function currency()
{
return $this->belongsTo(Currency::class);

ADD
app/Models/SpaceInvite.php
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class SpaceInvite extends Model
+{
+ protected $fillable = [
+ 'space_id',
+ 'inviter_user_id',
+ 'invitee_user_id',
+ 'role'
+ ];
+
+ // Relations
+ public function space()
+ {
+ return $this->belongsTo(Space::class);
+ }
+
+ public function invitee()
+ {
+ return $this->belongsTo(User::class, 'invitee_user_id');
+ }
+
+ public function inviter()
+ {
+ return $this->belongsTo(User::class, 'inviter_user_id');
+ }
+}

ADD
database/migrations/2020_07_15_195639_create_space_invites_table.php
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateSpaceInvitesTable extends Migration
+{
+ public function up(): void
+ {
+ Schema::create('space_invites', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedInteger('space_id');
+ $table->unsignedInteger('invitee_user_id');
+ $table->unsignedInteger('inviter_user_id');
+ $table->string('role');
+ $table->boolean('accepted')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('space_invites');
+ }
+}

MODIFY
resources/lang/en/actions.php
resources/lang/en/actions.php
@@ -10,5 +10,7 @@ return [
'verify' => 'Verify',
'yes' => 'Yes',
- 'no' => 'No'
+ 'no' => 'No',
+
+ 'invite' => 'Invite'
];

MODIFY
resources/lang/en/fields.php
resources/lang/en/fields.php
@@ -19,5 +19,7 @@ return [
'file' => 'File',
- 'color' => 'Color'
+ 'color' => 'Color',
+
+ 'role' => 'Role'
];

MODIFY
resources/lang/en/general.php
resources/lang/en/general.php
@@ -21,6 +21,9 @@ return [
'general' => 'General',
'members' => 'Members',
+ 'invites' => 'Invites',
+ 'invited_by' => 'Invited by',
+ 'invite_someone' => 'Invite someone',
'empty_state' => 'There aren\'t any :resource yet',

MODIFY
resources/lang/nl/actions.php
resources/lang/nl/actions.php
@@ -10,5 +10,7 @@ return [
'verify' => 'Controleer',
'yes' => 'Ja',
- 'no' => 'Nee'
+ 'no' => 'Nee',
+
+ 'invite' => 'Nodig uit'
];

MODIFY
resources/lang/nl/fields.php
resources/lang/nl/fields.php
@@ -19,5 +19,7 @@ return [
'file' => 'Bestand',
- 'color' => 'Kleur'
+ 'color' => 'Kleur',
+
+ 'role' => 'Rol'
];

MODIFY
resources/lang/nl/general.php
resources/lang/nl/general.php
@@ -21,6 +21,9 @@ return [
'general' => 'Algemeen',
'members' => 'Leden',
+ 'invites' => 'Uitnodigingen',
+ 'invited_by' => 'Uitgenodigd door',
+ 'invite_someone' => 'Nodig iemand uit',
'empty_state' => 'Er zijn nog geen :resource',

MODIFY
resources/views/spaces/edit.blade.php
resources/views/spaces/edit.blade.php
</div>
</div>
</div>
+ <div class="box mt-3">
+ <div class="box__section row">
+ <div class="row__column">
+ <h2>{{ __('general.invites') }}</h2>
+ </div>
+ <div class="row__column row__column--double">
+ @if (!count($space->invites))
+ <span>There aren't any invites</span>
+ @endif
+ @foreach ($space->invites as $i => $invite)
+ <div class="{{ $i > 0 ? 'mt-2' : '' }}">
+ <div class="color-dark mb-1">{{ $invite->invitee->name }}</div>
+ <div class="fs-sm">{{ __('general.invited_by') }} {{ $invite->inviter->name }}</div>
+ </div>
+ @endforeach
+ </div>
+ </div>
+ </div>
+ <div class="box mt-3">
+ <div class="box__section row">
+ <div class="row__column">
+ <h2>{{ __('general.invite_someone') }}</h2>
+ </div>
+ <div class="row__column row__column--double">
+ @if (session('inviteStatus'))
+ @switch(session('inviteStatus'))
+ @case('success')
+ <div class="color-green mb-2">An invite has been sent</div>
+ @break
+
+ @case('present')
+ <div class="color-red mb-2">User is already part of space</div>
+ @break
+
+ @case('exists')
+ <div class="color-red mb-2">Invite has already been sent</div>
+ @break
+ @endswitch
+ @endif
+ <form method="POST" action="/spaces/{{ $space->id }}/invite">
+ {{ csrf_field() }}
+ <div class="input input--small">
+ <label>{{ __('fields.email') }}</label>
+ <input type="email" name="email" />
+ @include('partials.validation_error', ['payload' => 'email'])
+ </div>
+ <div class="input input--small">
+ <label>{{ __('fields.role') }}</label>
+ <select name="role">
+ <option value="admin">Admin</option>
+ <option value="regular" selected>Regular</option>
+ </select>
+ @include('partials.validation_error', ['payload' => 'role'])
+ </div>
+ <button class="button">{{ __('actions.invite') }}</button>
+ </form>
+ </div>
+ </div>
+ </div>
</div>
@endsection

MODIFY
routes/web.php
routes/web.php
@@ -124,6 +124,7 @@ Route::group(['middleware' => ['auth']], function () {
Route::get('/spaces/{space}', [SpaceController::class, 'show'])->name('show');
Route::get('/spaces/{space}/edit', [SpaceController::class, 'edit'])->name('edit');
Route::post('/spaces/{space}/update', [SpaceController::class, 'update'])->name('update');
+ Route::post('/spaces/{space}/invite', [SpaceController::class, 'invite'])->name('invite');
});
Route::name('ideas.')->group(function () {

Implement invites for spaces