chore: cleanup project structure and update readme for beta release

This commit is contained in:
2025-12-23 04:59:21 +07:00
parent 1640ced748
commit 10a00bac0e
122 changed files with 8320 additions and 661 deletions

View File

@@ -0,0 +1,116 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Inbox / Messages
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">Inbox</li>
</ol>
</nav>
</div>
<!-- Inbox Layout -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Sender</p>
</th>
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Category & Subject</p>
</th>
<th class="px-5 py-3 text-left">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Date</p>
</th>
<th class="px-5 py-3 text-right">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">Actions</p>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@forelse ($submissions as $msg)
<tr class="group hover:bg-gray-50 dark:hover:bg-white/[0.02] {{ !$msg->is_read ? 'bg-brand-50/30 dark:bg-brand-500/5' : '' }}">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div class="relative">
<div class="w-10 h-10 rounded-full bg-brand-100 dark:bg-brand-500/10 flex items-center justify-center text-brand-600 font-bold text-sm">
{{ substr($msg->name, 0, 1) }}
</div>
@if(!$msg->is_read)
<span class="absolute top-0 right-0 h-3 w-3 rounded-full bg-brand-500 border-2 border-white dark:border-gray-900"></span>
@endif
</div>
<div>
<span class="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
{{ $msg->name }}
</span>
<span class="block text-gray-500 text-theme-xs dark:text-gray-400">
{{ $msg->email }}
</span>
</div>
</div>
</td>
<td class="px-5 py-4">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase mb-1 {{ $msg->category == 'Legal Inquiry' ? 'bg-purple-100 text-purple-700 dark:bg-purple-500/20' : 'bg-gray-100 text-gray-600 dark:bg-gray-700' }}">
{{ $msg->category }}
</span>
<p class="text-gray-800 dark:text-white/80 text-theme-sm font-medium line-clamp-1">
{{ $msg->subject }}
</p>
</td>
<td class="px-5 py-4">
<p class="text-gray-500 text-theme-xs dark:text-gray-400 whitespace-nowrap">
{{ $msg->created_at->format('M d, Y') }}
<span class="block opacity-60">{{ $msg->created_at->format('H:i') }}</span>
</p>
</td>
<td class="px-5 py-4 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('admin.contacts.show', $msg->id) }}" class="p-2 text-gray-400 hover:text-brand-500 dark:hover:text-white transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a>
<form action="{{ route('admin.contacts.destroy', $msg->id) }}" method="POST" onsubmit="return confirm('Delete this message?')">
@csrf
@method('DELETE')
<button type="submit" class="p-2 text-gray-400 hover:text-red-500 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-5 py-10 text-center">
<div class="flex flex-col items-center">
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path></svg>
<p class="text-gray-500 font-medium">No messages in your inbox yet.</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($submissions->hasPages())
<div class="px-5 py-4 border-t border-gray-100 dark:border-gray-800">
{{ $submissions->links() }}
</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,115 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Message Details
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('admin.contacts.index') }}">
Inbox /
</a>
</li>
<li class="font-medium text-brand-500">View Message</li>
</ol>
</nav>
</div>
<div class="bg-white rounded-xl border border-gray-200 shadow-sm dark:bg-white/[0.03] dark:border-gray-800 overflow-hidden">
<!-- Message Header -->
<div class="border-b border-gray-100 dark:border-gray-800 p-6 sm:p-8">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div class="flex items-center gap-4">
<div class="w-14 h-14 rounded-full bg-brand-500/10 flex items-center justify-center text-brand-600 font-bold text-xl">
{{ substr($contactSubmission->name, 0, 1) }}
</div>
<div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white leading-tight">
{{ $contactSubmission->name }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ $contactSubmission->email }}
</p>
</div>
</div>
<div class="text-right">
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold uppercase {{ $contactSubmission->category == 'Legal Inquiry' ? 'bg-purple-100 text-purple-700 dark:bg-purple-500/20' : 'bg-brand-50 text-brand-700 dark:bg-brand-500/20' }}">
{{ $contactSubmission->category }}
</span>
<p class="mt-2 text-xs text-gray-400 font-medium">
Received on {{ $contactSubmission->created_at->format('M d, Y \a\t H:i') }}
</p>
</div>
</div>
</div>
<!-- Message Body -->
<div class="p-6 sm:p-8 bg-gray-50/30 dark:bg-transparent">
<h4 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-4">Subject</h4>
<p class="text-lg font-semibold text-gray-900 dark:text-white mb-8">
{{ $contactSubmission->subject }}
</p>
<h4 class="text-sm font-bold text-gray-400 uppercase tracking-widest mb-4">Message</h4>
<div class="prose prose-gray dark:prose-invert max-w-none bg-white dark:bg-gray-800/20 p-6 rounded-2xl border border-gray-100 dark:border-gray-800 mb-10">
{!! nl2br(e($contactSubmission->message)) !!}
</div>
<!-- Quick Reply Form -->
<div class="mt-12 pt-10 border-t border-gray-100 dark:border-gray-800">
<h4 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-brand-500"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
Quick Reply via Portal
</h4>
<form action="{{ route('admin.contacts.reply', $contactSubmission->id) }}" method="POST" class="space-y-4">
@csrf
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Subject</label>
<input type="text" name="subject" value="Re: {{ $contactSubmission->subject }}" required
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-gray-900 transition focus:border-brand-500 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white">
</div>
<div>
<label class="block text-xs font-bold text-gray-400 uppercase mb-2">Message Body</label>
<textarea name="message" rows="6" required placeholder="Type your response here..."
class="w-full rounded-xl border-gray-200 bg-white px-4 py-3 text-gray-900 transition focus:border-brand-500 focus:ring-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"></textarea>
</div>
<div class="flex items-center justify-between pt-2">
<p class="text-[10px] text-gray-400 italic">
* Sending from: <span class="font-bold text-brand-500">support@lab.dyzulk.com</span>
</p>
<button type="submit" class="px-8 py-3 bg-brand-500 text-white rounded-xl font-bold hover:bg-brand-600 transition-all shadow-lg shadow-brand-500/20">
Send Reply Now
</button>
</div>
</form>
</div>
</div>
<!-- Footer Actions -->
<div class="p-6 border-t border-gray-100 dark:border-gray-800 flex items-center justify-between bg-gray-50 dark:bg-transparent">
<div class="flex items-center gap-4">
<a href="mailto:{{ $contactSubmission->email }}?subject=Re: {{ $contactSubmission->subject }}"
class="text-sm font-bold text-gray-500 hover:text-brand-500 transition-colors flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path><polyline points="22,6 12,13 2,6"></polyline></svg>
External Email App
</a>
</div>
<form action="{{ route('admin.contacts.destroy', $contactSubmission->id) }}" method="POST" onsubmit="return confirm('Delete this message permanently?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-500 font-bold hover:underline">
Delete Message
</button>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,211 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6" x-data="{
preview: false,
content: @js($legalPage->currentRevision->content ?? ''),
currentVersion: @js($legalPage->currentRevision->version ?? '1.0.0'),
selectedVersionType: 'patch',
customVersion: '',
updateExisting: false,
get suggestions() {
let parts = this.currentVersion.split('.').map(n => parseInt(n) || 0);
while(parts.length < 3) parts.push(0);
return {
major: (parts[0] + 1) + '.0.0',
minor: parts[0] + '.' + (parts[1] + 1) + '.0',
patch: parts[0] + '.' + parts[1] + '.' + (parts[2] + 1)
};
},
get finalVersion() {
if (this.updateExisting) return this.currentVersion;
if (this.selectedVersionType === 'custom') return this.customVersion;
return this.suggestions[this.selectedVersionType];
},
markdownToHtml(text) {
if (!text) return '';
return text
.replace(/^# (.*$)/gim, '<h1 class=\'text-2xl font-bold mb-4\'>$1</h1>')
.replace(/^## (.*$)/gim, '<h2 class=\'text-xl font-bold mb-3\'>$1</h2>')
.replace(/^### (.*$)/gim, '<h3 class=\'text-lg font-bold mb-2\'>$1</h3>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/\n/gim, '<br>');
}
}">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Edit: {{ $legalPage->title }}
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('admin.legal-pages.index') }}">
Legal Pages /
</a>
</li>
<li class="font-medium text-brand-500">Edit</li>
</ol>
</nav>
</div>
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<form action="{{ route('admin.legal-pages.update', $legalPage->id) }}" method="POST">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
<!-- Left Sidebar (Meta Information) -->
<div class="lg:col-span-1 border-r border-gray-100 dark:border-gray-800 pr-0 lg:pr-6">
<!-- Toggle Section -->
<div class="mb-8 p-4 rounded-xl bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-700">
<label class="flex items-center justify-between cursor-pointer">
<div>
<span class="block text-sm font-bold text-gray-800 dark:text-gray-200">Minor Correction</span>
<span class="block text-xs text-gray-400">Fixed typo or small tweaks?</span>
</div>
<div class="relative inline-block w-10 h-6">
<input type="checkbox" name="update_existing" value="true" x-model="updateExisting" class="sr-only peer">
<div class="w-full h-full bg-gray-300 dark:bg-gray-700 rounded-full peer peer-checked:bg-brand-500 transition-all after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:after:translate-x-4"></div>
</div>
</label>
<template x-if="updateExisting">
<p class="mt-3 text-[10px] leading-tight text-brand-600 dark:text-brand-400 font-medium italic">
* "Minor Correction" mode active. The system will update the existing record without creating a new revision, and the version will remain v<span x-text="currentVersion"></span>.
</p>
</template>
</div>
<div class="mb-6" :class="updateExisting ? 'opacity-40 grayscale pointer-events-none' : ''">
<label class="mb-3 block text-sm font-medium text-black dark:text-white">
Version Selection
</label>
<input type="hidden" name="version" :value="finalVersion">
<div class="grid grid-cols-1 gap-3">
<!-- Major -->
<button type="button" @click="selectedVersionType = 'major'"
:class="selectedVersionType === 'major' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-brand-600 dark:text-brand-400">MAJOR UPDATE</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.major"></p>
</div>
<div x-show="selectedVersionType === 'major'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Minor -->
<button type="button" @click="selectedVersionType = 'minor'"
:class="selectedVersionType === 'minor' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-gray-400">MINOR UPDATE</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.minor"></p>
</div>
<div x-show="selectedVersionType === 'minor'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Patch -->
<button type="button" @click="selectedVersionType = 'patch'"
:class="selectedVersionType === 'patch' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<div>
<p class="text-xs font-bold text-gray-400">PATCH / FIX</p>
<p class="text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="'v' + suggestions.patch"></p>
</div>
<div x-show="selectedVersionType === 'patch'" class="text-brand-500">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
</div>
</button>
<!-- Custom Toggle -->
<button type="button" @click="selectedVersionType = 'custom'"
:class="selectedVersionType === 'custom' ? 'border-brand-500 bg-brand-50 dark:bg-brand-500/10' : 'border-gray-200 dark:border-gray-700'"
class="flex items-center justify-between rounded-xl border-2 p-3 text-left transition-all">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider">Custom Version</p>
</button>
<div x-show="selectedVersionType === 'custom'" x-transition class="mt-2">
<input type="text" x-model="customVersion" placeholder="e.g 2.1.3"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-2 text-sm text-black outline-none transition focus:border-brand-500 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white" />
</div>
</div>
<p class="mt-4 text-xs text-gray-400 text-center">Current active: <span class="font-bold" x-text="'v' + currentVersion"></span></p>
</div>
<div class="mb-5">
<label class="mb-3 block text-sm font-medium text-black dark:text-white">
Change Log (Small note for internal audit)
</label>
<textarea name="change_log" rows="4"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-2 text-black outline-none transition focus:border-brand-500 active:border-brand-500 disabled:cursor-default disabled:bg-gray-100 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white @error('change_log') border-error-500 @enderror"
placeholder="What changed in this version?">{{ old('change_log') }}</textarea>
@error('change_log')
<p class="mt-1 text-xs text-error-500">{{ $message }}</p>
@enderror
</div>
<div class="mt-10">
<button type="submit" class="flex w-full justify-center rounded-lg bg-brand-500 px-6 py-3 font-medium text-white hover:bg-opacity-90 transition shadow-lg shadow-brand-500/20">
<span x-text="updateExisting ? 'Save Changes' : 'Save New Revision'"></span>
</button>
</div>
</div>
<!-- Right Main Content (Markdown Editor) -->
<div class="lg:col-span-2">
<div class="mb-4 flex items-center justify-between">
<label class="block text-sm font-medium text-black dark:text-white">
Content (Markdown)
</label>
<div class="flex rounded-lg bg-gray-100 p-1 dark:bg-gray-800">
<button type="button" @click="preview = false"
:class="!preview ? 'bg-white dark:bg-gray-700 text-brand-500 shadow-sm' : 'text-gray-500'"
class="px-4 py-1.5 text-xs font-medium rounded-md transition-all">
Editor
</button>
<button type="button" @click="preview = true"
:class="preview ? 'bg-white dark:bg-gray-700 text-brand-500 shadow-sm' : 'text-gray-500'"
class="px-4 py-1.5 text-xs font-medium rounded-md transition-all">
Preview
</button>
</div>
</div>
<div x-show="!preview">
<textarea name="content" x-model="content" rows="20"
class="w-full rounded-lg border-[1.5px] border-gray-200 bg-transparent px-4 py-4 text-black font-mono text-sm outline-none transition focus:border-brand-500 active:border-brand-500 disabled:cursor-default disabled:bg-gray-100 dark:border-gray-700 dark:bg-gray-900/50 dark:text-white @error('content') border-error-500 @enderror">{{ old('content', $legalPage->currentRevision->content ?? '') }}</textarea>
@error('content')
<p class="mt-1 text-xs text-error-500">{{ $message }}</p>
@enderror
</div>
<div x-show="preview" class="min-h-[465px] rounded-lg border border-gray-200 dark:border-gray-700 p-6 bg-gray-50 dark:bg-gray-900/30 overflow-y-auto">
<div class="prose prose-gray dark:prose-invert max-w-none" x-html="markdownToHtml(content)">
</div>
</div>
<p class="mt-4 text-xs text-gray-500 text-center">
TIP: Markdown is converted to high-quality typography on the public site.
</p>
</div>
</div>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,116 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
Legal Pages Management
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">Legal Pages</li>
</ol>
</nav>
</div>
<!-- Alert -->
@if (session('success'))
<div class="mb-6 flex w-full rounded-lg border-l-6 border-success-500 bg-success-500/10 px-7 py-4 shadow-md dark:bg-[#1B2B20] md:p-6">
<div class="mr-5 flex h-9 w-9 items-center justify-center rounded-lg bg-success-500">
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.2984 0.826822L15.2867 0.811822C14.7264 0.257759 13.8182 0.253452 13.2543 0.811822L13.2506 0.815572L6.10729 7.95892L2.74759 4.59922L2.74392 4.59554C2.18124 4.03717 1.27298 4.03717 0.710351 4.59554C0.148964 5.1524 0.148964 6.05622 0.710351 6.61308L0.714024 6.61676L5.08385 10.9866L5.08752 10.9903C5.64617 11.5443 6.55445 11.5486 7.11834 10.9903L7.12201 10.9866L15.2911 2.81754C15.8525 2.26067 15.8525 1.35685 15.2911 0.800041L15.2984 0.826822Z" fill="white" />
</svg>
</div>
<div class="w-full">
<h5 class="mb-2 text-lg font-semibold text-success-800 dark:text-[#34D399]">
Successfully
</h5>
<p class="text-sm text-success-700 dark:text-[#34D399]">
{{ session('success') }}
</p>
</div>
</div>
@endif
<!-- Table Section -->
<div class="overflow-hidden rounded-xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
<div class="max-w-full overflow-x-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-gray-50 dark:bg-gray-800">
<tr class="border-b border-gray-100 dark:border-gray-800">
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Page Title
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Slug
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Current Version
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Last Updated
</p>
</th>
<th class="px-5 py-3 text-left sm:px-6">
<p class="font-medium text-gray-500 text-theme-xs dark:text-gray-400">
Actions
</p>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
@foreach ($pages as $page)
<tr class="hover:bg-gray-50 dark:hover:bg-white/[0.02]">
<td class="px-5 py-4 sm:px-6">
<span class="block font-medium text-gray-800 text-theme-sm dark:text-white/90">
{{ $page->title }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<span class="text-gray-500 text-theme-sm dark:text-gray-400">
/legal/{{ $page->slug }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 dark:bg-blue-500/15 dark:text-blue-500">
v{{ $page->currentRevision->version ?? 'N/A' }}
</span>
</td>
<td class="px-5 py-4 sm:px-6">
<p class="text-gray-500 text-theme-sm dark:text-gray-400">
{{ $page->currentRevision ? $page->currentRevision->updated_at->format('M d, Y') : 'N/A' }}
</p>
</td>
<td class="px-5 py-4 sm:px-6">
<div class="flex items-center gap-3">
<a href="{{ route('admin.legal-pages.edit', $page->id) }}" class="text-brand-500 hover:text-brand-600 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path></svg>
</a>
<a href="{{ route('legal.show', $page->slug) }}" target="_blank" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</a>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,168 @@
@extends('layouts.app')
@section('content')
<div class="mx-auto max-w-(--breakpoint-2xl) p-4 md:p-6">
<!-- Breadcrumb -->
<div class="mb-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-title-md2 font-semibold text-black dark:text-white">
SMTP Tester
</h2>
<nav>
<ol class="flex items-center gap-2">
<li>
<a class="font-medium text-gray-500 hover:text-brand-500 dark:text-gray-400 dark:hover:text-brand-500"
href="{{ route('dashboard') }}">
Dashboard /
</a>
</li>
<li class="font-medium text-brand-500">SMTP Tester</li>
</ol>
</nav>
</div>
<div class="grid grid-cols-1 gap-9 sm:grid-cols-2">
<!-- Tester Form -->
<div class="flex flex-col gap-9">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<h3 class="font-semibold text-gray-900 dark:text-white">
Run Connection Test
</h3>
</div>
<form action="{{ route('admin.smtp-tester.send') }}" method="POST" class="p-6">
@csrf
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Select Mailer Configuration
</label>
<div class="relative z-20 bg-transparent dark:bg-form-input">
<select name="mailer" id="mailerSelect" class="relative z-20 w-full appearance-none rounded border border-stroke bg-transparent px-5 py-3 outline-none transition focus:border-brand-500 active:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-brand-500">
@foreach($configs as $key => $config)
<option value="{{ $key }}" {{ old('mailer') == $key ? 'selected' : '' }}>
{{ $config['name'] }} ({{ $config['host'] }}:{{ $config['port'] }})
</option>
@endforeach
</select>
<span class="absolute right-4 top-1/2 z-30 -translate-y-1/2">
<svg class="fill-current" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.10186 11.1044C5.97864 11.2384 5.91703 11.3857 5.91703 11.5457C5.91703 11.7214 5.97864 11.8726 6.10186 11.9991L11.5597 17.5108C11.6967 17.6492 11.8526 17.7184 12.0274 17.7184C12.2022 17.7184 12.3582 17.6492 12.4951 17.5108L17.8981 11.9991C18.0214 11.8726 18.083 11.7214 18.083 11.5457C18.083 11.3857 18.0214 11.2384 17.8981 11.1044C17.7612 10.9571 17.6052 10.8834 17.4304 10.8834C17.2556 10.8834 17.0997 10.9571 16.9628 11.1044L12.0274 16.1265L7.03714 11.1044C6.90022 10.9571 6.74426 10.8834 6.56948 10.8834C6.39469 10.8834 6.23873 10.9571 6.10186 11.1044Z" fill="currentColor"/>
</svg>
</span>
</div>
</div>
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Target Email Address
</label>
<input type="email" name="email" required placeholder="Enter your email to receive test..." value="{{ auth()->user()->email }}"
class="w-full rounded border border-stroke bg-transparent px-5 py-3 outline-none transition focus:border-brand-500 active:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:focus:border-brand-500" />
<p class="mt-1 text-xs text-gray-500">
We will send a raw test email to this address.
</p>
<div class="mb-4">
<label class="mb-2.5 block font-medium text-black dark:text-white">
Test Mode
</label>
<div class="flex gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mode" value="raw" checked class="text-brand-500 focus:ring-brand-500">
<span class="text-gray-900 dark:text-white">Raw Text (Connection Check)</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="mode" value="mailable" class="text-brand-500 focus:ring-brand-500">
<span class="text-gray-900 dark:text-white">ContactReply Mailable (Template Check)</span>
</label>
</div>
</div>
<button type="submit" class="flex w-full justify-center rounded bg-brand-500 p-3 font-medium text-gray hover:bg-opacity-90">
Send Test Email
</button>
</form>
</div>
@if(session('success'))
<div class="rounded-xl border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-900/10">
<div class="flex gap-3">
<div class="text-green-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>
</div>
<div>
<h4 class="font-bold text-green-900 dark:text-green-100">Test Successful</h4>
<p class="text-sm text-green-700 dark:text-green-300">{{ session('success') }}</p>
</div>
</div>
</div>
@endif
@if(session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800 dark:bg-red-900/10">
<div class="flex gap-3">
<div class="text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line></svg>
</div>
<div>
<h4 class="font-bold text-red-900 dark:text-red-100">Connection Failed</h4>
<p class="text-sm text-red-700 dark:text-red-300 break-all">{{ session('error') }}</p>
<p class="mt-2 text-xs text-red-600 dark:text-red-400">Please check your .env configuration and ensure your SMTP server is accessible.</p>
</div>
</div>
</div>
@endif
</div>
<!-- Configuration Details -->
<div class="flex flex-col gap-9">
<div class="rounded-xl border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-white/[0.03]">
<div class="border-b border-gray-200 px-6 py-4 dark:border-gray-800">
<h3 class="font-semibold text-gray-900 dark:text-white">
Current Configuration (Read-Only)
</h3>
</div>
<div class="p-6">
<div class="space-y-6">
@foreach($configs as $key => $config)
<div class="p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50 border border-gray-100 dark:border-gray-700">
<h4 class="font-bold text-brand-500 mb-3 uppercase text-xs tracking-wider">
{{ $config['name'] }}
</h4>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Host</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['host'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Port</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['port'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Username</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white text-ellipsis overflow-hidden">{{ $config['username'] ?? 'N/A' }}</dd>
</div>
<div class="sm:col-span-1">
<dt class="text-xs font-medium text-gray-500">Encryption</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white uppercase">{{ $config['encryption'] ?? 'None' }}</dd>
</div>
<div class="sm:col-span-2">
<dt class="text-xs font-medium text-gray-500">From Address</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">{{ $config['from'] ?? 'N/A' }}</dd>
</div>
</dl>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// Simple script to update expected 'From' display based on selection if needed,
// but the backend handles the actual sending.
</script>
@endsection

View File

@@ -0,0 +1,110 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Ticket Management
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Admin
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Tickets</li>
</ol>
</nav>
</div>
</div>
<x-common.component-card :title="$title">
<!-- Filters -->
<div class="mb-6 flex flex-col md:flex-row gap-4 justify-between">
<div class="flex gap-2">
<a href="{{ request()->fullUrlWithQuery(['status' => 'all']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'all' || !request('status') ? 'bg-brand-50 text-brand-700 dark:bg-brand-900/20 dark:text-brand-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">All</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'open']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'open' ? 'bg-blue-50 text-blue-700 dark:bg-blue-900/20 dark:text-blue-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Open</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'answered']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'answered' ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Answered</a>
<a href="{{ request()->fullUrlWithQuery(['status' => 'closed']) }}" class="px-3 py-1.5 text-sm font-medium rounded-lg {{ request('status') == 'closed' ? 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300' : 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800' }}">Closed</a>
</div>
<form method="GET" class="flex gap-2">
<input type="hidden" name="status" value="{{ request('status', 'all') }}">
<div class="relative">
<input type="text" name="search" value="{{ request('search') }}" placeholder="Search ID, Subject or User..." class="pl-9 pr-4 py-1.5 text-sm border border-gray-200 rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white focus:ring-brand-500 focus:border-brand-500 w-64">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
</div>
<button type="submit" class="px-3 py-1.5 text-sm font-medium text-white bg-gray-800 rounded-lg hover:bg-gray-700 dark:bg-gray-700 dark:hover:bg-gray-600">Filter</button>
</form>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-100 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Ticket Info</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">User</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Priority</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Updated</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-white/10">
@forelse($tickets as $ticket)
<tr class="hover:bg-gray-50 dark:hover:bg-white/5 transition">
<td class="px-6 py-4 whitespace-nowrap">
@php
$statusClass = match($ticket->status) {
'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
default => 'bg-gray-100 text-gray-600'
};
@endphp
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full {{ $statusClass }}">
{{ ucfirst($ticket->status) }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
<div class="font-medium text-gray-800 dark:text-white/90">#{{ $ticket->ticket_number }}</div>
<div class="truncate max-w-xs text-gray-500">{{ $ticket->subject }}</div>
<div class="text-xs text-gray-400 mt-1">{{ $ticket->category }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-300">
<div class="font-medium text-gray-800 dark:text-white">{{ $ticket->user->name }}</div>
<div class="text-xs text-gray-500">{{ $ticket->user->email }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 py-0.5 text-xs rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border border-gray-200 dark:border-gray-600">
{{ ucfirst($ticket->priority) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $ticket->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('admin.tickets.show', $ticket->id) }}" class="text-brand-500 hover:text-brand-600 dark:text-brand-400">Manage</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-10 text-center text-gray-500 dark:text-gray-400">
<p>No tickets found matching your criteria.</p>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $tickets->links() }}
</div>
</x-common.component-card>
@endsection

View File

@@ -0,0 +1,317 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Manage Ticket #{{ $ticket->ticket_number }}
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Admin
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li>
<a href="{{ route('admin.tickets.index') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Tickets
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Manage</li>
</ol>
</nav>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Chat Area -->
<div class="lg:col-span-2 space-y-6">
<x-common.component-card>
<x-slot:header>
<div class="mb-6 pb-6 border-b border-gray-100 dark:border-gray-700 flex justify-between items-start">
<div>
<h1 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-2">{{ $ticket->subject }}</h1>
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200">
{{ $ticket->category }}
</span>
</div>
</div>
</x-slot:header>
<div id="admin-ticket-chat-container" class="space-y-8">
@foreach($ticket->replies as $reply)
@php
// In Admin view: User messages on LEFT, Admin messages (ours) on RIGHT
// But since multiple admins might exist, we check if reply user is Admin Role or specific user
// Simpler: If reply->user_id == Auth::id() -> Right (It's ME)
// If reply->user->isAdmin() -> Right (It's a Colleague)
// Else (Customer) -> Left
$isStaff = $reply->user->isAdmin();
@endphp
<div class="flex {{ $isStaff ? 'justify-end' : 'justify-start' }}">
@if(!$isStaff)
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs font-bold text-blue-600 dark:text-blue-300">
{{ substr($reply->user->name, 0, 1) }}
</div>
</div>
@endif
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 {{ $isStaff ? 'text-right' : 'text-left' }}">
{{ $reply->user->name }} {{ $isStaff ? '(Staff)' : '' }} {{ $reply->created_at->format('M d, Y H:i A') }}
</div>
<div class="px-4 py-3 rounded-lg {{ $isStaff ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none' }}">
<p class="whitespace-pre-wrap text-sm">{{ $reply->message }}</p>
@if($reply->attachment_path)
<div class="mt-3 pt-3 border-t {{ $isStaff ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600' }}">
<a href="{{ Storage::url($reply->attachment_path) }}" target="_blank" class="flex items-center text-xs {{ $isStaff ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' }}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
@endif
</div>
</div>
@if($isStaff)
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
</div>
@endif
</div>
@endforeach
</div>
<div class="mt-8 pt-6 border-t border-gray-100 dark:border-gray-700">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Post Staff Reply</h3>
<form id="admin-ticket-reply-form" action="{{ route('admin.tickets.reply', $ticket->id) }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="mb-4">
<textarea id="reply-message" name="message" rows="4" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-brand-500 dark:focus:border-brand-500" placeholder="Type your reply here..." required></textarea>
</div>
<div class="mb-4" x-data="{ fileName: '' }">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="admin_attachment">Attachment (Optional)</label>
<div class="flex items-center justify-center w-full">
<label for="admin_attachment" class="flex flex-col items-center justify-center w-full h-24 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600 transition-colors">
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="!fileName">
<svg class="w-6 h-6 mb-2 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-1 text-xs text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-xs text-gray-500 dark:text-gray-400">JPG, PNG, PDF or DOCX (MAX. 2MB)</p>
</div>
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="fileName" style="display: none;">
<svg class="w-6 h-6 mb-2 text-brand-500 dark:text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="mb-1 text-xs text-gray-700 dark:text-gray-300 truncate max-w-xs" x-text="fileName"></p>
</div>
<input id="admin_attachment" name="attachment" type="file" class="hidden" accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" @change="fileName = $event.target.files[0] ? $event.target.files[0].name : ''" />
</label>
</div>
</div>
<div class="flex justify-end">
<button type="submit" id="submit-admin-reply" class="flex items-center text-white bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-brand-500 dark:hover:bg-brand-600 dark:focus:ring-brand-800 transition-all disabled:opacity-50">
<span id="submit-text">Send Staff Reply</span>
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</form>
</div>
</x-common.component-card>
</div>
<!-- Sidebar Info -->
<div class="space-y-6">
<!-- Ticket Status Control -->
<x-common.component-card title="Ticket Status">
<form action="{{ route('admin.tickets.update-status', $ticket->id) }}" method="POST">
@csrf
@method('PATCH')
<div class="space-y-4">
<div>
<label class="block mb-1.5 text-xs font-medium text-gray-700 dark:text-gray-400">Status</label>
<select name="status" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="open" {{ $ticket->status == 'open' ? 'selected' : '' }}>Open</option>
<option value="answered" {{ $ticket->status == 'answered' ? 'selected' : '' }}>Answered</option>
<option value="closed" {{ $ticket->status == 'closed' ? 'selected' : '' }}>Closed</option>
</select>
</div>
<div>
<label class="block mb-1.5 text-xs font-medium text-gray-700 dark:text-gray-400">Priority</label>
<select name="priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white">
<option value="low" {{ $ticket->priority == 'low' ? 'selected' : '' }}>Low</option>
<option value="medium" {{ $ticket->priority == 'medium' ? 'selected' : '' }}>Medium</option>
<option value="high" {{ $ticket->priority == 'high' ? 'selected' : '' }}>High</option>
</select>
</div>
<button type="submit" class="w-full text-white bg-gray-800 hover:bg-gray-900 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-gray-700 dark:hover:bg-gray-600 dark:focus:ring-gray-800">
Update Status
</button>
</div>
</form>
</x-common.component-card>
<!-- User Info -->
<x-common.component-card title="Customer Profile">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 rounded-full bg-brand-100 flex items-center justify-center text-lg font-bold text-brand-600">
{{ substr($ticket->user->name, 0, 1) }}
</div>
<div>
<div class="font-medium text-gray-900 dark:text-white">{{ $ticket->user->name }}</div>
<div class="text-xs text-gray-500">{{ $ticket->user->email }}</div>
</div>
</div>
<div class="pt-4 border-t border-gray-100 dark:border-gray-700">
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">Recent Tickets</h4>
@if($ticket->user->tickets->count() > 1)
<ul class="space-y-2">
@foreach($ticket->user->tickets as $userTicket)
@if($userTicket->id !== $ticket->id)
<li>
<a href="{{ route('admin.tickets.show', $userTicket->id) }}" class="flex items-center justify-between text-xs text-gray-600 dark:text-gray-400 hover:text-brand-500">
<span>#{{ $userTicket->ticket_number }}</span>
<span class="px-1.5 py-0.5 rounded-sm bg-gray-100 dark:bg-gray-800 text-gray-500">{{ $userTicket->status }}</span>
</a>
</li>
@endif
@endforeach
</ul>
@else
<p class="text-xs text-gray-400 italic">No other tickets.</p>
@endif
</div>
</x-common.component-card>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const ticketId = "{{ $ticket->id }}";
const currentUserId = "{{ Auth::id() }}";
const chatContainer = document.getElementById('admin-ticket-chat-container');
const replyForm = document.getElementById('admin-ticket-reply-form');
const submitBtn = document.getElementById('submit-admin-reply');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
const messageInput = document.getElementById('reply-message');
const appendMessage = (data) => {
// Check if message already exists to avoid duplicates
if (document.getElementById(`reply-${data.id}`)) return;
const isStaff = data.is_staff;
const messageHtml = `
<div id="reply-${data.id}" class="flex ${isStaff ? 'justify-end' : 'justify-start'} animate-fade-in-up mb-8 last:mb-0">
${!isStaff ? `
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center text-xs font-bold text-blue-600 dark:text-blue-300">
${data.user_name.substring(0, 1)}
</div>
</div>
` : ''}
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 ${isStaff ? 'text-right' : 'text-left'}">
${data.user_name} ${isStaff ? '(Staff)' : ''} ${data.created_at}
</div>
<div class="px-4 py-3 rounded-lg ${isStaff ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none'}">
<p class="whitespace-pre-wrap text-sm">${data.message}</p>
${data.attachment_url ? `
<div class="mt-3 pt-3 border-t ${isStaff ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600'}">
<a href="${data.attachment_url}" target="_blank" class="flex items-center text-xs ${isStaff ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
` : ''}
</div>
</div>
${isStaff ? `
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
</div>
` : ''}
</div>
`;
chatContainer.insertAdjacentHTML('beforeend', messageHtml);
// Scroll to bottom
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
};
// Listen for realtime messages
if (window.Echo) {
window.Echo.private(`ticket.${ticketId}`)
.listen('.ticket.message.sent', (e) => {
console.log('Realtime message received:', e);
appendMessage(e);
});
}
// Handle AJAX form submission
if (replyForm) {
replyForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(replyForm);
// UI States
submitBtn.disabled = true;
submitText.innerText = 'Sending...';
submitSpinner.classList.remove('hidden');
window.axios.post(replyForm.action, formData)
.then(response => {
if (response.data.success) {
// Append message locally immediately
appendMessage(response.data.reply);
// Reset form
replyForm.reset();
}
})
.catch(error => {
console.error('Error sending staff reply:', error);
alert('Failed to send reply. Please try again.');
})
.finally(() => {
submitBtn.disabled = false;
submitText.innerText = 'Send Staff Reply';
submitSpinner.classList.add('hidden');
});
});
}
});
</script>
<style>
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fadeInUp 0.3s ease-out forwards;
}
</style>
@endpush

View File

@@ -1,4 +1,7 @@
@extends('layouts.fullscreen-layout')
@extends('layouts.fullscreen-layout', ['title' => 'Reset Your Password'])
@section('meta_description', 'Recover access to your TrustLab account. Enter your email to receive a password reset link.')
@section('robots', 'noindex, nofollow')
@section('content')
<div class="relative z-1 bg-white p-6 sm:p-0 dark:bg-gray-900">
@@ -13,7 +16,7 @@
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke="" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to Home
Back to home
</a>
</div>
<div class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
@@ -76,7 +79,7 @@
<img src="{{ asset('images/logo/auth-logo.svg') }}" alt="Logo" />
</a>
<p class="text-center text-gray-400 dark:text-white/60">
Free and Open-Source Tailwind CSS Admin Dashboard Template
Professional Certificate Authority & API Management System
</p>
</div>
</div>

View File

@@ -1,4 +1,6 @@
@extends('layouts.fullscreen-layout')
@extends('layouts.fullscreen-layout', ['title' => 'Set New Password'])
@section('robots', 'noindex, nofollow')
@section('content')
<div class="relative z-1 bg-white p-6 sm:p-0 dark:bg-gray-900">
@@ -13,7 +15,7 @@
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke="" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to Home
Back to home
</a>
</div>
<div class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
@@ -108,7 +110,7 @@
<img src="{{ asset('images/logo/auth-logo.svg') }}" alt="Logo" />
</a>
<p class="text-center text-gray-400 dark:text-white/60">
Free and Open-Source Tailwind CSS Admin Dashboard Template
Professional Certificate Authority & API Management System
</p>
</div>
</div>

View File

@@ -5,6 +5,17 @@
<div class="relative flex h-screen w-full flex-col justify-center sm:p-0 lg:flex-row dark:bg-gray-900">
<!-- Form -->
<div class="flex w-full flex-1 flex-col lg:w-1/2">
<div class="mx-auto w-full max-w-md pt-10">
<a href="{{ route('home') }}"
class="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="stroke-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
viewBox="0 0 20 20" fill="none">
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke="" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to home
</a>
</div>
<div class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
<div>
<div class="mb-5 sm:mb-8">

View File

@@ -1,17 +1,20 @@
@extends('layouts.fullscreen-layout')
@extends('layouts.fullscreen-layout', ['title' => 'Sign In to Portal'])
@section('meta_description', 'Access your Certificate Authority dashboard and manage your API keys and certificates securely.')
@section('robots', 'noindex, nofollow')
@section('content')
<div class="relative z-1 bg-white p-6 sm:p-0 dark:bg-gray-900">
<div class="relative flex h-screen w-full flex-col justify-center sm:p-0 lg:flex-row dark:bg-gray-900">
<!-- Form -->
<div class="flex w-full flex-1 flex-col lg:w-1/2">
<div class="mx-auto w-full max-w-md pt-10">
<a href="{{ route('dashboard') }}"
<div class="mx-auto w-full max-w-md pt-5 sm:py-10">
<a href="{{ route('home') }}"
class="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
<svg class="stroke-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to dashboard
Back to home
</a>
</div>
<div class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
@@ -136,6 +139,12 @@
Don't have an account?
<a href="{{ route('signup') }}" class="text-brand-500 hover:text-brand-600 dark:text-brand-400">Sign Up</a>
</p>
<p class="mt-4 text-center text-xs text-gray-500 sm:text-start dark:text-gray-500">
By signing in, you agree to our
<a href="{{ route('legal.show', 'terms-and-conditions') }}" class="underline hover:text-gray-700 dark:hover:text-gray-300">Terms</a>
and
<a href="{{ route('legal.show', 'privacy-policy') }}" class="underline hover:text-gray-700 dark:hover:text-gray-300">Privacy Policy</a>.
</p>
</div>
</div>
</div>
@@ -151,7 +160,7 @@
<img src="./images/logo/auth-logo.svg" alt="Logo" />
</a>
<p class="text-center text-gray-400 dark:text-white/60">
Free and Open-Source Tailwind CSS Admin Dashboard Template
Professional Certificate Authority & API Management System
</p>
</div>
</div>

View File

@@ -1,4 +1,7 @@
@extends('layouts.fullscreen-layout')
@extends('layouts.fullscreen-layout', ['title' => 'Create Your Account'])
@section('meta_description', 'Get started with DyDev TrustLab. Create an account to manage your certificates and API keys with a unified dashboard.')
@section('robots', 'noindex, nofollow')
@section('content')
<div class="relative z-1 bg-white p-6 sm:p-0 dark:bg-gray-900">
@@ -11,7 +14,7 @@
<svg class="stroke-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to dashboard
Back to home
</a>
</div>
<div class="mx-auto flex w-full max-w-md flex-1 flex-col justify-center">
@@ -157,6 +160,12 @@
Already have an account?
<a href="{{ route('signin') }}" class="text-brand-500 hover:text-brand-600 dark:text-brand-400">Sign In</a>
</p>
<p class="mt-4 text-center text-xs text-gray-500 sm:text-start dark:text-gray-500">
By signing up, you agree to our
<a href="{{ route('legal.show', 'terms-and-conditions') }}" class="underline hover:text-gray-700 dark:hover:text-gray-300">Terms</a>
and
<a href="{{ route('legal.show', 'privacy-policy') }}" class="underline hover:text-gray-700 dark:hover:text-gray-300">Privacy Policy</a>.
</p>
</div>
</div>
</div>
@@ -170,7 +179,7 @@
<img src="./images/logo/auth-logo.svg" alt="Logo" />
</a>
<p class="text-center text-gray-400 dark:text-white/60">
Free and Open-Source Tailwind CSS Admin Dashboard Template
Professional Certificate Authority & API Management System
</p>
</div>
</div>

View File

@@ -1,4 +1,6 @@
@extends('layouts.fullscreen-layout')
@extends('layouts.fullscreen-layout', ['title' => 'Verify Your Email'])
@section('robots', 'noindex, nofollow')
@section('content')
<div class="relative z-1 bg-white p-6 sm:p-0 dark:bg-gray-900">
@@ -17,7 +19,9 @@
</svg>
</span>
<p class="text-gray-700 dark:text-gray-400">
Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you?
Thanks for signing up! Before getting started, could you verify your email address
<span class="font-bold text-gray-900 dark:text-white">({{ auth()->user()->email }})</span>
by clicking on the link we just emailed to you?
</p>
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
If you didn't receive the email, we will gladly send you another.
@@ -56,7 +60,7 @@
<img src="{{ asset('images/logo/auth-logo.svg') }}" alt="Logo" />
</a>
<p class="text-center text-gray-400 dark:text-white/60">
Secure Certificate Management System
Professional Certificate Authority & API Management System
</p>
</div>
</div>

View File

@@ -1,24 +1,125 @@
@extends('layouts.app')
@section('content')
<div class="space-y-6" x-data="{
loading: false,
latency: '---',
async refreshDashboard() {
this.loading = true;
window.location.reload();
},
async measureLatency() {
const start = performance.now();
try {
await fetch(window.location.origin + '/ping', { method: 'HEAD', cache: 'no-cache' });
const end = performance.now();
this.latency = Math.round(end - start) + 'ms';
} catch (e) {
this.latency = 'Offline';
<div class="space-y-6" x-data="dashboardData()">
@push('scripts')
<script>
function dashboardData() {
return {
loading: false,
latency: '---',
status: 'connecting',
stats: {
totalCertificates: {{ $totalCertificates }},
activeCertificates: {{ $activeCertificates }},
totalApiKeys: {{ $totalApiKeys }},
expiringSoonCount: {{ $expiringSoonCount }},
recentCertificates: @json($recentCertificates),
recentApiActivity: @json($recentApiActivity),
months: @json($months),
issuanceData: @json($issuanceData),
maxIssuance: {{ max($issuanceData) ?: 1 }}
},
async refreshDashboard() {
this.loading = true;
await this.fetchStats();
this.loading = false;
},
async fetchStats() {
try {
const response = await fetch('{{ route('dashboard.stats') }}');
const data = await response.json();
this.stats = data;
} catch (e) {
console.error('Failed to fetch stats:', e);
}
},
init() {
this.status = 'searching';
const setupEcho = () => {
if (window.Echo) {
console.log('Echo detected, joining private channel user.{{ auth()->id() }}');
const channel = window.Echo.private('user.{{ auth()->id() }}');
channel.listen('DashboardStatsUpdated', (e) => {
console.log('Dashboard stats updated event received');
this.fetchStats();
})
.listen('.PingResponse', (e) => {
if (this.pingStartTime) {
const end = performance.now();
this.latency = Math.round(end - this.pingStartTime) + 'ms';
console.log('WebSocket Latency received:', this.latency);
this.pingStartTime = null;
}
});
const updateStatus = () => {
if (window.Echo.connector && window.Echo.connector.pusher) {
const state = window.Echo.connector.pusher.connection.state;
console.log('WebSocket Connection State:', state);
this.status = state;
if (state === 'connected') {
this.measureLatency();
} else {
this.latency = '---';
}
} else {
console.warn('Echo connector or pusher not available');
this.status = 'unavailable';
}
};
window.Echo.connector.pusher.connection.bind('state_change', (states) => {
console.log('State change:', states.previous, '->', states.current);
updateStatus();
});
// Periodic refresh of latency if connected
setInterval(() => {
if (this.status === 'connected') {
this.measureLatency();
}
}, 5000);
updateStatus();
return true;
}
return false;
};
// Try immediately
if (!setupEcho()) {
// Try again every 500ms for up to 5 seconds
let attempts = 0;
const interval = setInterval(() => {
attempts++;
if (setupEcho() || attempts > 10) {
clearInterval(interval);
if (!window.Echo) {
console.error('Laravel Echo not found after 5 seconds');
this.status = 'unavailable';
}
}
}, 500);
}
},
pingStartTime: null,
async measureLatency() {
this.pingStartTime = performance.now();
try {
// Trigger a WebSocket round-trip via a lightweight endpoint
await fetch('{{ route('dashboard.ping') }}', { cache: 'no-cache' });
} catch (e) {
this.latency = 'Offline';
this.pingStartTime = null;
}
}
}
}
}" x-init="measureLatency(); setInterval(() => measureLatency(), 5000)">
</script>
@endpush
<!-- Top Header -->
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
@@ -50,12 +151,12 @@
</div>
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Certificates</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ $totalCertificates }}</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1" x-text="stats.totalCertificates">{{ $totalCertificates }}</span>
<span class="text-xs text-green-500 flex items-center gap-1 mt-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.395 6.227a.75.75 0 011.082.022l3.992 4.497a.75.75 0 01-1.104 1.012l-3.469-3.908-4.496 3.992a.75.75 0 01-1.012-1.104l5.007-4.511z" clip-rule="evenodd" />
</svg>
{{ $activeCertificates }} Active Now
<span x-text="stats.activeCertificates">{{ $activeCertificates }}</span> Active Now
</span>
</div>
</div>
@@ -69,9 +170,9 @@
</div>
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Manageable API Keys</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ $totalApiKeys }}</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1" x-text="stats.totalApiKeys">{{ $totalApiKeys }}</span>
<span class="text-xs text-blue-500 flex items-center gap-1 mt-2">
Latest usage: {{ $recentApiActivity->first()->last_used_at?->diffForHumans() ?? 'None' }}
Latest usage: <span x-text="stats.recentApiActivity[0]?.last_used_diff || 'None'">{{ $recentApiActivity->first()?->last_used_at?->diffForHumans() ?? 'None' }}</span>
</span>
</div>
</div>
@@ -85,7 +186,7 @@
</div>
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Expiring Soon</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1">{{ $expiringSoonCount }}</span>
<span class="text-3xl font-bold text-gray-900 dark:text-white mt-1" x-text="stats.expiringSoonCount">{{ $expiringSoonCount }}</span>
<span class="text-xs text-orange-500 flex items-center gap-1 mt-2">
Action required within 14 days
</span>
@@ -101,7 +202,16 @@
</div>
<div class="flex flex-col">
<span class="text-sm font-medium text-gray-500 dark:text-gray-400">Node Status</span>
<span class="text-3xl font-bold mt-1" :class="latency === 'Offline' ? 'text-red-500' : 'text-green-500'" x-text="latency === 'Offline' ? 'Offline' : 'Operational'">Operational</span>
<span class="text-3xl font-bold mt-1"
:class="{
'text-green-500': status === 'connected',
'text-yellow-500': status === 'connecting' || status === 'searching',
'text-red-500': status === 'offline' || status === 'unavailable' || status === 'failed'
}"
x-text="status === 'connected' ? 'Operational' :
(status === 'connecting' ? 'Connecting...' :
(status === 'searching' ? 'Initializing...' :
(status === 'unavailable' ? 'Echo Missing' : 'Offline')))">Operational</span>
<span class="text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1 mt-2">
Latency: <span x-text="latency"></span>
</span>
@@ -119,23 +229,18 @@
</div>
<div class="flex-1 flex items-end justify-between gap-2 min-h-[200px] px-2">
@foreach($issuanceData as $index => $count)
<template x-for="(count, index) in stats.issuanceData" :key="index">
<div class="flex-1 flex flex-col items-center gap-2 group">
<div class="relative w-full flex items-end justify-center">
@php
$max = max($issuanceData) ?: 1;
$percentage = ($count / $max) * 100;
@endphp
<div class="w-full max-w-[40px] bg-brand-500/20 dark:bg-brand-500/10 rounded-t-lg group-hover:bg-brand-500/30 transition-all cursor-pointer relative"
style="height: {{ max($percentage, 5) }}%;">
<div class="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 dark:bg-gray-700 text-white text-[10px] py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity">
{{ $count }}
:style="'height: ' + Math.max((count / stats.maxIssuance) * 100, 5) + '%;'">
<div class="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-900 dark:bg-gray-700 text-white text-[10px] py-1 px-2 rounded opacity-0 group-hover:opacity-100 transition-opacity" x-text="count">
</div>
</div>
</div>
<span class="text-[10px] font-bold text-gray-400 uppercase">{{ $months[$index] }}</span>
<span class="text-[10px] font-bold text-gray-400 uppercase" x-text="stats.months[index]"></span>
</div>
@endforeach
</template>
</div>
</div>
@@ -143,23 +248,22 @@
<div class="lg:col-span-4 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 p-6">
<h3 class="font-bold text-gray-900 dark:text-white mb-6">Recent API Activity</h3>
<div class="space-y-4">
@forelse($recentApiActivity as $activity)
<template x-for="activity in stats.recentApiActivity" :key="activity.name + activity.last_used_diff">
<div class="flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-900/50 rounded-xl border border-gray-100 dark:border-gray-700">
<div class="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg text-blue-600 dark:text-blue-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11.536 11 9 13.536 7.464 12 4.929 14.536V17h2.472l4.243-4.243a6 6 0 018.828-5.743zM16.5 13.5V18h6v-4.5h-6z" />
</svg>
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate">{{ $activity->name }}</p>
<p class="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5">Used {{ $activity->last_used_at->diffForHumans() }}</p>
<p class="text-sm font-semibold text-gray-900 dark:text-white truncate" x-text="activity.name"></p>
<p class="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5" x-text="'Used ' + activity.last_used_diff"></p>
</div>
</div>
@empty
<div class="text-center py-6">
<p class="text-sm text-gray-400">No recent activity detected.</p>
</div>
@endforelse
</template>
<div x-show="stats.recentApiActivity.length === 0" class="text-center py-6">
<p class="text-sm text-gray-400">No recent activity detected.</p>
</div>
</div>
</div>
@@ -167,7 +271,7 @@
<div class="lg:col-span-12 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div class="p-6 border-b border-gray-100 dark:border-gray-700 flex items-center justify-between">
<h3 class="font-bold text-gray-900 dark:text-white">Recently Issued Certificates</h3>
<a href="#" class="text-xs font-bold text-brand-500 hover:text-brand-600 uppercase tracking-wider">View All</a>
<a href="{{ route('certificate.index') }}" class="text-xs font-bold text-brand-500 hover:text-brand-600 uppercase tracking-wider">View All</a>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left">
@@ -181,25 +285,21 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@forelse($recentCertificates as $cert)
<template x-for="cert in stats.recentCertificates" :key="cert.common_name">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">{{ $cert->common_name }}</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $cert->organization }}</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $cert->created_at->format('M d, Y') }}</td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ $cert->valid_to->format('M d, Y') }}</td>
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white" x-text="cert.common_name"></td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400" x-text="cert.organization"></td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400" x-text="cert.created_at"></td>
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400" x-text="cert.valid_to"></td>
<td class="px-6 py-4 text-right">
@if($cert->valid_to > now())
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-500 uppercase">Valid</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-500 uppercase">Expired</span>
@endif
<span x-show="cert.is_valid" class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-500 uppercase">Valid</span>
<span x-show="!cert.is_valid" class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-500 uppercase">Expired</span>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400">No certificates found.</td>
</tr>
@endforelse
</template>
<tr x-show="stats.recentCertificates.length === 0">
<td colspan="5" class="px-6 py-8 text-center text-sm text-gray-500 dark:text-gray-400">No certificates found.</td>
</tr>
</tbody>
</table>
</div>
@@ -207,3 +307,4 @@
</div>
</div>
@endsection

