different names in different dynamics
This commit is contained in:
parent
3c414e1ef1
commit
0fee3c1972
@ -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'
|
||||
]);
|
||||
|
||||
@ -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',
|
||||
|
||||
24
app/Http/Controllers/ParticipantController.php
Normal file
24
app/Http/Controllers/ParticipantController.php
Normal 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!');
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -12,5 +12,6 @@ class Participant extends Pivot
|
||||
'user_id',
|
||||
'dynamic_id',
|
||||
'role',
|
||||
'display_name',
|
||||
];
|
||||
}
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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";*/
|
||||
|
||||
113
resources/js/components/DisplayNameForm.vue
Normal file
113
resources/js/components/DisplayNameForm.vue
Normal 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>
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'])
|
||||
|
||||
76
tests/Feature/ParticipantTest.php
Normal file
76
tests/Feature/ParticipantTest.php
Normal 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);
|
||||
});
|
||||
@ -16,7 +16,7 @@ use Tests\TestCase;
|
||||
|
||||
pest()->extend(TestCase::class)
|
||||
->use(RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
->in('Feature', 'Browser');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user