feat(02-01): add inline note UI to member list
- Add Alpine.js x-data scope to each member row with noteFormOpen, noteContent, isSubmitting, errors, noteCount - Add submitNote() async method calling axios.post to admin.members.notes.store route - Add note count badge with reactive x-text binding to noteCount (initialized from withCount) - Add toggle button to expand/collapse inline note form - Add inline form with textarea, cancel button, and submit button - Submit button disabled when isSubmitting or noteContent is empty - Loading state toggles between 儲存 and 儲存中... - Validation errors display in Traditional Chinese via x-show and x-text - Cancel button clears content, closes form, and resets errors - Add 備忘錄 column header between 狀態 and 操作 - Update empty state colspan from 7 to 8 - Add x-cloak CSS to prevent flash of unstyled content - All elements include dark mode classes (dark:*)
This commit is contained in:
@@ -5,6 +5,10 @@
|
|||||||
</h2>
|
</h2>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
|
|
||||||
|
@push('styles')
|
||||||
|
<style>[x-cloak] { display: none !important; }</style>
|
||||||
|
@endpush
|
||||||
|
|
||||||
<div class="py-12">
|
<div class="py-12">
|
||||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||||
@@ -179,6 +183,9 @@
|
|||||||
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||||||
狀態
|
狀態
|
||||||
</th>
|
</th>
|
||||||
|
<th scope="col" class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||||||
|
備忘錄
|
||||||
|
</th>
|
||||||
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
<th scope="col" class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-300">
|
||||||
<span class="sr-only">操作</span>
|
<span class="sr-only">操作</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -186,7 +193,31 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
|
<tbody class="divide-y divide-gray-200 dark:divide-gray-700 bg-white dark:bg-gray-800">
|
||||||
@forelse ($members as $member)
|
@forelse ($members as $member)
|
||||||
<tr>
|
<tr x-data="{
|
||||||
|
noteFormOpen: false,
|
||||||
|
noteContent: '',
|
||||||
|
isSubmitting: false,
|
||||||
|
errors: {},
|
||||||
|
noteCount: {{ $member->notes_count ?? 0 }},
|
||||||
|
async submitNote() {
|
||||||
|
this.isSubmitting = true;
|
||||||
|
this.errors = {};
|
||||||
|
try {
|
||||||
|
await axios.post('{{ route('admin.members.notes.store', $member) }}', {
|
||||||
|
content: this.noteContent
|
||||||
|
});
|
||||||
|
this.noteCount++;
|
||||||
|
this.noteContent = '';
|
||||||
|
this.noteFormOpen = false;
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response && error.response.status === 422) {
|
||||||
|
this.errors = error.response.data.errors || {};
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}">
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<input type="checkbox" name="selected_ids[]" value="{{ $member->id }}" class="member-checkbox rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700">
|
<input type="checkbox" name="selected_ids[]" value="{{ $member->id }}" class="member-checkbox rounded border-gray-300 dark:border-gray-600 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:bg-gray-700">
|
||||||
</td>
|
</td>
|
||||||
@@ -221,6 +252,60 @@
|
|||||||
{{ $member->membership_status_label }}
|
{{ $member->membership_status_label }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<!-- Note count badge -->
|
||||||
|
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200">
|
||||||
|
<span x-text="noteCount"></span>
|
||||||
|
</span>
|
||||||
|
<!-- Toggle button -->
|
||||||
|
<button
|
||||||
|
@click="noteFormOpen = !noteFormOpen"
|
||||||
|
type="button"
|
||||||
|
class="text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400"
|
||||||
|
:title="noteFormOpen ? '收合' : '新增備忘錄'"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Inline note form -->
|
||||||
|
<div x-show="noteFormOpen" x-cloak
|
||||||
|
x-transition:enter="transition ease-out duration-200"
|
||||||
|
x-transition:enter-start="opacity-0"
|
||||||
|
x-transition:enter-end="opacity-100"
|
||||||
|
class="mt-2">
|
||||||
|
<form @submit.prevent="submitNote()">
|
||||||
|
<textarea
|
||||||
|
x-model="noteContent"
|
||||||
|
rows="2"
|
||||||
|
class="block w-full rounded-md border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 dark:focus:border-indigo-500 focus:ring-indigo-500 dark:focus:ring-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-gray-200"
|
||||||
|
:class="{ 'border-red-500 dark:border-red-400': errors.content }"
|
||||||
|
placeholder="輸入備忘錄..."
|
||||||
|
></textarea>
|
||||||
|
<p x-show="errors.content" x-text="errors.content?.[0]"
|
||||||
|
class="mt-1 text-xs text-red-600 dark:text-red-400"></p>
|
||||||
|
<div class="mt-1 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="noteFormOpen = false; noteContent = ''; errors = {};"
|
||||||
|
class="inline-flex items-center rounded-md px-2.5 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="isSubmitting || noteContent.trim() === ''"
|
||||||
|
class="inline-flex items-center rounded-md border border-transparent bg-indigo-600 dark:bg-indigo-500 px-2.5 py-1.5 text-xs font-medium text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<span x-show="!isSubmitting">儲存</span>
|
||||||
|
<span x-show="isSubmitting">儲存中...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td class="whitespace-nowrap px-4 py-3 text-right text-sm font-medium">
|
<td class="whitespace-nowrap px-4 py-3 text-right text-sm font-medium">
|
||||||
<a href="{{ route('admin.members.show', $member) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
|
<a href="{{ route('admin.members.show', $member) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">
|
||||||
檢視
|
檢視
|
||||||
@@ -229,7 +314,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
|
<td colspan="8" class="px-4 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||||
找不到會員。
|
找不到會員。
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user