View File

@@ -0,0 +1,48 @@
@extends('layouts.fullscreen-layout', ['title' => $page->title])
@section('content')
<div class="relative min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300 overflow-hidden flex flex-col">
<!-- Background Decoration -->
<div class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-brand-500/10 rounded-full blur-[120px] pointer-events-none"></div>
<div class="absolute bottom-0 left-0 -translate-x-1/2 translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-[150px] pointer-events-none"></div>
<x-public.navbar />
<main class="flex-grow pt-32 pb-20 px-4 relative z-10">
<div class="max-w-4xl mx-auto px-6">
<!-- Header -->
<header class="mb-12 border-b border-gray-100 pb-8 dark:border-gray-800 text-center sm:text-left">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 text-[10px] font-bold uppercase tracking-widest mb-6">
📜 Legal Document
</div>
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white mb-6">
{{ $page->title }}
</h1>
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-4 text-sm text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-2 bg-white dark:bg-gray-800 px-3 py-1.5 rounded-lg shadow-sm border border-gray-100 dark:border-gray-700">
<svg class="text-brand-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Last updated: {{ $revision->created_at->format('M d, Y') }}
</span>
<span class="flex items-center gap-2 bg-white dark:bg-gray-800 px-3 py-1.5 rounded-lg shadow-sm border border-gray-100 dark:border-gray-700">
<svg class="text-brand-500" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
Version {{ $revision->version }}
</span>
</div>
</header>
<!-- Content -->
<article class="prose prose-lg prose-gray dark:prose-invert max-w-none
prose-headings:font-bold prose-headings:text-gray-900 dark:prose-headings:text-white
prose-p:text-gray-600 dark:prose-p:text-gray-400 prose-p:leading-relaxed
prose-strong:text-brand-600 dark:prose-strong:text-brand-400
prose-a:text-brand-500 hover:prose-a:text-brand-600 prose-a:font-semibold
prose-li:text-gray-600 dark:prose-li:text-gray-400
prose-pre:bg-gray-50 dark:prose-pre:bg-gray-800/50 prose-pre:border prose-pre:border-gray-100 dark:prose-pre:border-gray-700">
{!! Str::markdown($revision->content) !!}
</article>
</div>
</main>
<x-public.footer />
</div>
@endsection

