diff --git a/app/Http/Controllers/DynamicController.php b/app/Http/Controllers/DynamicController.php index 998dc5e..16f16af 100644 --- a/app/Http/Controllers/DynamicController.php +++ b/app/Http/Controllers/DynamicController.php @@ -54,7 +54,7 @@ class DynamicController extends Controller $dynamic->load([ 'ledgers.media', - 'participants', + 'participants' => fn($query) => $query->withPivot('display_name'), 'chat.messages.user', 'chat.messages.media' ]); diff --git a/app/Http/Controllers/LedgerController.php b/app/Http/Controllers/LedgerController.php index 3ec97a4..c50a2c0 100644 --- a/app/Http/Controllers/LedgerController.php +++ b/app/Http/Controllers/LedgerController.php @@ -64,7 +64,7 @@ class LedgerController extends Controller $activityService->updateCursor($request->user(), $ledger); - $dynamic->load('chat', 'participants'); + $dynamic->load(['chat', 'participants' => fn($query) => $query->withPivot('display_name')]); $ledger->load([ 'media', diff --git a/app/Http/Controllers/ParticipantController.php b/app/Http/Controllers/ParticipantController.php new file mode 100644 index 0000000..969b34c --- /dev/null +++ b/app/Http/Controllers/ParticipantController.php @@ -0,0 +1,24 @@ +validate([ + 'display_name' => ['required', 'string', 'max:255'], + ]); + + $participant = $dynamic->participants()->where('user_id', $request->user()->id)->firstOrFail(); + + $dynamic->participants()->updateExistingPivot($participant->id, [ + 'display_name' => $request->input('display_name'), + ]); + + return redirect()->back()->with('success', 'Display name updated successfully!'); + } +} diff --git a/app/Models/Dynamic.php b/app/Models/Dynamic.php index cebac43..75f41e4 100644 --- a/app/Models/Dynamic.php +++ b/app/Models/Dynamic.php @@ -21,7 +21,7 @@ class Dynamic extends Model public function participants(): BelongsToMany { - return $this->belongsToMany(User::class, 'participants')->withPivot('role'); + return $this->belongsToMany(User::class, 'participants')->withPivot('role', 'display_name'); } public function ledgers(): HasMany diff --git a/app/Models/Participant.php b/app/Models/Participant.php index 09219b1..621a6c7 100644 --- a/app/Models/Participant.php +++ b/app/Models/Participant.php @@ -12,5 +12,6 @@ class Participant extends Pivot 'user_id', 'dynamic_id', 'role', + 'display_name', ]; } diff --git a/app/Models/User.php b/app/Models/User.php index b507c99..afcd538 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -49,6 +49,13 @@ class User extends Authenticatable implements PasskeyUser return $this->hasMany(ReadCursor::class); } + public function displayNameFor(Dynamic $dynamic): string + { + $participant = $dynamic->participants()->where('user_id', $this->id)->first(); + + return $participant?->pivot?->display_name ?? $this->name; + } + /** * Get the attributes that should be cast. * diff --git a/database/migrations/2026_06_17_100000_add_display_name_to_participants_table.php b/database/migrations/2026_06_17_100000_add_display_name_to_participants_table.php new file mode 100644 index 0000000..9f799b2 --- /dev/null +++ b/database/migrations/2026_06_17_100000_add_display_name_to_participants_table.php @@ -0,0 +1,28 @@ +string('display_name')->nullable()->after('role'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('participants', function (Blueprint $table) { + $table->dropColumn('display_name'); + }); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 2683bea..424d4f0 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -56,9 +56,9 @@ class DatabaseSeeder extends Seeder ]); // Add participants (Test User is owner, Alice is owner, Bob is submissive/participant) - $velvetSanctuary->participants()->attach($testUser->id, ['role' => 'owner']); + $velvetSanctuary->participants()->attach($testUser->id, ['role' => 'owner', 'display_name' => 'The Master']); $velvetSanctuary->participants()->attach($alice->id, ['role' => 'owner']); - $velvetSanctuary->participants()->attach($bob->id, ['role' => 'participant']); + $velvetSanctuary->participants()->attach($bob->id, ['role' => 'participant', 'display_name' => 'Bitch Boi']); // Chat has been auto-created by the booted hook on Dynamic $velvetChat = $velvetSanctuary->chat; diff --git a/resources/css/components.css b/resources/css/components.css index 5e63a2c..7460f91 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -13,5 +13,6 @@ @import './components/password-input.css'; @import './components/auth-layout.css'; @import './components/chat.css'; -@import './components/lightbox.css'; -@import './components/invite-form.css'; +@import "./components/lightbox.css"; +@import "./components/invite-form.css"; +/*@import "./components/display-name-form.css";*/ diff --git a/resources/js/components/DisplayNameForm.vue b/resources/js/components/DisplayNameForm.vue new file mode 100644 index 0000000..326fd94 --- /dev/null +++ b/resources/js/components/DisplayNameForm.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/resources/js/components/ParticipantsList.vue b/resources/js/components/ParticipantsList.vue index 9c656da..61bb71e 100644 --- a/resources/js/components/ParticipantsList.vue +++ b/resources/js/components/ParticipantsList.vue @@ -3,6 +3,9 @@ defineProps<{ participants: Array<{ id: number; name: string; + pivot: { + display_name: string | null; + }; }>; }>(); @@ -16,7 +19,7 @@ defineProps<{ :key="participant.id" class="c-participants-list__item" > - {{ participant.name }} + {{ participant.pivot.display_name ?? participant.name }} diff --git a/resources/js/pages/Dynamics/Show.vue b/resources/js/pages/Dynamics/Show.vue index e56afcf..50cbc34 100644 --- a/resources/js/pages/Dynamics/Show.vue +++ b/resources/js/pages/Dynamics/Show.vue @@ -2,8 +2,10 @@ import Chat from '@/components/Chat.vue'; import ParticipantsList from '@/components/ParticipantsList.vue'; import LedgerList from '@/components/LedgerList.vue'; -import { Head, Link as InertiaLink } from '@inertiajs/vue3'; +import DisplayNameForm from '@/components/DisplayNameForm.vue'; +import { Head, Link as InertiaLink, usePage } from '@inertiajs/vue3'; import { route } from 'ziggy-js'; +import { computed } from 'vue'; const props = defineProps<{ dynamic: { @@ -11,7 +13,7 @@ const props = defineProps<{ name: string; rules: string; chat: any; - participants: Array<{ id: number; name: string }>; + participants: Array<{ id: number; name: string, pivot: { display_name: string | null } }>; ledgers: Array<{ id: number; name: string; @@ -23,6 +25,18 @@ const props = defineProps<{ isOwner: boolean; }>(); +const currentUser = computed(() => { + const page = usePage(); + const authUser = page.props.auth.user; + const participant = props.dynamic.participants.find(p => p.id === authUser.id); + return { + id: authUser.id, + name: authUser.name, + display_name: participant?.pivot?.display_name ?? null, + }; +}); + + const breadcrumbs = [ { name: 'Dynamics', @@ -65,6 +79,9 @@ const breadcrumbs = [ + + +
Invite User diff --git a/routes/web.php b/routes/web.php index 3670c34..5aa79e4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -25,6 +25,8 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::post('/dynamics/{dynamic}/invitations', [\App\Http\Controllers\DynamicInvitationController::class, 'store'])->name('dynamics.invitations.store'); Route::post('/chats/{chat}/messages', [MessageController::class, 'store'])->name('chats.messages.store'); + + Route::put('/dynamics/{dynamic}/participant', [\App\Http\Controllers\ParticipantController::class, 'update'])->name('dynamics.participant.update'); }); Route::get('/invitations/accept/{token}', [\App\Http\Controllers\DynamicInvitationController::class, 'accept']) diff --git a/tests/Feature/ParticipantTest.php b/tests/Feature/ParticipantTest.php new file mode 100644 index 0000000..8cf9adf --- /dev/null +++ b/tests/Feature/ParticipantTest.php @@ -0,0 +1,76 @@ +create(['name' => 'Alice']); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($user->id, ['role' => 'participant']); + + expect($user->displayNameFor($dynamic))->toBe('Alice'); +}); + +test('user displayNameFor returns custom display name when set', function () { + $user = User::factory()->create(['name' => 'Alice']); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($user->id, [ + 'role' => 'participant', + 'display_name' => 'Ally', + ]); + + expect($user->displayNameFor($dynamic))->toBe('Ally'); +}); + +test('participant can update their display name', function () { + $user = User::factory()->create(['name' => 'Alice']); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($user->id, ['role' => 'participant']); + + $this->actingAs($user); + + $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + 'display_name' => 'Ally', + ]); + + $response->assertRedirect(); + + // Check database + $this->assertDatabaseHas('participants', [ + 'user_id' => $user->id, + 'dynamic_id' => $dynamic->id, + 'display_name' => 'Ally', + ]); + + // Check display name method + expect($user->displayNameFor($dynamic))->toBe('Ally'); +}); + +test('display name update requires display_name parameter', function () { + $user = User::factory()->create(['name' => 'Alice']); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($user->id, ['role' => 'participant']); + + $this->actingAs($user); + + $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + 'display_name' => '', + ]); + + $response->assertSessionHasErrors(['display_name']); +}); + +test('non participant cannot update display name', function () { + $user = User::factory()->create(['name' => 'Alice']); + $nonParticipant = User::factory()->create(['name' => 'Bob']); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($user->id, ['role' => 'participant']); + + $this->actingAs($nonParticipant); + + $response = $this->put(route('dynamics.participant.update', $dynamic->id), [ + 'display_name' => 'Bobby', + ]); + + $response->assertStatus(404); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 941e024..97e9b1a 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -16,7 +16,7 @@ use Tests\TestCase; pest()->extend(TestCase::class) ->use(RefreshDatabase::class) - ->in('Feature'); + ->in('Feature', 'Browser'); /* |--------------------------------------------------------------------------