different names in different dynamics
Some checks failed
linter / quality (push) Failing after 1m9s
tests / ci (8.3) (push) Failing after 51s
tests / ci (8.4) (push) Failing after 1m7s
tests / ci (8.5) (push) Failing after 1m9s

This commit is contained in:
Daan Meijer 2026-06-17 09:38:54 +02:00
parent 3c414e1ef1
commit 0fee3c1972
15 changed files with 283 additions and 11 deletions

View File

@ -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'
]);

View File

@ -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',

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Controllers;
use App\Models\Dynamic;
use Illuminate\Http\Request;
class ParticipantController extends Controller
{
public function update(Request $request, Dynamic $dynamic)
{
$request->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!');
}
}

View File

@ -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

View File

@ -12,5 +12,6 @@ class Participant extends Pivot
'user_id',
'dynamic_id',
'role',
'display_name',
];
}

View File

@ -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.
*

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->string('display_name')->nullable()->after('role');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('participants', function (Blueprint $table) {
$table->dropColumn('display_name');
});
}
};

View File

@ -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;

View File

@ -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";*/

View File

@ -0,0 +1,113 @@
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { route } from 'ziggy-js';
const props = defineProps<{
dynamicId: number;
participant: {
id: number;
name: string;
display_name: string | null;
};
}>();
const form = useForm({
display_name: props.participant.display_name ?? '',
});
function submit() {
form.put(route('dynamics.participant.update', props.dynamicId));
}
</script>
<template>
<div class="c-display-name-form">
<h4 class="c-display-name-form__title">
Display Name for {{ participant.name }}
</h4>
<form @submit.prevent="submit" class="c-display-name-form__form">
<div class="c-display-name-form__field">
<label
for="display_name"
class="c-display-name-form__label"
>
Dynamic-specific Display Name
</label>
<input
v-model="form.display_name"
id="display_name"
type="text"
class="c-display-name-form__input"
/>
<div
v-if="form.errors.display_name"
class="c-display-name-form__error"
>
{{ form.errors.display_name }}
</div>
</div>
<div class="c-display-name-form__actions">
<button
type="submit"
:disabled="form.processing"
class="c-display-name-form__submit-btn"
>
Save
</button>
</div>
</form>
</div>
</template>
<style scoped>
@reference '../../css/app.css';
.c-display-name-form {
@apply mt-8;
}
.c-display-name-form__title {
@apply text-lg font-medium;
color: var(--foreground);
}
.c-display-name-form__form {
@apply mt-4 space-y-4;
}
.c-display-name-form__field {
@apply block;
}
.c-display-name-form__label {
@apply block text-sm font-medium;
color: var(--foreground);
}
.c-display-name-form__input {
@apply mt-1 block w-full rounded-md border shadow-sm focus:border-indigo-500 focus:ring-indigo-500;
border-color: var(--border);
background-color: var(--background);
color: var(--foreground);
}
.c-display-name-form__error {
@apply text-sm;
color: var(--destructive);
}
.c-display-name-form__actions {
@apply flex items-center gap-4;
}
.c-display-name-form__submit-btn {
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900;
background-color: var(--primary);
color: var(--primary-foreground);
&:hover {
opacity: 0.9;
}
}
</style>

View File

@ -3,6 +3,9 @@ defineProps<{
participants: Array<{
id: number;
name: string;
pivot: {
display_name: string | null;
};
}>;
}>();
</script>
@ -16,7 +19,7 @@ defineProps<{
:key="participant.id"
class="c-participants-list__item"
>
{{ participant.name }}
{{ participant.pivot.display_name ?? participant.name }}
</li>
</ul>
</div>

View File

@ -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 = [
<!-- Ledgers List Component -->
<LedgerList :dynamic-id="dynamic.id" :ledgers="dynamic.ledgers" />
<!-- Display Name Form -->
<DisplayNameForm :dynamic-id="dynamic.id" :participant="currentUser" />
<div v-if="isOwner" class="mt-8 flex gap-4">
<InertiaLink :href="route('dynamics.invitations.create', dynamic.id)" class="c-dynamic-show__action-btn">
Invite User

View File

@ -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'])

View File

@ -0,0 +1,76 @@
<?php
use App\Models\User;
use App\Models\Dynamic;
test('user displayNameFor returns user name by default', function () {
$user = User::factory()->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);
});

View File

@ -16,7 +16,7 @@ use Tests\TestCase;
pest()->extend(TestCase::class)
->use(RefreshDatabase::class)
->in('Feature');
->in('Feature', 'Browser');
/*
|--------------------------------------------------------------------------