View File

@@ -0,0 +1,90 @@
@extends('layouts.fullscreen-layout', ['title' => 'Contact Us'])
@section('content')
<div class="relative min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300 overflow-hidden flex flex-col">
<!-- Background Decoration -->
<div class="absolute top-0 left-0 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-brand-500/10 rounded-full blur-[120px] pointer-events-none"></div>
<div class="absolute bottom-0 right-0 translate-x-1/2 translate-y-1/2 w-[600px] h-[600px] bg-brand-500/5 rounded-full blur-[150px] pointer-events-none"></div>
<x-public.navbar />
<main class="flex-grow pt-32 pb-20 px-4 relative z-10">
<div class="max-w-xl mx-auto space-y-8">
<div class="text-center">
<h1 class="text-4xl font-extrabold text-gray-900 dark:text-white mb-4">
Contact Our Team
</h1>
<p class="text-gray-600 dark:text-gray-400">
Have a question or need legal assistance? We're here to help.
</p>
</div>
@if (session('success'))
<div class="rounded-2xl bg-green-50 p-4 dark:bg-green-900/30 border border-green-200 dark:border-green-800 shadow-sm animate-pulse-soft">
<div class="flex items-center gap-3">
<svg class="h-5 w-5 text-green-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<p class="text-sm font-bold text-green-800 dark:text-green-200">
{{ session('success') }}
</p>
</div>
</div>
@endif
<div class="bg-white px-8 py-10 shadow-2xl rounded-[2.5rem] dark:bg-white/[0.03] border border-gray-100 dark:border-gray-800 backdrop-blur-sm">
<form action="{{ route('contact.store') }}" method="POST" class="space-y-6">
@csrf
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label for="name" class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Name</label>
<input type="text" name="name" id="name" required value="{{ old('name') }}"
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 px-5 py-4 text-sm text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white">
@error('name')<p class="mt-1 text-[10px] text-red-500 font-bold uppercase">{{ $message }}</p>@enderror
</div>
<div>
<label for="email" class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Email Address</label>
<input type="email" name="email" id="email" required value="{{ old('email') }}"
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 px-5 py-4 text-sm text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white">
@error('email')<p class="mt-1 text-[10px] text-red-500 font-bold uppercase">{{ $message }}</p>@enderror
</div>
</div>
<div>
<label for="category" class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Category</label>
<select name="category" id="category" required
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 px-5 py-4 text-sm text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white appearance-none">
<option value="Technical Support">Technical Support</option>
<option value="Legal Inquiry">Legal Inquiry</option>
<option value="Partnership">Partnership</option>
<option value="Other">Other</option>
</select>
</div>
<div>
<label for="subject" class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Subject</label>
<input type="text" name="subject" id="subject" required value="{{ old('subject') }}"
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 px-5 py-4 text-sm text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white">
</div>
<div>
<label for="message" class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Message</label>
<textarea name="message" id="message" rows="4" required
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 px-5 py-4 text-sm text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white resize-none">{{ old('message') }}</textarea>
</div>
<div>
<button type="submit"
class="flex w-full justify-center rounded-2xl bg-brand-500 px-4 py-5 text-sm font-bold text-white shadow-xl shadow-brand-500/30 hover:bg-brand-600 focus:outline-none focus:ring-2 focus:ring-brand-500 transition-all active:scale-[0.98]">
Send Message
</button>
</div>
</form>
</div>
</div>
</main>
<x-public.footer />
</div>
@endsection

