Files
Kol-Manager/resources/views/content/index.blade.php

208 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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>
@foreach(['instagram', 'tiktok', 'youtube'] as $p)
<a href="{{ request()->fullUrlWithQuery(['platform' => $p]) }}"
class="px-3 py-1.5 rounded-full text-xs font-semibold transition-colors
{{ request('platform') === $p ? 'text-white' : 'bg-white text-gray-500 hover:bg-pink-50 border border-pink-100' }}"
style="{{ request('platform') === $p ? 'background:#FFB7C5' : '' }}">
{{ ['instagram' => '📸', 'tiktok' => '🎵', 'youtube' => '▶️'][$p] }} {{ ucfirst($p) }}
</a>
@endforeach
@if(request('platform'))
<a href="{{ route('content.index') }}"
class="px-3 py-1.5 rounded-full text-xs text-gray-400 hover:text-gray-600 border border-gray-200 bg-white">✕
Clear</a>
@endif
</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 --}}
<div class="{{ $status->colorClass() }} flex flex-col w-full lg:flex-1 lg:min-w-36 rounded-2xl p-3"
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