215 lines
13 KiB
PHP
215 lines
13 KiB
PHP
@extends('layouts.app')
|
||
|
||
@section('title', 'Content Board')
|
||
@section('page-title', 'Content Board 🎬')
|
||
@section('page-subtitle', 'Drag cards between columns to update status')
|
||
|
||
@section('content')
|
||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||
<div class="flex gap-2 flex-wrap">
|
||
<a href="{{ route('content.create') }}" class="btn-sakura text-sm shrink-0">+ New Brief</a>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{{--
|
||
Mobile : grid grid-cols-1 → columns stack vertically, each full width
|
||
Desktop : flex flex-nowrap → columns in a single horizontal row, each flex-1
|
||
x-sort:group is on the INNER items div (not the column div) to avoid
|
||
SortableJS mutating the outer layout container.
|
||
--}}
|
||
<div class="grid grid-cols-1 gap-4 lg:flex lg:flex-nowrap lg:gap-4" x-data="kanban()">
|
||
@foreach($columns as $col)
|
||
@php $status = $col['status']; @endphp
|
||
|
||
{{-- Column shell — layout only, NOT the sort target --}}
|
||
@php
|
||
$bgColors = [
|
||
'drafting' => '#f2f2f2',
|
||
'filming' => 'rgba(251, 189, 35, 0.2)',
|
||
'editing' => 'rgba(58, 191, 248, 0.2)',
|
||
'review' => 'rgba(240, 0, 184, 0.2)',
|
||
'posted' => 'rgba(54, 211, 153, 0.2)',
|
||
'archived' => 'rgba(42, 50, 60, 0.2)',
|
||
];
|
||
$bgStyle = isset($bgColors[$status->value]) ? 'background-color: ' . $bgColors[$status->value] . ';' : '';
|
||
@endphp
|
||
<div class="{{ $status->colorClass() }} flex flex-col w-full lg:flex-1 lg:min-w-36 rounded-2xl p-3"
|
||
style="{{ $bgStyle }}" data-status="{{ $status->value }}">
|
||
|
||
{{-- Column header --}}
|
||
<div class="flex items-center justify-between mb-3">
|
||
<div class="flex items-center gap-1.5">
|
||
<span class="text-base">{{ $status->icon() }}</span>
|
||
<span class="font-bold text-gray-700 text-sm">{{ $status->label() }}</span>
|
||
</div>
|
||
<span
|
||
class="text-xs bg-white rounded-full px-2 py-0.5 font-bold text-gray-500 shadow-sm">{{ count($col['items']) }}</span>
|
||
</div>
|
||
|
||
{{-- Sortable items container --}}
|
||
<div class="flex flex-col gap-2 flex-1 min-h-12 kanban-column" data-col-status="{{ $status->value }}">
|
||
|
||
@foreach($col['items'] as $brief)
|
||
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
||
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
||
data-id="{{ $brief->id }}">
|
||
<div class="flex items-start gap-2 mb-1.5">
|
||
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
||
<a href="#" data-brief="{{ json_encode([
|
||
'id' => $brief->id,
|
||
'title' => $brief->title,
|
||
'platform' => $brief->platform_icon . ' ' . ucfirst($brief->platform),
|
||
'status' => $brief->status->label(),
|
||
'client' => $brief->client?->name,
|
||
'target_date' => $brief->target_date?->format('d M Y'),
|
||
'hook' => $brief->hook,
|
||
'body' => $brief->body,
|
||
'notes' => $brief->notes
|
||
]) }}" @click.prevent="openModal(JSON.parse($el.dataset.brief))"
|
||
class="font-semibold text-gray-800 text-xs leading-tight hover:text-pink-500 line-clamp-2 flex-1">
|
||
{{ $brief->title }}
|
||
</a>
|
||
</div>
|
||
@if($brief->client)
|
||
<div class="text-xs text-gray-400 mb-1">👥 {{ $brief->client->name }}</div>
|
||
@endif
|
||
@if($brief->target_date)
|
||
<div
|
||
class="text-xs {{ $brief->target_date->isPast() && $brief->status->value !== 'posted' ? 'text-red-400 font-bold' : 'text-gray-400' }}">
|
||
📅 {{ $brief->target_date->format('d M') }}
|
||
</div>
|
||
@endif
|
||
<div class="flex justify-end mt-1.5">
|
||
<a href="{{ route('content.edit', $brief) }}"
|
||
class="text-xs text-gray-400 hover:text-pink-500">Edit</a>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
|
||
{{-- Add button --}}
|
||
<a href="{{ route('content.create') }}?status={{ $status->value }}"
|
||
class="mt-3 text-xs text-gray-400 hover:text-pink-500 text-center py-2
|
||
border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block">
|
||
+ Add
|
||
</a>
|
||
</div>
|
||
@endforeach
|
||
|
||
{{-- Content Detail Request Modal --}}
|
||
<div x-show="modalOpen" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/40 backdrop-blur-sm"
|
||
style="display: none;" x-transition>
|
||
<div @click.outside="modalOpen = false"
|
||
class="bg-white p-5 w-full max-w-lg rounded-3xl shadow-xl mx-4 max-h-[90vh] flex flex-col relative"
|
||
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 translate-y-4"
|
||
x-transition:enter-end="opacity-100 translate-y-0" x-transition:leave="transition ease-in duration-200"
|
||
x-transition:leave-start="opacity-100 translate-y-0" x-transition:leave-end="opacity-0 translate-y-4">
|
||
<div class="flex items-center justify-between mb-4 border-b border-pink-50 pb-3 shrink-0">
|
||
<h3 class="font-extrabold text-gray-800 text-xl truncate pr-4" x-text="activeBrief?.title"></h3>
|
||
<button @click="modalOpen = false"
|
||
class="text-gray-400 hover:text-pink-500 hover:bg-pink-50 w-8 h-8 rounded-full flex items-center justify-center transition-colors">✕</button>
|
||
</div>
|
||
|
||
<div class="text-sm text-gray-600 space-y-4 flex-1 overflow-y-auto pr-2 custom-scrollbar">
|
||
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Platform</span><span
|
||
class="font-bold text-gray-800" x-text="activeBrief?.platform"></span></div>
|
||
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Status</span><span
|
||
class="font-bold badge bg-pink-100 text-pink-700 rounded-full px-2 py-0.5"
|
||
x-text="activeBrief?.status"></span></div>
|
||
<template x-if="activeBrief?.client">
|
||
<div class="flex gap-3 items-center"><span
|
||
class="text-gray-400 w-20 shrink-0 font-medium">Client</span><span
|
||
class="font-semibold text-pink-500" x-text="activeBrief?.client"></span></div>
|
||
</template>
|
||
<template x-if="activeBrief?.target_date">
|
||
<div class="flex gap-3"><span class="text-gray-400 w-20 shrink-0 font-medium">Target</span><span
|
||
class="font-medium" x-text="activeBrief?.target_date"></span></div>
|
||
</template>
|
||
|
||
<template x-if="activeBrief?.hook">
|
||
<div class="flex flex-col gap-1 mt-4">
|
||
<span class="text-gray-400 font-medium">🎣 Hook</span>
|
||
<div class="whitespace-pre-wrap bg-pink-50/50 border border-pink-100 p-3 rounded-2xl text-gray-700 leading-relaxed"
|
||
x-text="activeBrief?.hook"></div>
|
||
</div>
|
||
</template>
|
||
<template x-if="activeBrief?.body">
|
||
<div class="flex flex-col gap-1 mt-4">
|
||
<span class="text-gray-400 font-medium">📖 Body</span>
|
||
<div class="whitespace-pre-wrap bg-gray-50 border border-gray-100 p-3 rounded-2xl text-gray-700 leading-relaxed"
|
||
x-text="activeBrief?.body"></div>
|
||
</div>
|
||
</template>
|
||
<template x-if="activeBrief?.notes">
|
||
<div class="flex flex-col gap-1 mt-4">
|
||
<span class="text-gray-400 font-medium">📝 Notes</span>
|
||
<div class="whitespace-pre-wrap bg-yellow-50/50 border border-yellow-100 p-3 rounded-2xl text-gray-700 leading-relaxed"
|
||
x-text="activeBrief?.notes"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<div class="flex gap-3 mt-5 pt-4 border-t border-pink-50 shrink-0">
|
||
<a :href="`/content/${activeBrief?.id}/edit`" class="btn-sakura text-sm flex-1 text-center py-2.5">Edit
|
||
Brief</a>
|
||
<a :href="`/content/${activeBrief?.id}`"
|
||
class="py-2.5 rounded-full border border-pink-200 text-pink-500 text-sm hover:bg-pink-50 flex-1 text-center font-bold bg-white transition-colors">Details
|
||
Page</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
@push('scripts')
|
||
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
|
||
<script>
|
||
function kanban() {
|
||
return {
|
||
modalOpen: false,
|
||
activeBrief: null,
|
||
init() {
|
||
// Start Sortable once script loads
|
||
const initSort = () => {
|
||
if (typeof Sortable !== 'undefined') {
|
||
document.querySelectorAll('.kanban-column').forEach(el => {
|
||
Sortable.create(el, {
|
||
group: 'kanban-board',
|
||
animation: 150,
|
||
onEnd: (evt) => {
|
||
const id = evt.item.dataset.id;
|
||
const targetCol = evt.to.dataset.colStatus;
|
||
if (!id || !targetCol) return;
|
||
|
||
// If dropped in same column identically, skip fetch
|
||
if (evt.from === evt.to && evt.oldIndex === evt.newIndex) return;
|
||
|
||
fetch(`/content/${id}/status`, {
|
||
method: 'PATCH',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||
},
|
||
body: JSON.stringify({ status: targetCol }),
|
||
}).then(r => {
|
||
if (r.ok) window.KolManager.notify('Status updated! 🎬');
|
||
else window.KolManager.notify('❌ Failed to update status');
|
||
}).catch(() => window.KolManager.notify('❌ Network error!'));
|
||
}
|
||
});
|
||
});
|
||
} else {
|
||
setTimeout(initSort, 100);
|
||
}
|
||
};
|
||
initSort();
|
||
},
|
||
openModal(brief) {
|
||
this.activeBrief = brief;
|
||
this.modalOpen = true;
|
||
}
|
||
};
|
||
}
|
||
</script>
|
||
@endpush
|
||
@endsection |