View File

@@ -0,0 +1,101 @@
@extends('layouts.fullscreen-layout', ['title' => 'Laravel APP_KEY Generator'])
@section('content')
<div class="relative min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300 overflow-hidden flex flex-col">
<!-- Background Decoration -->
<div class="absolute top-0 right-0 translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-brand-500/10 rounded-full blur-[120px] pointer-events-none"></div>
<div class="absolute bottom-0 left-0 -translate-x-1/2 translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-[150px] pointer-events-none"></div>
<x-public.navbar />
<main class="flex-grow pt-32 pb-20 px-4 relative z-10">
<div class="max-w-2xl mx-auto space-y-12">
<div class="text-center">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 text-blue-600 dark:text-blue-400 text-[10px] font-bold uppercase tracking-widest mb-6">
🔐 Security Utility
</div>
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white mb-4">
Key Generator
</h1>
<p class="text-gray-600 dark:text-gray-400 max-w-md mx-auto">
Generate a production-ready 32-byte <code>APP_KEY</code> for your Laravel application securely in your browser.
</p>
</div>
<div x-data="{
generatedKey: '',
copying: false,
generate() {
const array = new Uint8Array(32);
window.crypto.getRandomValues(array);
const binary = Array.from(array, byte => String.fromCharCode(byte)).join('');
this.generatedKey = 'base64:' + btoa(binary);
},
copy() {
if (!this.generatedKey) return;
navigator.clipboard.writeText(this.generatedKey);
this.copying = true;
setTimeout(() => this.copying = false, 2000);
}
}" x-init="generate()" class="bg-white px-8 py-10 shadow-2xl rounded-[2.5rem] dark:bg-white/[0.03] border border-gray-100 dark:border-gray-800 backdrop-blur-sm">
<div class="space-y-8">
<div>
<label class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-4 text-center">Your Generated Security Key</label>
<div class="relative group">
<div class="w-full rounded-2xl border-gray-200 bg-gray-50/50 p-6 text-sm font-mono text-gray-900 transition dark:border-gray-700 dark:bg-gray-800/50 dark:text-white break-all text-center min-h-[60px] flex items-center justify-center"
x-text="generatedKey || 'Generating...'">
</div>
<button @click="copy()" x-show="generatedKey"
class="absolute top-1/2 -right-4 -translate-y-1/2 p-4 bg-brand-500 text-white rounded-2xl shadow-xl shadow-brand-500/30 hover:scale-110 transition-all active:scale-95"
title="Copy to clipboard">
<svg x-show="!copying" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
<svg x-show="copying" style="display: none;" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="text-white"><polyline points="20 6 9 17 4 12"></polyline></svg>
</button>
<div x-show="copying" x-transition style="display: none;" class="absolute -top-12 left-1/2 -translate-x-1/2 bg-brand-600 text-white text-[10px] font-bold px-4 py-1.5 rounded-full shadow-lg">
COPIED TO CLIPBOARD
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-4 pt-4">
<button @click="generate()"
class="flex-1 rounded-2xl bg-gray-900 dark:bg-white text-white dark:text-gray-900 px-6 py-4 text-sm font-bold shadow-lg transition-all hover:-translate-y-0.5 active:scale-95">
Generate New Key
</button>
<button @click="copy()"
class="flex-1 rounded-2xl bg-brand-500 text-white px-6 py-4 text-sm font-bold shadow-lg shadow-brand-500/20 transition-all hover:-translate-y-0.5 active:scale-95">
Copy to .env
</button>
</div>
<div class="p-6 bg-gray-50 dark:bg-gray-800/50 rounded-3xl border border-gray-100 dark:border-gray-700">
<h4 class="text-xs font-bold text-gray-800 dark:text-white uppercase tracking-widest mb-3 flex items-center gap-2">
Quick Guide
</h4>
<ul class="text-[11px] text-gray-500 dark:text-gray-400 space-y-2 font-medium">
<li class="flex gap-2">
<span class="text-brand-500 font-bold">1.</span>
<div>Copy the generated key above.</div>
</li>
<li class="flex gap-2">
<span class="text-brand-500 font-bold">2.</span>
<div>Open your <code>.env</code> file in your Laravel project root.</div>
</li>
<li class="flex gap-2">
<span class="text-brand-500 font-bold">3.</span>
<div>Update the <code>APP_KEY=</code> variable with this key.</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<x-public.footer />
</div>
@endsection

