dashboard

Tung Pham (tungpham42) php Public Mar 03, 2026 02:34 PM
Share
@extends('layouts.app')

@section('title', 'Admin Dashboard - SOFT Paste')

@section('content')
<div class="max-w-7xl mx-auto w-full mt-4 p-6 md:p-8 bg-white dark:bg-slate-900 rounded-2xl shadow-xl shadow-slate-200/50 dark:shadow-none border border-rose-100 dark:border-rose-900/30 transition-colors"
     x-data="{ activeTab: localStorage.getItem('adminActiveTab') || 'pastes' }"
     x-init="$watch('activeTab', value => { localStorage.setItem('adminActiveTab', value); $dispatch('tab-changed', value); })">

    <div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 mb-6">
        <div>
            <h1 class="text-2xl font-bold text-slate-800 dark:text-white flex items-center gap-2">
                <svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
                Admin Center
            </h1>
            <p class="text-slate-500 dark:text-slate-400 text-sm mt-1">Manage all snippets and users across the entire platform.</p>
        </div>
    </div>

    @if(session('success'))
        <div class="bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20 text-emerald-700 dark:text-emerald-400 px-5 py-4 rounded-xl mb-6 flex items-center gap-3 font-medium">
            <svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
            {{ session('success') }}
        </div>
    @endif

    @if(session('error'))
        <div class="bg-rose-50 dark:bg-rose-500/10 border border-rose-200 dark:border-rose-500/20 text-rose-700 dark:text-rose-400 px-5 py-4 rounded-xl mb-6 flex items-center gap-3 font-medium">
            <svg class="w-5 h-5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
            {{ session('error') }}
        </div>
    @endif

    <div class="flex space-x-6 mb-6 border-b border-slate-200 dark:border-slate-800">
        <button @click="activeTab = 'pastes'"
                :class="activeTab === 'pastes' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'"
                class="pb-3 border-b-2 font-semibold transition-colors focus:outline-none flex items-center gap-2">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
            Snippets
        </button>
        <button @click="activeTab = 'users'"
                :class="activeTab === 'users' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'"
                class="pb-3 border-b-2 font-semibold transition-colors focus:outline-none flex items-center gap-2">
            <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
            Users
        </button>
    </div>

    <div x-show="activeTab === 'pastes'" x-cloak>
        <div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-800 p-4">
            <table id="adminPastesTable" class="w-full text-left border-collapse">
                <thead>
                    <tr class="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800">
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Title</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Author</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Syntax</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Visibility</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Status</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Created</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400 text-right">Actions</th>
                    </tr>
                </thead>
                <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
                    @foreach($pastes as $paste)
                        <tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/50 transition group">
                            <td class="p-4 font-medium">
                                <a href="{{ route('pastes.show', $paste) }}" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 transition">
                                    {{ $paste->title ?? 'Untitled Snippet' }}
                                </a>
                            </td>
                            <td class="p-4 text-sm text-slate-600 dark:text-slate-300">
                                {{ $paste->user ? $paste->user->name : 'Guest' }}
                            </td>
                            <td class="p-4 text-xs font-bold tracking-wide uppercase text-slate-500 dark:text-slate-400">{{ $paste->syntax }}</td>
                            <td class="p-4 text-sm capitalize text-slate-600 dark:text-slate-300 flex items-center gap-1.5">
                                @if($paste->visibility === 'public') 🌍
                                @elseif($paste->visibility === 'unlisted') 🔗
                                @else 🔒 @endif
                                {{ $paste->visibility }}
                            </td>
                            <td class="p-4">
                                @if($paste->expires_at && $paste->expires_at->isPast())
                                    <span class="bg-rose-50 dark:bg-rose-500/10 text-rose-700 dark:text-rose-400 px-2.5 py-1 rounded-md text-xs font-bold tracking-wide border border-rose-100 dark:border-rose-500/20">Expired</span>
                                @else
                                    <span class="bg-emerald-50 dark:bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 px-2.5 py-1 rounded-md text-xs font-bold tracking-wide border border-emerald-100 dark:border-emerald-500/20">Active</span>
                                @endif
                            </td>
                            <td class="p-4 text-sm text-slate-500 dark:text-slate-400" data-order="{{ $paste->created_at->timestamp }}">
                                {{ $paste->created_at->format('M d, Y H:i') }}
                            </td>
                            <td class="p-4 text-right">
                                <div class="flex justify-end items-center gap-3">
                                    <a target="_blank" href="{{ route('pastes.show', $paste) }}" class="text-sm font-medium text-slate-500 dark:text-slate-400 hover:text-indigo-600 transition">View</a>
                                    <form action="{{ route('admin.pastes.destroy', $paste) }}" method="POST" onsubmit="confirmDelete(event, this, 'paste');">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="text-sm font-medium text-rose-500 hover:text-rose-700 transition">Delete</button>
                                    </form>
                                </div>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>

    <div x-show="activeTab === 'users'" x-cloak>
        <div class="overflow-x-auto rounded-xl border border-slate-200 dark:border-slate-800 p-4">
            <table id="adminUsersTable" class="w-full text-left border-collapse">
                <thead>
                    <tr class="bg-slate-50 dark:bg-slate-800/50 border-b border-slate-200 dark:border-slate-800">
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">User</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Email</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Role</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400">Joined Time</th>
                        <th class="p-4 font-semibold text-sm text-slate-600 dark:text-slate-400 text-right">Actions</th>
                    </tr>
                </thead>
                <tbody class="divide-y divide-slate-100 dark:divide-slate-800">
                    @foreach($users ?? [] as $user)
                        <tr class="hover:bg-slate-50/80 dark:hover:bg-slate-800/50 transition group">
                            <td class="p-4">
                                <div class="flex items-center gap-3">
                                    @if($user->avatar)
                                        <img src="{{ $user->avatar }}" alt="Avatar" class="w-8 h-8 rounded-full border border-slate-200 dark:border-slate-700" referrerpolicy="no-referrer">
                                    @else
                                        <div class="w-8 h-8 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 flex items-center justify-center font-bold text-xs">
                                            {{ substr($user->name, 0, 1) }}
                                        </div>
                                    @endif
                                    <span class="font-medium text-slate-800 dark:text-slate-200">{{ $user->name }}</span>
                                </div>
                            </td>
                            <td class="p-4 text-sm text-slate-600 dark:text-slate-400">{{ $user->email }}</td>
                            <td class="p-4">
                                @if($user->is_admin)
                                    <span class="bg-indigo-50 dark:bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 px-2.5 py-1 rounded-md text-xs font-bold tracking-wide border border-indigo-100 dark:border-indigo-500/20">Admin</span>
                                @else
                                    <span class="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400 px-2.5 py-1 rounded-md text-xs font-bold tracking-wide border border-slate-200 dark:border-slate-700">User</span>
                                @endif
                            </td>
                            <td class="p-4 text-sm text-slate-500 dark:text-slate-400" data-order="{{ $user->created_at->timestamp }}">
                                {{ $user->created_at->format('M d, Y H:i') }}
                            </td>
                            <td class="p-4 text-right">
                                <div class="flex justify-end items-center gap-3">
                                    <form action="{{ route('admin.users.destroy', $user) }}" method="POST" onsubmit="confirmDelete(event, this, 'user');">
                                        @csrf
                                        @method('DELETE')
                                        <button type="submit" class="text-sm font-medium text-rose-500 hover:text-rose-700 transition" @if(auth()->id() === $user->id) disabled title="You cannot delete yourself" class="opacity-50 cursor-not-allowed" @endif>Delete</button>
                                    </form>
                                </div>
                            </td>
                        </tr>
                    @endforeach
                </tbody>
            </table>
        </div>
    </div>