View File

@@ -0,0 +1,185 @@
@extends('layouts.fullscreen-layout', ['title' => 'Telegram Chat ID Finder'])
@section('content')
<div class="relative min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300 overflow-hidden flex flex-col">
<!-- Background Decoration -->
<div class="absolute top-0 left-0 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-brand-500/10 rounded-full blur-[120px] pointer-events-none"></div>
<div class="absolute bottom-0 right-0 translate-x-1/2 translate-y-1/2 w-[600px] h-[600px] bg-brand-500/5 rounded-full blur-[150px] pointer-events-none"></div>
<x-public.navbar />
<main class="flex-grow pt-32 pb-20 px-4 relative z-10">
<div class="max-w-3xl mx-auto space-y-12">
<div class="text-center">
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-brand-50 dark:bg-brand-500/10 border border-brand-100 dark:border-brand-500/20 text-brand-600 dark:text-brand-400 text-[10px] font-bold uppercase tracking-widest mb-6">
🛠️ Developer Utility
</div>
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white mb-4">
Chat ID Finder
</h1>
<p class="text-gray-600 dark:text-gray-400 max-w-lg mx-auto">
Quickly retrieve your Telegram Chat ID using your Bot Token. All processing happens locally in your browser.
</p>
</div>
<div x-data="{
botToken: '',
chats: [],
loading: false,
error: '',
copiedId: null,
async findChats() {
if (!this.botToken) {
this.error = 'Please enter a Bot Token';
return;
}
this.loading = true;
this.error = '';
this.chats = [];
try {
const response = await fetch(`https://api.telegram.org/bot${this.botToken}/getUpdates`);
const data = await response.json();
if (!data.ok) {
this.error = data.description || 'Invalid token or API error';
return;
}
if (data.result.length === 0) {
this.error = 'No recent messages found. Please send a message to your bot first!';
return;
}
// Extract unique chats
const uniqueChats = {};
data.result.forEach(update => {
const message = update.message || update.edited_message || update.callback_query?.message;
if (message && message.chat) {
uniqueChats[message.chat.id] = {
id: message.chat.id,
name: message.chat.title || message.chat.first_name || 'Group/Channel',
username: message.chat.username ? `@${message.chat.username}` : 'N/A',
type: message.chat.type
};
}
});
this.chats = Object.values(uniqueChats);
if (this.chats.length === 0) {
this.error = 'Could not find any chat information in recent updates.';
}
} catch (err) {
this.error = 'Network error. Please check your connection.';
console.error(err);
} finally {
this.loading = false;
}
},
copyToClipboard(text, id) {
navigator.clipboard.writeText(text);
this.copiedId = id;
setTimeout(() => this.copiedId = null, 2000);
}
}" class="bg-white px-8 py-10 shadow-2xl rounded-[2.5rem] dark:bg-white/[0.03] border border-gray-100 dark:border-gray-800 backdrop-blur-sm">
<div class="space-y-8">
<div>
<label class="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-4">Telegram Bot Token</label>
<div class="relative group">
<input
type="text"
x-model="botToken"
placeholder="123456789:ABCDE..."
class="w-full rounded-2xl border-gray-200 bg-gray-50/50 p-5 text-sm font-mono text-gray-900 transition focus:ring-brand-500 focus:border-brand-500 dark:border-gray-700 dark:bg-gray-800/50 dark:text-white"
@keydown.enter="findChats()"
/>
<button
@click="findChats()"
class="absolute right-2 top-2 bottom-2 px-6 bg-brand-500 text-white text-xs font-bold rounded-xl hover:bg-brand-600 transition-all flex items-center justify-center gap-2 shadow-lg shadow-brand-500/20"
:disabled="loading"
>
<span x-show="!loading">Fetch Updates</span>
<svg x-show="loading" class="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</div>
<!-- Error Message -->
<div x-show="error" x-transition x-cloak class="p-4 rounded-2xl bg-red-50 dark:bg-red-500/10 border border-red-100 dark:border-red-500/20 text-red-600 dark:text-red-400 text-sm flex gap-3">
<svg class="h-5 w-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
<span x-text="error"></span>
</div>
<!-- Detected Chats -->
<div x-show="chats.length > 0" x-transition x-cloak class="space-y-4">
<h3 class="text-sm font-bold text-gray-900 dark:text-white flex items-center gap-2">
Detected Chats
<span class="px-2 py-0.5 bg-brand-50 dark:bg-brand-500/20 text-brand-600 dark:text-brand-400 text-[10px] rounded-full" x-text="chats.length"></span>
</h3>
<div class="overflow-hidden rounded-2xl border border-gray-100 dark:border-gray-800">
<table class="w-full text-left text-sm">
<thead class="bg-gray-50/50 dark:bg-gray-800/50">
<tr>
<th class="px-5 py-4 font-bold text-gray-500 dark:text-gray-400 uppercase text-[10px] tracking-wider">Chat Identity</th>
<th class="px-5 py-4 font-bold text-gray-500 dark:text-gray-400 uppercase text-[10px] tracking-wider">Numeric ID</th>
<th class="px-5 py-4"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<template x-for="chat in chats" :key="chat.id">
<tr class="hover:bg-gray-50/50 dark:hover:bg-gray-800/20 transition-colors group">
<td class="px-5 py-4">
<div class="font-bold text-gray-900 dark:text-white" x-text="chat.name"></div>
<div class="text-[11px] text-gray-500 dark:text-gray-500 font-mono" x-text="chat.username"></div>
</td>
<td class="px-5 py-4">
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-800 text-brand-600 dark:text-brand-400 rounded-lg font-mono text-xs font-bold" x-text="chat.id"></code>
</td>
<td class="px-5 py-4 text-right">
<button
@click="copyToClipboard(chat.id, chat.id)"
class="p-2.5 rounded-xl bg-gray-50 dark:bg-gray-800 text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-all border border-gray-100 dark:border-gray-700 active:scale-90"
>
<svg x-show="copiedId !== chat.id" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3" />
</svg>
<svg x-show="copiedId === chat.id" class="h-4 w-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="display: none;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
<div class="p-6 bg-brand-50/50 dark:bg-brand-500/5 rounded-3xl border border-brand-100 dark:border-brand-500/10">
<h4 class="text-xs font-bold text-brand-600 dark:text-brand-400 uppercase tracking-widest mb-4 flex items-center gap-2">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Instructions
</h4>
<ol class="text-xs text-gray-600 dark:text-gray-400 space-y-3 list-decimal ml-4 font-medium">
<li>Send a message (e.g., "Hello") to your Bot from the account or group you want the ID from.</li>
<li>Paste your <strong>Bot Token</strong> above and click <strong>Fetch Updates</strong>.</li>
<li>Your <code>CHAT_ID</code> will appear in the table. Copy it for use in your API or integrations.</li>
</ol>
</div>
</div>
</div>
</div>
</main>
<x-public.footer />
</div>
@endsection

View File

@@ -0,0 +1,105 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Open New Ticket
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Home
</a>
</li>
<li>
<a href="{{ route('support.index') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Support Tickets
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Open Ticket</li>
</ol>
</nav>
</div>
</div>
<x-common.component-card :title="$title">
<form action="{{ route('support.store') }}" method="POST" enctype="multipart/form-data" class="space-y-6">
@csrf
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Subject -->
<div class="col-span-2">
<label for="subject" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Subject</label>
<input type="text" name="subject" id="subject" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-400 dark:text-white" placeholder="Briefly describe your issue" required>
@error('subject') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Category -->
<div>
<label for="category" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Category</label>
<select name="category" id="category" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-400 dark:text-white" required>
<option value="Technical">Technical Support</option>
<option value="Billing">Billing & Subscription</option>
<option value="General">General Inquiry</option>
<option value="Feature Request">Feature Request</option>
<option value="Other">Other</option>
</select>
@error('category') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Priority -->
<div>
<label for="priority" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Priority</label>
<select name="priority" id="priority" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-400 dark:text-white" required>
<option value="low">Low - General Question</option>
<option value="medium" selected>Medium - Need Assistance</option>
<option value="high">High - Urgent Issue</option>
</select>
@error('priority') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Message -->
<div class="col-span-2">
<label for="message" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Message</label>
<textarea name="message" id="message" rows="6" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-700 dark:placeholder-gray-400 dark:text-white" placeholder="Please provide detailed information about your issue..." required></textarea>
@error('message') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
<!-- Attachment -->
<div class="col-span-2" x-data="{ fileName: '' }">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="attachment">Attachment (Optional)</label>
<div class="flex items-center justify-center w-full">
<label for="attachment" class="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600 transition-colors">
<div class="flex flex-col items-center justify-center pt-5 pb-6 p-4 text-center" x-show="!fileName">
<svg class="w-8 h-8 mb-3 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-1 text-sm text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-gray-500 dark:text-gray-400">JPG, PNG, PDF or DOCX (MAX. 2MB)</p>
</div>
<div class="flex flex-col items-center justify-center pt-5 pb-6 p-4 text-center" x-show="fileName" style="display: none;">
<svg class="w-8 h-8 mb-3 text-brand-500 dark:text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="mb-1 text-sm text-gray-700 dark:text-gray-300 truncate max-w-xs" x-text="fileName"></p>
<p class="text-xs text-brand-500 dark:text-brand-400 font-medium">Click to change file</p>
</div>
<input id="attachment" name="attachment" type="file" class="hidden" accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" @change="fileName = $event.target.files[0] ? $event.target.files[0].name : ''" />
</label>
</div>
@error('attachment') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
</div>
</div>
<div class="flex justify-end pt-4 border-t border-gray-100 dark:border-gray-700">
<button type="submit" class="text-white bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-brand-500 dark:hover:bg-brand-600 dark:focus:ring-brand-800">
Submit Ticket
</button>
</div>
</form>
</x-common.component-card>
@endsection