</div>

@push('scripts')
<script>
    // Reusable function to inject an Alpine dropdown matching create.blade.php
    function applyCustomAlpineLengthMenu(tableId, itemLabel) {
        const dtLengthContainer = $('#' + tableId + '_wrapper .dt-length');
        if (dtLengthContainer.length) {
            dtLengthContainer.children().hide();
            const dropdownHTML = `
                <div class="flex items-center gap-3">
                    <label class="font-semibold text-sm text-slate-700 dark:text-slate-300">Show</label>
                    <div x-data="{
                        open: false, dropUp: false, value: '10',
                        options: { '10': '10', '25': '25', '50': '50', '100': '100' },
                        get selectedLabel() { return this.options[this.value]; },
                        reposition() {
                            if (!this.$refs.button) return;
                            const rect = this.$refs.button.getBoundingClientRect();
                            const spaceBelow = window.innerHeight - rect.bottom;
                            this.dropUp = spaceBelow < 260 && rect.top > spaceBelow;
                        }
                    }"
                    @click.away="open = false" @scroll.window="open ? reposition() : null" @resize.window="open ? reposition() : null"
                    class="relative min-w-[4.5rem]">
                        <button x-ref="button" @click="open = !open; if(open) $nextTick(() => reposition())" type="button" class="w-full flex items-center justify-between bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200 py-1.5 px-3 rounded-lg focus:ring-4 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none text-sm transition shadow-sm">
                            <span x-text="selectedLabel"></span>
                            <svg class="w-4 h-4 text-slate-400 transition-transform ml-2" :class="open ? 'rotate-180' : ''" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
                        </button>
                        <ul x-show="open" x-transition.opacity.duration.200ms :class="dropUp ? 'bottom-full mb-2' : 'top-full mt-2'" class="absolute z-50 w-full bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl max-h-60 overflow-y-auto py-1 text-sm" style="display: none;">
                            <template x-for="(label, key) in options" :key="key">
                                <li @click="value = key; open = false; $('#${tableId}').DataTable().page.len(parseInt(key)).draw();" class="px-4 py-2 cursor-pointer transition-colors" :class="value === key ? 'bg-indigo-50 dark:bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 font-semibold' : 'text-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700/50 hover:text-indigo-600 dark:hover:text-indigo-400'">
                                    <span x-text="label"></span>
                                </li>
                            </template>
                        </ul>
                    </div>
                    <label class="font-semibold text-sm text-slate-700 dark:text-slate-300">${itemLabel} per page</label>
                </div>
            `;
            dtLengthContainer.append(dropdownHTML);
        }
    }

    // New reusable function to style the DataTables search bar
    function applyCustomSearchStyling(tableId) {
        const dtSearchContainer = $('#' + tableId + '_wrapper .dt-search');
        if (dtSearchContainer.length) {
            // Style the label to match the "Show" label
            dtSearchContainer.find('label').addClass('font-semibold text-sm text-slate-700 dark:text-slate-300 mr-2');

            // Overwrite DataTables default input classes with your custom UI classes
            const input = dtSearchContainer.find('input');
            input.removeClass().addClass('bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-200 py-1.5 px-3 rounded-lg focus:ring-4 focus:ring-indigo-500/20 focus:border-indigo-500 outline-none text-sm transition shadow-sm placeholder:text-slate-400 dark:placeholder:text-slate-500');
        }
    }

    $(document).ready(function() {
        // Init Pastes Table
        let pastesTable = $('#adminPastesTable').DataTable({
            responsive: true, order: [[5, 'desc']],
            language: {
                search: "", searchPlaceholder: "Search pastes...",
                info: "Showing _START_ to _END_ of _TOTAL_ snippets",
                infoEmpty: "Showing 0 to 0 of 0 snippets", infoFiltered: "(filtered from _MAX_ total snippets)"
            },
            columnDefs: [{ orderable: false, targets: 6 }]
        });
        applyCustomAlpineLengthMenu('adminPastesTable', 'snippets');
        applyCustomSearchStyling('adminPastesTable');

        // Init Users Table
        let usersTable = $('#adminUsersTable').DataTable({
            responsive: true, order: [[3, 'desc']],
            language: {
                search: "", searchPlaceholder: "Search users...",
                info: "Showing _START_ to _END_ of _TOTAL_ users",
                infoEmpty: "Showing 0 to 0 of 0 users", infoFiltered: "(filtered from _MAX_ total users)"
            },
            columnDefs: [{ orderable: false, targets: 4 }]
        });
        applyCustomAlpineLengthMenu('adminUsersTable', 'users');
        applyCustomSearchStyling('adminUsersTable');

        window.addEventListener('tab-changed', event => {
            setTimeout(() => {
                if(event.detail === 'pastes') pastesTable.columns.adjust();
                if(event.detail === 'users') usersTable.columns.adjust();
            }, 50);
        });
    });
</script>
@endpush
@endsection