View File

@@ -0,0 +1,95 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
My Support Tickets
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Home
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">Support Tickets</li>
</ol>
</nav>
</div>
<div>
<a href="{{ route('support.create') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition rounded-lg bg-brand-500 hover:bg-brand-600 shadow-theme-xs">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
Open New Ticket
</a>
</div>
</div>
<x-common.component-card :title="$title">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-100 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Ticket ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Subject</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Category</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Last Updated</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider dark:text-gray-400">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-white/10">
@forelse($tickets as $ticket)
<tr class="hover:bg-gray-50 dark:hover:bg-white/5 transition">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-white/90">
#{{ $ticket->ticket_number }}
</td>
<td class="px-6 py-4 text-sm text-gray-600 dark:text-gray-300">
<div class="font-medium text-gray-800 dark:text-white/90 mb-0.5 truncate max-w-xs">{{ $ticket->subject }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200">
{{ $ticket->category }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
@php
$statusClass = match($ticket->status) {
'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
default => 'bg-gray-100 text-gray-600'
};
@endphp
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full {{ $statusClass }}">
{{ ucfirst($ticket->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
{{ $ticket->updated_at->diffForHumans() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('support.show', $ticket->id) }}" class="text-brand-500 hover:text-brand-600 dark:text-brand-400">View</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-6 py-10 text-center text-gray-500 dark:text-gray-400">
<div class="flex flex-col items-center justify-center">
<svg class="w-10 h-10 mb-3 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path></svg>
<p>No tickets found.</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-4">
{{ $tickets->links() }}
</div>
</x-common.component-card>
@endsection

View File

@@ -0,0 +1,299 @@
@extends('layouts.app')
@section('content')
<div class="flex flex-col gap-4 mb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<h2 class="text-xl font-semibold text-gray-800 dark:text-white/90">
Ticket #{{ $ticket->ticket_number }}
</h2>
<nav class="mt-1">
<ol class="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
<li>
<a href="{{ route('dashboard') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
Home
</a>
</li>
<li>
<a href="{{ route('support.index') }}" class="inline-flex items-center gap-1.5 hover:text-brand-500 transition">
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Support Tickets
</a>
</li>
<li>
<svg class="stroke-current opacity-60" width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.75 2.5L6.25 5L3.75 7.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</li>
<li class="font-medium text-gray-800 dark:text-white/90">View Ticket</li>
</ol>
</nav>
</div>
<div>
@if($ticket->status !== 'closed')
<form action="{{ route('support.close', $ticket->id) }}" method="POST" onsubmit="return confirm('Are you sure you want to close this ticket?');">
@csrf
<button type="submit" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white transition rounded-lg bg-red-500 hover:bg-red-600 shadow-theme-xs">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
Close Ticket
</button>
</form>
@endif
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Chat Area -->
<div class="lg:col-span-2 space-y-6">
<x-common.component-card>
<x-slot:header>
<div class="flex items-start justify-between">
<h3 class="text-xl font-bold text-gray-800 dark:text-gray-100">{{ $ticket->subject }}</h3>
<div class="flex flex-wrap gap-2">
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200">
{{ $ticket->category }}
</span>
@php
$statusClass = match($ticket->status) {
'open' => 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400',
'answered' => 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400',
'closed' => 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400',
default => 'bg-gray-100 text-gray-600'
};
@endphp
<span class="px-2.5 py-0.5 inline-flex items-center text-xs font-medium rounded-full {{ $statusClass }}">
{{ ucfirst($ticket->status) }}
</span>
</div>
</div>
</x-slot:header>
<div id="ticket-chat-container" class="space-y-8">
@foreach($ticket->replies as $reply)
@php
$isMe = $reply->user_id === Auth::id();
@endphp
<div class="flex {{ $isMe ? 'justify-end' : 'justify-start' }}">
@if(!$isMe)
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300">
{{ substr($reply->user->name, 0, 1) }}
</div>
</div>
@endif
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 {{ $isMe ? 'text-right' : 'text-left' }}">
{{ $reply->user->name }} {{ $reply->created_at->format('M d, Y H:i A') }}
</div>
<div class="px-4 py-3 rounded-lg {{ $isMe ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none' }}">
<p class="whitespace-pre-wrap text-sm">{{ $reply->message }}</p>
@if($reply->attachment_path)
<div class="mt-3 pt-3 border-t {{ $isMe ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600' }}">
<a href="{{ Storage::url($reply->attachment_path) }}" target="_blank" class="flex items-center text-xs {{ $isMe ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200' }}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
@endif
</div>
</div>
@if($isMe)
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
Me
</div>
</div>
@endif
</div>
@endforeach
</div>
@if($ticket->status !== 'closed')
<div class="mt-8 pt-6 border-t border-gray-100 dark:border-gray-700">
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-4">Post a Reply</h3>
<form id="ticket-reply-form" action="{{ route('support.reply', $ticket->id) }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="mb-4">
<textarea id="reply-message" name="message" rows="4" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-brand-500 dark:focus:border-brand-500" placeholder="Type your reply here..." required></textarea>
</div>
<div class="mb-4" x-data="{ fileName: '' }">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" for="attachment">Attachment (Optional)</label>
<div class="flex items-center justify-center w-full">
<label for="attachment" class="flex flex-col items-center justify-center w-full h-24 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 dark:hover:bg-gray-800 dark:bg-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:hover:border-gray-500 dark:hover:bg-gray-600 transition-colors">
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="!fileName">
<svg class="w-6 h-6 mb-2 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-1 text-xs text-gray-500 dark:text-gray-400"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-xs text-gray-500 dark:text-gray-400">JPG, PNG, PDF or DOCX (MAX. 2MB)</p>
</div>
<div class="flex flex-col items-center justify-center pt-3 pb-4 p-2 text-center" x-show="fileName" style="display: none;">
<svg class="w-6 h-6 mb-2 text-brand-500 dark:text-brand-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="mb-1 text-xs text-gray-700 dark:text-gray-300 truncate max-w-xs" x-text="fileName"></p>
</div>
<input id="attachment" name="attachment" type="file" class="hidden" accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" @change="fileName = $event.target.files[0] ? $event.target.files[0].name : ''" />
</label>
</div>
</div>
<div class="flex justify-end">
<button type="submit" id="submit-reply" class="flex items-center text-white bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-brand-500 dark:hover:bg-brand-600 dark:focus:ring-brand-800 transition-all disabled:opacity-50">
<span id="submit-text">Send Reply</span>
<svg id="submit-spinner" class="hidden animate-spin ml-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</button>
</div>
</form>
</div>
@else
<div class="mt-8 pt-6 border-t border-gray-100 dark:border-gray-700 text-center">
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-gray-800 dark:text-yellow-300" role="alert">
<span class="font-medium">This ticket is closed.</span> You cannot reply to this ticket anymore. Please open a new ticket if you need further assistance.
</div>
</div>
@endif
</x-common.component-card>
</div>
<!-- Sidebar Info -->
<div class="space-y-6">
<x-common.component-card title="Ticket Info">
<dl class="space-y-3 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Created</dt>
<dd class="text-gray-900 dark:text-white font-medium">{{ $ticket->created_at->format('M d, Y') }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Last Updated</dt>
<dd class="text-gray-900 dark:text-white font-medium">{{ $ticket->updated_at->diffForHumans() }}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">Priority</dt>
<dd>
<span class="px-2 py-0.5 text-xs rounded bg-gray-100 dark:bg-gray-700">{{ ucfirst($ticket->priority) }}</span>
</dd>
</div>
</dl>
</x-common.component-card>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const ticketId = "{{ $ticket->id }}";
const currentUserId = "{{ Auth::id() }}";
const chatContainer = document.getElementById('ticket-chat-container');
const replyForm = document.getElementById('ticket-reply-form');
const submitBtn = document.getElementById('submit-reply');
const submitText = document.getElementById('submit-text');
const submitSpinner = document.getElementById('submit-spinner');
const messageInput = document.getElementById('reply-message');
const appendMessage = (data) => {
// Check if message already exists to avoid duplicates
if (document.getElementById(`reply-${data.id}`)) return;
const isMe = data.user_id === currentUserId;
const messageHtml = `
<div id="reply-${data.id}" class="flex ${isMe ? 'justify-end' : 'justify-start'} animate-fade-in-up mb-8 last:mb-0">
${!isMe ? `
<div class="flex-shrink-0 mr-3">
<div class="w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-xs font-bold text-gray-600 dark:text-gray-300">
${data.user_name.substring(0, 1)}
</div>
</div>
` : ''}
<div class="max-w-xl">
<div class="text-xs text-gray-500 mb-1 ${isMe ? 'text-right' : 'text-left'}">
${data.user_name} ${data.created_at}
</div>
<div class="px-4 py-3 rounded-lg ${isMe ? 'bg-brand-500 text-white rounded-tr-none' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-tl-none'}">
<p class="whitespace-pre-wrap text-sm">${data.message}</p>
${data.attachment_url ? `
<div class="mt-3 pt-3 border-t ${isMe ? 'border-brand-400' : 'border-gray-200 dark:border-gray-600'}">
<a href="${data.attachment_url}" target="_blank" class="flex items-center text-xs ${isMe ? 'text-brand-100 hover:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'}">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
Attachment
</a>
</div>
` : ''}
</div>
</div>
${isMe ? `
<div class="flex-shrink-0 ml-3">
<div class="w-8 h-8 rounded-full bg-brand-200 flex items-center justify-center text-xs font-bold text-brand-700">
Me
</div>
</div>
` : ''}
</div>
`;
chatContainer.insertAdjacentHTML('beforeend', messageHtml);
// Scroll to bottom
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
};
// Listen for realtime messages
if (window.Echo) {
window.Echo.private(`ticket.${ticketId}`)
.listen('.ticket.message.sent', (e) => {
console.log('Realtime message received:', e);
appendMessage(e);
});
}
// Handle AJAX form submission
if (replyForm) {
replyForm.addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(replyForm);
// UI States
submitBtn.disabled = true;
submitText.innerText = 'Sending...';
submitSpinner.classList.remove('hidden');
window.axios.post(replyForm.action, formData)
.then(response => {
if (response.data.success) {
// Append message locally immediately
appendMessage(response.data.reply);
// Reset form
replyForm.reset();
// If Alpine.js is used for fileName, it might need manual reset or Alpine would handle it if we used x-model
// But fileName is in a separate x-data on the label's parent.
}
})
.catch(error => {
console.error('Error sending reply:', error);
alert('Failed to send reply. Please try again.');
})
.finally(() => {
submitBtn.disabled = false;
submitText.innerText = 'Send Reply';
submitSpinner.classList.add('hidden');
});
});
}
});
</script>
<style>
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in-up {
animation: fadeInUp 0.3s ease-out forwards;
}
</style>
@endpush