chore: cleanup project structure and update readme for beta release

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

View File

@@ -1,6 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap') layer(base);
@import 'prismjs/themes/prism.min.css';
@import 'tailwindcss';
@plugin '@tailwindcss/typography';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';

View File

@@ -2,3 +2,11 @@ import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allow your team to quickly build robust real-time web applications.
*/
import './echo';

14
resources/js/echo.js Normal file
View File

@@ -0,0 +1,14 @@
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});

View File

@@ -1,21 +1,48 @@
{{-- Notification Dropdown Component --}}
<div class="relative" x-data="{
dropdownOpen: false,
notifying: true,
notifying: false,
notifications: [],
init() {
this.fetchNotifications();
// Listen for realtime notifications
if (window.Echo) {
window.Echo.private('App.Models.User.{{ auth()->id() }}')
.notification((notification) => {
this.notifications.unshift({
id: notification.id,
data: {
title: notification.data?.title || notification.title || 'Notification',
body: notification.data?.body || notification.body || '',
icon: notification.data?.icon || notification.icon,
action_url: notification.data?.action_url || notification.action_url
},
created_at_human: 'Just now',
read_url: notification.read_url || '#'
});
this.notifying = true;
// Dispatch Global Event for Toast Alerts
window.dispatchEvent(new CustomEvent('reverb-notification', {
detail: notification.data?.title ? notification.data : notification
}));
});
}
},
fetchNotifications() {
fetch('{{ route('notifications.unread') }}')
.then(response => response.json())
.then(data => {
this.notifications = data.notifications;
this.notifying = data.count > 0;
});
},
toggleDropdown() {
this.dropdownOpen = !this.dropdownOpen;
this.notifying = false;
},
closeDropdown() {
this.dropdownOpen = false;
},
handleItemClick() {
console.log('Notification item clicked');
this.closeDropdown();
},
handleViewAllClick() {
console.log('View All Notifications clicked');
this.closeDropdown();
}
}" @click.away="closeDropdown()">
<!-- Notification Button -->
@@ -28,6 +55,7 @@
<span
x-show="notifying"
class="absolute right-0 top-0.5 z-1 h-2 w-2 rounded-full bg-orange-400"
style="display: none;"
>
<span
class="absolute inline-flex w-full h-full bg-orange-400 rounded-full opacity-75 -z-1 animate-ping"
@@ -61,7 +89,7 @@
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100"
x-transition:leave-end="transform opacity-0 scale-95"
class="absolute -right-[240px] mt-[17px] flex h-[480px] w-[350px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark sm:w-[361px] lg:right-0"
class="absolute -right-[240px] mt-[17px] flex h-auto max-h-[480px] w-[350px] flex-col rounded-2xl border border-gray-200 bg-white p-3 shadow-theme-lg dark:border-gray-800 dark:bg-gray-dark sm:w-[361px] lg:right-0"
style="display: none;"
>
<!-- Dropdown Header -->
@@ -80,7 +108,7 @@
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
d="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51356 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
fill=""
/>
</svg>
@@ -88,135 +116,55 @@
</div>
<!-- Notification List -->
<ul class="flex flex-col h-auto overflow-y-auto custom-scrollbar">
@php
$notifications = [
[
'id' => 1,
'userName' => 'Terry Franci',
'userImage' => '/images/user/user-02.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Nganter App',
'type' => 'Project',
'time' => '5 min ago',
'status' => 'online',
],
[
'id' => 2,
'userName' => 'Alex Johnson',
'userImage' => '/images/user/user-03.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Nganter App',
'type' => 'Project',
'time' => '10 min ago',
'status' => 'offline',
],
[
'id' => 3,
'userName' => 'Sarah Williams',
'userImage' => '/images/user/user-04.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Dashboard UI',
'type' => 'Project',
'time' => '15 min ago',
'status' => 'online',
],
[
'id' => 4,
'userName' => 'Mike Brown',
'userImage' => '/images/user/user-05.jpg',
'action' => 'requests permission to change',
'project' => 'Project - E-commerce',
'type' => 'Project',
'time' => '20 min ago',
'status' => 'online',
],
[
'id' => 5,
'userName' => 'Emma Davis',
'userImage' => '/images/user/user-06.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Mobile App',
'type' => 'Project',
'time' => '25 min ago',
'status' => 'offline',
],
[
'id' => 6,
'userName' => 'John Smith',
'userImage' => '/images/user/user-07.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Landing Page',
'type' => 'Project',
'time' => '30 min ago',
'status' => 'online',
],
[
'id' => 7,
'userName' => 'Lisa Anderson',
'userImage' => '/images/user/user-08.jpg',
'action' => 'requests permission to change',
'project' => 'Project - Blog System',
'type' => 'Project',
'time' => '35 min ago',
'status' => 'online',
],
[
'id' => 8,
'userName' => 'David Wilson',
'userImage' => '/images/user/user-09.jpg',
'action' => 'requests permission to change',
'project' => 'Project - CRM Dashboard',
'type' => 'Project',
'time' => '40 min ago',
'status' => 'online',
],
];
@endphp
@foreach ($notifications as $notification)
<li @click="handleItemClick()">
<ul class="flex flex-col h-auto overflow-y-auto custom-scrollbar max-h-[300px]">
<template x-for="notification in notifications" :key="notification.id">
<li>
<a
class="flex gap-3 rounded-lg border-b border-gray-100 p-3 px-4.5 py-3 hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-white/5"
href="#"
:href="notification.read_url"
>
<span class="relative block w-full h-10 rounded-full z-1 max-w-10">
<img src="{{ $notification['userImage'] }}" alt="User" class="overflow-hidden rounded-full" />
<span
class="absolute bottom-0 right-0 z-10 h-2.5 w-full max-w-2.5 rounded-full border-[1.5px] border-white dark:border-gray-900 {{ $notification['status'] === 'online' ? 'bg-success-500' : 'bg-error-500' }}"
></span>
</span>
<div class="flex-shrink-0">
<div class="w-10 h-10 rounded-full bg-brand-100 text-brand-500 flex items-center justify-center">
<template x-if="notification.data.icon">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
</template>
<template x-if="!notification.data.icon">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
</template>
</div>
</div>
<span class="block">
<span class="mb-1.5 block text-theme-sm text-gray-500 dark:text-gray-400">
<span class="font-medium text-gray-800 dark:text-white/90">
{{ $notification['userName'] }}
</span>
{{ $notification['action'] }}
<span class="font-medium text-gray-800 dark:text-white/90">
{{ $notification['project'] }}
</span>
<span class="font-medium text-gray-800 dark:text-white/90" x-text="notification.data.title"></span>
<span class="block text-xs mt-1" x-text="notification.data.body"></span>
</span>
<span class="flex items-center gap-2 text-gray-500 text-theme-xs dark:text-gray-400">
<span>{{ $notification['type'] }}</span>
<span class="w-1 h-1 bg-gray-400 rounded-full"></span>
<span>{{ $notification['time'] }}</span>
<span x-text="notification.created_at_human"></span>
</span>
</span>
</a>
</li>
@endforeach
</template>
<li x-show="notifications.length === 0" class="p-6 text-center text-gray-500 dark:text-gray-400">
<p>No new notifications.</p>
</li>
</ul>
<!-- View All Button -->
<a
href="#"
class="mt-3 flex justify-center rounded-lg border border-gray-300 bg-white p-3 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
@click.prevent="handleViewAllClick()"
>
View All Notification
</a>
<div x-show="notifications.length > 0">
<!-- Mark All Read Button -->
<form action="{{ route('notifications.readAll') }}" method="POST" class="mt-3">
@csrf
<button
type="submit"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white p-3 text-theme-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
>
Mark All as Read
</button>
</form>
</div>
</div>
<!-- Dropdown End -->
</div>

View File

@@ -0,0 +1,12 @@
<footer class="py-12 border-t border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-900">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="flex flex-wrap justify-center gap-6 mb-6 text-gray-500 dark:text-gray-400 text-sm font-medium">
<a href="{{ route('contact') }}" class="hover:text-brand-500 transition-colors">Contact</a>
<a href="{{ route('legal.show', 'terms-and-conditions') }}" class="hover:text-brand-500 transition-colors">Terms and Conditions</a>
<a href="{{ route('legal.show', 'privacy-policy') }}" class="hover:text-brand-500 transition-colors">Privacy Policy</a>
</div>
<p class="text-gray-500 dark:text-gray-400 text-sm font-medium">
&copy; {{ date('Y') }} {{ config('app.name') }}. Built for security and performance.
</p>
</div>
</footer>

View File

@@ -0,0 +1,127 @@
<nav class="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-lg border-b border-gray-100 dark:border-gray-800">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-20">
<!-- Logo -->
<a href="/" class="flex items-center gap-3">
<div class="p-2 bg-brand-500 rounded-xl shadow-lg shadow-brand-500/20">
<svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<span class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-500 dark:from-white dark:to-gray-400">
{{ config('app.name') }}
</span>
</a>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center gap-8 text-sm font-medium">
<a href="{{ route('home') }}" class="text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors">Home</a>
<a href="{{ route('home') }}#features" @if(Route::is('home')) @click.prevent="window.appSmoothScroll('#features')" @endif class="text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors">Features</a>
<!-- Tools Dropdown -->
<div class="relative" x-data="{ open: false }" @click.away="open = false">
<button @click="open = !open" class="flex items-center gap-1 text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors">
Tools
<svg class="w-4 h-4 transition-transform duration-200" :class="{ 'rotate-180': open }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-1"
style="display: none;"
class="absolute left-0 mt-3 w-64 rounded-2xl bg-white dark:bg-gray-800 shadow-xl border border-gray-100 dark:border-gray-700 py-2 z-50 overflow-hidden">
<a href="{{ route('tools.chat-id-finder') }}" class="flex items-center gap-3 px-4 py-3 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:text-brand-500 transition-all">
<div class="w-8 h-8 bg-brand-50 dark:bg-brand-500/10 rounded-lg flex items-center justify-center text-brand-500">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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" /></svg>
</div>
<div>
<div class="font-bold">Chat ID Finder</div>
<div class="text-[10px] text-gray-400">Find your Telegram ID</div>
</div>
</a>
<a href="{{ route('tools.app-key-generator') }}" class="flex items-center gap-3 px-4 py-3 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700/50 hover:text-brand-500 transition-all">
<div class="w-8 h-8 bg-blue-50 dark:bg-blue-500/10 rounded-lg flex items-center justify-center text-blue-500">
<svg class="w-5 h-5" 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 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" /></svg>
</div>
<div>
<div class="font-bold">App Key Generator</div>
<div class="text-[10px] text-gray-400">Secure Laravel keys</div>
</div>
</a>
</div>
</div>
<a href="{{ route('contact') }}" class="text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors">Contact</a>
<a href="{{ route('signin') }}" class="text-gray-600 dark:text-gray-400 hover:text-brand-500 dark:hover:text-brand-400 transition-colors">Sign In</a>
<!-- Theme Toggle -->
<button @click.prevent="$store.theme.toggle()" class="p-2 ml-2 rounded-xl bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
<svg x-show="$store.theme.theme === 'light'" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<svg x-show="$store.theme.theme === 'dark'" style="display: none;" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 9H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</button>
<a href="{{ route('signup') }}" class="px-5 py-2.5 bg-brand-500 hover:bg-brand-600 text-white rounded-xl font-semibold shadow-lg shadow-brand-500/25 transition-all hover:scale-105">
Get Started
</a>
</div>
<!-- Mobile Header Actions -->
<div class="md:hidden flex items-center gap-2">
<button @click.prevent="$store.theme.toggle()" class="p-2 rounded-xl bg-gray-50 dark:bg-gray-800 text-gray-500 dark:text-gray-400">
<svg x-show="$store.theme.theme === 'light'" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
</svg>
<svg x-show="$store.theme.theme === 'dark'" style="display: none;" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 9H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
</button>
<button @click.prevent="$store.sidebar.toggleMobileOpen()" class="p-2 text-gray-600 dark:text-gray-400">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16m-7 6h7" />
</svg>
</button>
</div>
</div>
</div>
</nav>
<!-- Mobile Navigation Overlay -->
<div x-show="$store.sidebar.isMobileOpen"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 -translate-y-full"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-full"
style="display: none;"
class="fixed inset-0 z-40 md:hidden bg-white dark:bg-gray-900 pt-24 px-6">
<div class="flex flex-col gap-6">
<a href="{{ route('home') }}" @click="$store.sidebar.setMobileOpen(false)" class="text-lg font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">Home</a>
<a href="{{ route('home') }}#features" @click="$store.sidebar.setMobileOpen(false); if(Route::is('home')) window.appSmoothScroll('#features')" class="text-lg font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">Features</a>
<div x-data="{ expanded: false }">
<button @click="expanded = !expanded" class="w-full flex justify-between items-center text-lg font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">
Tools
<svg class="w-5 h-5 transition-transform" :class="{ 'rotate-180': expanded }" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path d="M19 9l-7 7-7-7" /></svg>
</button>
<div x-show="expanded" class="pl-4 pt-4 space-y-4" style="display: none;">
<a href="{{ route('tools.chat-id-finder') }}" class="block text-gray-600 dark:text-gray-400 font-medium">Chat ID Finder</a>
<a href="{{ route('tools.app-key-generator') }}" class="block text-gray-600 dark:text-gray-400 font-medium">App Key Generator</a>
</div>
</div>
<a href="{{ route('contact') }}" class="text-lg font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">Contact</a>
<a href="{{ route('signin') }}" class="text-lg font-bold text-gray-900 dark:text-white border-b border-gray-100 dark:border-gray-800 pb-4">Sign In</a>
<a href="{{ route('signup') }}" class="mt-6 w-full py-4 bg-brand-500 text-white rounded-2xl font-bold text-center shadow-xl shadow-brand-500/20">Get Started</a>
</div>
</div>

View File

@@ -0,0 +1,226 @@
<div x-data="{
isOpen: false,
search: '',
results: {},
selectedIndex: -1,
isLoading: false,
init() {
// Global Hotkey
window.addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
this.toggle();
}
if (e.key === 'Escape' && this.isOpen) {
this.toggle();
}
});
// Listen for internal trigger
window.addEventListener('open-global-search', () => this.open());
},
open() {
this.isOpen = true;
this.search = '';
this.results = {};
this.selectedIndex = -1;
this.$nextTick(() => {
$refs.searchInput.focus();
this.fetchResults(); // Fetch initial navigation
});
document.body.classList.add('overflow-hidden');
},
close() {
this.isOpen = false;
document.body.classList.remove('overflow-hidden');
},
toggle() {
this.isOpen ? this.close() : this.open();
},
async fetchResults() {
// We still fetch if search.length < 2 to get the default navigation
this.isLoading = true;
try {
const query = this.search.length >= 2 ? encodeURIComponent(this.search) : '';
const response = await fetch(`/search/global?q=${query}`);
this.results = await response.json();
this.selectedIndex = -1;
} catch (error) {
console.error('Search failed:', error);
} finally {
this.isLoading = false;
}
},
get flatResults() {
const flat = [];
Object.keys(this.results).forEach(group => {
this.results[group].forEach(item => {
flat.push({ ...item, group });
});
});
return flat;
},
navigate(direction) {
const total = this.flatResults.length;
if (total === 0) return;
if (direction === 'down') {
this.selectedIndex = (this.selectedIndex + 1) % total;
} else {
this.selectedIndex = (this.selectedIndex - 1 + total) % total;
}
// Scroll select into view if needed
this.$nextTick(() => {
const el = document.getElementById(`search-result-${this.selectedIndex}`);
if (el) el.scrollIntoView({ block: 'nearest' });
});
},
select() {
const item = this.flatResults[this.selectedIndex];
if (item) {
window.location.href = item.url;
}
}
}" @keydown.window.escape="close()" x-show="isOpen"
class="fixed inset-0 z-[99999] flex items-start justify-center pt-20 sm:pt-32"
style="display: none;" x-cloak>
<!-- Backdrop (Click to close) -->
<div x-show="isOpen"
@click="close()"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0"
class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm shadow-inner"></div>
<!-- Modal Content -->
<div x-show="isOpen"
@click.away="close()"
x-transition:enter="ease-out duration-300" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95"
class="relative w-full max-w-2xl px-4 mx-auto">
<div class="overflow-hidden bg-white rounded-2xl shadow-2xl dark:bg-gray-900 border border-gray-200 dark:border-gray-800 ring-1 ring-black/5">
<!-- Search Input -->
<div class="relative flex items-center px-6 py-5 border-b border-gray-100 dark:border-gray-800">
<svg class="w-6 h-6 text-gray-400 dark:text-gray-500 mr-4" 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" />
</svg>
<input x-ref="searchInput"
x-model="search"
@input.debounce.300ms="fetchResults()"
@keydown.down.prevent="navigate('down')"
@keydown.up.prevent="navigate('up')"
@keydown.enter.prevent="select()"
type="text"
class="w-full py-2 text-xl text-gray-800 bg-transparent border-none focus:ring-0 dark:text-gray-200 placeholder:text-gray-400"
placeholder="Search certificates, tickets, or try 'Settings'...">
<!-- Loading Indicator -->
<div x-show="isLoading" class="absolute right-20 top-1/2 -translate-y-1/2">
<svg class="w-5 h-5 animate-spin text-brand-500" 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>
</div>
<!-- ESC Button UI -->
<div @click="close()"
class="cursor-pointer ml-4 hidden sm:flex items-center gap-1 px-2.5 py-1.5 text-xs font-bold text-gray-400 bg-gray-100 hover:bg-gray-200 rounded-lg dark:bg-white/5 dark:text-gray-500 dark:hover:bg-white/10 border border-gray-200 dark:border-gray-800 transition-all active:scale-95">
<span class="text-[10px]">ESC</span>
</div>
</div>
<!-- Results Area -->
<div class="max-h-[60vh] overflow-y-auto p-2 scrollbar-thin scrollbar-thumb-gray-200 dark:scrollbar-thumb-gray-800">
<template x-if="Object.keys(results).length === 0 && !isLoading">
<div class="px-4 py-12 text-center text-gray-500">
<template x-if="search.length < 2">
<p class="text-sm">Type to search for specific items...</p>
</template>
<template x-if="search.length >= 2">
<p class="text-sm">No results found for "<span class="font-medium text-gray-800 dark:text-gray-200" x-text="search"></span>"</p>
</template>
<div class="mt-6 flex flex-wrap justify-center gap-2 opacity-50">
<span class="px-2 py-1 text-[11px] bg-gray-50 dark:bg-white/5 rounded border border-gray-100 dark:border-gray-800"> K to toggle</span>
<span class="px-2 py-1 text-[11px] bg-gray-50 dark:bg-white/5 rounded border border-gray-100 dark:border-gray-800">↑↓ to navigate</span>
<span class="px-2 py-1 text-[11px] bg-gray-50 dark:bg-white/5 rounded border border-gray-100 dark:border-gray-800"> to select</span>
</div>
</div>
</template>
<div class="space-y-4">
<template x-for="(items, group) in results" :key="group">
<div>
<h3 class="px-3 py-2 text-xs font-semibold tracking-wider text-gray-400 uppercase dark:text-gray-500" x-text="group"></h3>
<div class="space-y-1">
<template x-for="(item, index) in items" :key="item.url">
@php $flatIndex = 'flatResults.findIndex(f => f.url === item.url)'; @endphp
<a :id="'search-result-' + flatResults.findIndex(f => f.url === item.url)"
:href="item.url"
@mouseenter="selectedIndex = flatResults.findIndex(f => f.url === item.url)"
class="flex items-center gap-3 px-3 py-3 text-sm transition-colors rounded-xl group"
:class="selectedIndex === flatResults.findIndex(f => f.url === item.url) ? 'bg-brand-50 text-brand-700 dark:bg-brand-500/10 dark:text-brand-400' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-white/5'">
<!-- Icon Wrapper -->
<div class="flex items-center justify-center w-9 h-9 min-w-9 rounded-lg bg-gray-100 group-hover:bg-white dark:bg-gray-800 dark:group-hover:bg-gray-700 transition-colors"
:class="selectedIndex === flatResults.findIndex(f => f.url === item.url) ? 'bg-white shadow-sm dark:bg-gray-700' : ''">
<template x-if="item.icon === 'certificate'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A11.955 11.955 0 0121.056 12a11.955 11.955 0 01-2.944 5.96z" /></svg>
</template>
<template x-if="item.icon === 'user'">
<svg class="w-5 h-5 flex-shrink-0" 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" /></svg>
</template>
<template x-if="item.icon === 'ticket'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 012-2h10a2 2 0 012 2v14a2 2 0 01-2 2H7a2 2 0 01-2-2V5z" /></svg>
</template>
<template x-if="item.icon === 'home'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
</template>
<template x-if="item.icon === 'key'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 11-7.743-5.743L11 7.001M11 7H9v2H7v2H4v3l2 2h3.5" /></svg>
</template>
<template x-if="item.icon === 'shield'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A11.955 11.955 0 0121.056 12a11.955 11.955 0 01-2.944 5.96z" /></svg>
</template>
<template x-if="item.icon === 'users'">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
</template>
<template x-if="!['certificate', 'user', 'ticket', 'home', 'shield', 'users', 'key'].includes(item.icon)">
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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>
</template>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="font-medium truncate" x-text="item.label"></p>
<span x-show="selectedIndex === flatResults.findIndex(f => f.url === item.url)" class="text-[10px] text-brand-500 font-bold">ENTER </span>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 truncate" x-text="item.sublabel || item.url"></p>
</div>
</a>
</template>
</div>
</div>
</template>
</div>
</div>
<!-- Footer -->
<div class="px-4 py-3 bg-gray-50 dark:bg-white/[0.02] border-t border-gray-100 dark:border-gray-800 flex items-center justify-between text-[11px] text-gray-400">
<div class="flex items-center gap-4">
<span class="flex items-center gap-1"><kbd class="px-1.5 py-0.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded shadow-sm font-sans">↑↓</kbd> Navigate</span>
<span class="flex items-center gap-1"><kbd class="px-1.5 py-0.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded shadow-sm font-sans"></kbd> Select</span>
<span class="flex items-center gap-1"><kbd class="px-1.5 py-0.5 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded shadow-sm font-sans">ESC</kbd> Close</span>
</div>
<div class="font-medium">Command Palette</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,103 @@
<div
x-data="{
toasts: [],
add(data) {
const id = Date.now();
this.toasts.push({
id: id,
title: data.title || 'Notification',
body: data.body || '',
type: data.type || 'info',
icon: data.icon || 'notification',
progress: 100
});
// Handle progress bar animation
const duration = 6000;
const interval = 50;
const step = (interval / duration) * 100;
const timer = setInterval(() => {
const toast = this.toasts.find(t => t.id === id);
if (toast) {
toast.progress -= step;
} else {
clearInterval(timer);
}
}, interval);
setTimeout(() => {
this.remove(id);
clearInterval(timer);
}, duration);
},
remove(id) {
this.toasts = this.toasts.filter(t => t.id !== id);
}
}"
@reverb-notification.window="add($event.detail)"
class="fixed top-24 right-5 sm:right-10 z-[1000] flex flex-col gap-4 w-auto pointer-events-none"
>
<template x-for="toast in toasts" :key="toast.id">
<div
x-show="true"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 translate-y-[-20px] translate-x-12 scale-90"
x-transition:enter-end="opacity-100 translate-y-0 translate-x-0 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 translate-x-0"
x-transition:leave-end="opacity-0 translate-x-20"
class="pointer-events-auto relative overflow-hidden rounded-2xl border border-white/20 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur-3xl shadow-[0_25px_50px_-12px_rgba(0,0,0,0.5)] p-5 flex items-start gap-4 transition-all hover:scale-[1.02] w-80 sm:w-96"
>
<!-- Background Accent Glow -->
<div class="absolute -left-10 -top-10 w-24 h-24 blur-3xl opacity-20 pointer-none"
:class="{
'bg-blue-500': toast.type === 'info',
'text-green-500': toast.type === 'success',
'text-yellow-500': toast.type === 'warning',
'text-red-500': toast.type === 'error'
}"></div>
<!-- Icon Ring -->
<div class="flex-shrink-0 w-12 h-12 rounded-xl flex items-center justify-center shadow-inner"
:class="{
'bg-blue-500/10 text-blue-600 dark:text-blue-400': toast.type === 'info',
'bg-green-500/10 text-green-600 dark:text-green-400': toast.type === 'success',
'bg-yellow-500/10 text-yellow-600 dark:text-yellow-400': toast.type === 'warning',
'bg-red-500/10 text-red-600 dark:text-red-400': toast.type === 'error'
}">
<template x-if="toast.icon === 'ticket'">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4v-3a2 2 0 002-2V7a2 2 0 00-2-2H5z"></path></svg>
</template>
<template x-if="toast.icon === 'chat'">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"></path></svg>
</template>
<template x-if="toast.icon !== 'ticket' && toast.icon !== 'chat'">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><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"></path></svg>
</template>
</div>
<!-- Content -->
<div class="flex-1 min-w-0 pr-4">
<h4 class="text-base font-bold text-gray-900 dark:text-white leading-tight" x-text="toast.title"></h4>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1 lines-2 font-medium" x-text="toast.body"></p>
</div>
<!-- Close Button -->
<button @click="remove(toast.id)" class="absolute top-4 right-4 text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors">
<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="M6 18L18 6M6 6l12 12"></path></svg>
</button>
<!-- Progress Bar -->
<div class="absolute bottom-0 left-0 h-1 transition-all ease-linear"
:style="`width: ${toast.progress}%`"
:class="{
'bg-blue-500': toast.type === 'info',
'bg-green-500': toast.type === 'success',
'bg-yellow-500': toast.type === 'warning',
'bg-red-500': toast.type === 'error'
}"></div>
</div>
</template>
</div>

View File

@@ -0,0 +1,14 @@
<x-mail::message>
# Hello,
Thank you for reaching out to **DyDev TrustLab**.
<x-mail::panel>
{{ $replyMessage }}
</x-mail::panel>
If you have any further questions, feel free to respond to this email or visit our [Support Portal]({{ config('app.url') }}).
Best regards,<br>
**{{ config('app.name') }} Support Team**
</x-mail::message>

View File

@@ -22,7 +22,7 @@
<a href="/"
class="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
Back to Home Page
Back to home
</a>
</div>
<!-- Footer -->

View File

@@ -22,7 +22,7 @@
<a href="/"
class="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
Back to Home Page
Back to home
</a>
</div>
<!-- Footer -->

View File

@@ -20,7 +20,7 @@
<a href="/"
class="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
Back to Home Page
Back to home
</a>
</div>
<p class="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">

View File

@@ -0,0 +1,217 @@
@extends('layouts.fullscreen-layout', ['title' => 'Secure Certificate & API Management'])
@section('meta_description', 'Manage Root CA, Intermediate CAs, and API keys through a powerful developer portal. Fast, secure, and ready for production.')
@section('meta_keywords', 'ssl certificate, tls issuance, api management, ca authority, security dashboard')
@section('content')
<script>
// Define global scroll function immediately
window.appSmoothScroll = function(selector) {
console.log('[App] Attempting to scroll to:', selector);
const element = document.querySelector(selector);
if (!element) {
console.error('[App] Scroll target not found:', selector);
return;
}
const navbarOffset = 80;
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - navbarOffset;
console.log('[App] Calculated offset:', offsetPosition);
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
// Clean URL after a short delay
setTimeout(() => {
if (window.location.hash) {
history.replaceState(null, null, window.location.pathname);
console.log('[App] URL cleaned');
}
}, 500);
};
// Handle initial hash
window.addEventListener('DOMContentLoaded', () => {
if (window.location.hash) {
console.log('[App] Initial hash detected:', window.location.hash);
setTimeout(() => window.appSmoothScroll(window.location.hash), 500);
}
});
</script>
<div class="relative min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300 overflow-hidden">
<!-- 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 top-1/2 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>
<div class="absolute bottom-0 left-0 -translate-x-1/4 translate-y-1/4 w-[400px] h-[400px] bg-brand-500/10 rounded-full blur-[100px] pointer-events-none"></div>
<!-- Navbar -->
<x-public.navbar />
<!-- Hero Section -->
<header class="relative pt-32 pb-20 overflow-hidden" id="home">
<!-- Background Shapes -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-7xl h-full -z-10 opacity-30 dark:opacity-20">
<div class="absolute top-20 left-10 w-72 h-72 bg-brand-500 rounded-full blur-[120px]"></div>
<div class="absolute bottom-10 right-10 w-96 h-96 bg-blue-500 rounded-full blur-[150px]"></div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="inline-flex items-center gap-2 px-4 py-2 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-xs font-bold uppercase tracking-widest mb-8 animate-bounce">
🚀 Unified Certificate Management
</div>
<h1 class="text-5xl md:text-7xl font-extrabold text-gray-900 dark:text-white mb-6 leading-tight">
Secure Your Assets with <br/>
<span class="text-transparent bg-clip-text bg-gradient-to-r from-brand-500 to-blue-600">
Trusted Certificate Authority
</span>
</h1>
<p class="text-lg md:text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10">
Issue, manage, and track SSL/TLS certificates and API keys through a powerful, developer-friendly management system.
</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="{{ route('signup') }}" class="w-full sm:w-auto px-8 py-4 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-2xl font-bold shadow-xl transition-all hover:-translate-y-1 hover:shadow-2xl">
Create Global Account
</a>
<a href="#features" @click.prevent="window.appSmoothScroll('#features')" class="w-full sm:w-auto px-8 py-4 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 border border-gray-200 dark:border-gray-700 rounded-2xl font-bold transition-all hover:bg-gray-50 dark:hover:bg-gray-700">
Explore Features
</a>
</div>
<!-- Preview/Abstract UI -->
<div class="mt-20 relative mx-auto max-w-5xl">
<div class="aspect-video bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 shadow-2xl p-4 overflow-hidden group">
<div class="flex items-center gap-2 mb-4 border-b border-gray-100 dark:border-gray-700 pb-3">
<div class="flex gap-1.5">
<div class="w-3 h-3 rounded-full bg-red-400"></div>
<div class="w-3 h-3 rounded-full bg-yellow-400"></div>
<div class="w-3 h-3 rounded-full bg-green-400"></div>
</div>
<div class="flex-1 ml-4 h-6 bg-gray-100 dark:bg-gray-900/50 rounded-lg max-w-xs"></div>
</div>
<!-- Mock Dashboard Content -->
<div class="grid grid-cols-3 gap-4">
<div class="col-span-2 space-y-4">
<div class="h-40 bg-brand-500/5 rounded-2xl border border-brand-500/10"></div>
<div class="grid grid-cols-2 gap-4">
<div class="h-24 bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
<div class="h-24 bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
</div>
</div>
<div class="space-y-4">
<div class="h-full bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
</div>
</div>
<!-- Overlay Gradient -->
<div class="absolute inset-0 bg-gradient-to-t from-white dark:from-gray-900 via-transparent to-transparent pointer-events-none"></div>
</div>
</div>
</div>
</header>
<!-- Features Section -->
<section id="features" class="py-24 bg-gray-50 dark:bg-gray-900/50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">Powerful Features for Modern Apps</h2>
<p class="text-gray-600 dark:text-gray-400">Everything you need to manage your security layer efficiently.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Feature 1 -->
<div class="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
<div class="w-14 h-14 bg-brand-50 dark:bg-brand-500/10 rounded-2xl flex items-center justify-center text-brand-500 mb-6 group-hover:scale-110 transition-transform">
<svg class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">Custom CA Issuance</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
Issue professional Root and Intermediate CA certificates with a single click. Fully compliant with standard encryption protocols.
</p>
</div>
<!-- Feature 2 -->
<div class="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
<div class="w-14 h-14 bg-blue-50 dark:bg-blue-500/10 rounded-2xl flex items-center justify-center text-blue-500 mb-6 group-hover:scale-110 transition-transform">
<svg class="h-7 w-7" 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>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">API Management</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
Secure your external services with granular API keys. Track usage patterns and revoke access instantly when needed.
</p>
</div>
<!-- Feature 3 -->
<div class="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
<div class="w-14 h-14 bg-green-50 dark:bg-green-500/10 rounded-2xl flex items-center justify-center text-green-500 mb-6 group-hover:scale-110 transition-transform">
<svg class="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-3">Real-time Tracking</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
Monitor issuance trends and expiring certificates through intuitive analytical dashboards and automated alerts.
</p>
</div>
</div>
</div>
</section>
<!-- CTA Section -->
<section class="py-20">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-brand-600 rounded-[3rem] p-12 md:p-16 text-center text-white relative overflow-hidden shadow-2xl">
<div class="relative z-10">
<h2 class="text-3xl md:text-4xl font-bold mb-6">Ready to secure your application?</h2>
<p class="text-brand-100 mb-10 max-w-lg mx-auto">Join hundreds of developers managing their security infrastructure with {{ config('app.name') }}.</p>
<div class="flex flex-col sm:flex-row items-center justify-center gap-4">
<a href="{{ route('signup') }}" class="px-8 py-4 bg-white text-brand-600 rounded-2xl font-bold hover:scale-105 transition-transform">
Create Free Account
</a>
<a href="{{ route('signin') }}" class="px-8 py-4 bg-brand-700 text-white rounded-2xl font-bold hover:bg-brand-800 transition-colors">
Sign In to Portal
</a>
</div>
</div>
<!-- Abstract Design -->
<div class="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
<x-common.common-grid-shape/>
</div>
</div>
</div>
</section>
<!-- Footer -->
<x-public.footer />
<!-- Back to Top Button -->
<button
x-data="{ show: false }"
x-on:scroll.window="show = window.pageYOffset > 500"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-10"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-10"
@click="window.appSmoothScroll('#home')"
class="fixed bottom-8 right-8 z-50 p-4 bg-brand-500 hover:bg-brand-600 text-white rounded-2xl shadow-2xl shadow-brand-500/40 transition-all hover:-translate-y-1 active:scale-95"
aria-label="Back to top"
>
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
</div>
@endsection

View File

@@ -67,26 +67,24 @@
<!-- Search Bar (desktop only) -->
<div class="hidden xl:block">
<form>
<div class="relative">
<span class="absolute -translate-y-1/2 pointer-events-none left-4 top-1/2">
<!-- Search Icon -->
<svg class="fill-gray-500 dark:fill-gray-400" width="20" height="20"
viewBox="0 0 20 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3.04175 9.37363C3.04175 5.87693 5.87711 3.04199 9.37508 3.04199C12.8731 3.04199 15.7084 5.87693 15.7084 9.37363C15.7084 12.8703 12.8731 15.7053 9.37508 15.7053C5.87711 15.7053 3.04175 12.8703 3.04175 9.37363ZM9.37508 1.54199C5.04902 1.54199 1.54175 5.04817 1.54175 9.37363C1.54175 13.6991 5.04902 17.2053 9.37508 17.2053C11.2674 17.2053 13.003 16.5344 14.357 15.4176L17.177 18.238C17.4699 18.5309 17.9448 18.5309 18.2377 18.238C18.5306 17.9451 18.5306 17.4703 18.2377 17.1774L15.418 14.3573C16.5365 13.0033 17.2084 11.2669 17.2084 9.37363C17.2084 5.04817 13.7011 1.54199 9.37508 1.54199Z"
fill="" />
</svg>
</span>
<input type="text" placeholder="Search or type command..."
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-white/3 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]" />
<button
class="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
<span> </span>
<span> K </span>
</button>
</div>
</form>
<div class="relative cursor-pointer" @click="$dispatch('open-global-search')">
<span class="absolute -translate-y-1/2 pointer-events-none left-4 top-1/2">
<!-- Search Icon -->
<svg class="fill-gray-500 dark:fill-gray-400" width="20" height="20"
viewBox="0 0 20 20" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M3.04175 9.37363C3.04175 5.87693 5.87711 3.04199 9.37508 3.04199C12.8731 3.04199 15.7084 5.87693 15.7084 9.37363C15.7084 12.8703 12.8731 15.7053 9.37508 15.7053C5.87711 15.7053 3.04175 12.8703 3.04175 9.37363ZM9.37508 1.54199C5.04902 1.54199 1.54175 5.04817 1.54175 9.37363C1.54175 13.6991 5.04902 17.2053 9.37508 17.2053C11.2674 17.2053 13.003 16.5344 14.357 15.4176L17.177 18.238C17.4699 18.5309 17.9448 18.5309 18.2377 18.238C18.5306 17.9451 18.5306 17.4703 18.2377 17.1774L15.418 14.3573C16.5365 13.0033 17.2084 11.2669 17.2084 9.37363C17.2084 5.04817 13.7011 1.54199 9.37508 1.54199Z"
fill="" />
</svg>
</span>
<input type="text" placeholder="Search or type command..." readonly
class="cursor-pointer dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-white/3 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]" />
<button
class="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
<span> </span>
<span> K </span>
</button>
</div>
</div>
</div>

View File

@@ -113,6 +113,9 @@
{{-- Flash Message Component --}}
<x-ui.flash-message />
{{-- Real-time Toast Component --}}
<x-ui.realtime-toast />
{{-- preloader --}}
<x-common.preloader/>
{{-- preloader end --}}
@@ -137,6 +140,7 @@
</div>
<x-ui.global-search />
</body>
@stack('scripts')

View File

@@ -6,7 +6,28 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'Dashboard' }} | {{ config('app.name') }}</title>
<title>{{ $title ?? 'Unified Management' }} | {{ config('app.name', 'DyDev Admin') }}</title>
<!-- SEO Meta Tags -->
<meta name="description" content="@yield('meta_description', 'Professional Certificate Authority and API Management System for modern developers. Issue SSL/TLS certificates and manage API keys with ease.')">
<meta name="keywords" content="@yield('meta_keywords', 'certificate authority, ssl manager, api key management, trustlab, security portal')">
<meta name="robots" content="@yield('robots', 'index, follow')">
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="{{ url()->current() }}">
<meta property="og:title" content="{{ $title ?? 'Unified Management' }} | {{ config('app.name', 'DyDev Admin') }}">
<meta property="og:description" content="@yield('meta_description', 'Professional Certificate Authority and API Management System for modern developers. Issue SSL/TLS certificates and manage API keys with ease.')">
<meta property="og:image" content="{{ asset('images/og-share.png') }}">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="{{ url()->current() }}">
<meta property="twitter:title" content="{{ $title ?? 'Unified Management' }} | {{ config('app.name', 'DyDev Admin') }}">
<meta property="twitter:description" content="@yield('meta_description', 'Professional Certificate Authority and API Management System for modern developers. Issue SSL/TLS certificates and manage API keys with ease.')">
<meta property="twitter:image" content="{{ asset('images/og-share.png') }}">
@yield('meta')
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])

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

View File

@@ -0,0 +1,24 @@
@props([
'url',
'color' => 'primary',
'align' => 'center',
])
<table class="action" align="{{ $align }}" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="{{ $align }}">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="{{ $align }}">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color }}" target="_blank" rel="noopener">{!! $slot !!}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

View File

@@ -0,0 +1,8 @@
@props(['url'])
<tr>
<td class="header">
<a href="{{ $url }}" style="display: inline-block;">
<img src="{{ config('app.url') }}/images/logo/logo.png" class="logo" alt="{{ config('app.name') }}" style="height: 40px; width: auto;">
</a>
</td>
</tr>

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{{ config('app.name') }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="color-scheme" content="light dark" />
<meta name="supported-color-schemes" content="light dark" />
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
{!! $head ?? '' !!}
</head>
<body>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
{!! $header ?? '' !!}
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0" style="border: hidden !important;">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
{!! Illuminate\Mail\Markdown::parse($slot) !!}
{!! $subcopy ?? '' !!}
</td>
</tr>
</table>
</td>
</tr>
{!! $footer ?? '' !!}
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,28 @@
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{!! $slot !!}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{!! $subcopy !!}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} **{{ config('app.name') }}**. [Visit Portal]({{ config('app.url') }}) <br>
{{ __('Secure Certificate Management for Developers.') }}
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@@ -0,0 +1,14 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

View File

@@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

View File

@@ -0,0 +1,220 @@
/* Base */
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
box-sizing: border-box;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
position: relative;
}
body {
-webkit-text-size-adjust: none;
background-color: #f8fafc;
color: #475569;
height: 100%;
line-height: 1.6;
margin: 0;
padding: 0;
width: 100% !important;
}
p,
ul,
ol,
blockquote {
line-height: 1.6;
text-align: left;
}
a {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #1e293b;
font-size: 22px;
font-weight: 800;
margin-top: 0;
margin-bottom: 24px;
text-align: left;
letter-spacing: -0.025em;
}
h2 {
color: #1e293b;
font-size: 18px;
font-weight: 700;
margin-top: 0;
text-align: left;
}
h3 {
color: #1e293b;
font-size: 16px;
font-weight: 700;
margin-top: 0;
text-align: left;
}
p {
font-size: 16px;
line-height: 1.6em;
margin-top: 0;
margin-bottom: 16px;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
background-color: #f8fafc;
margin: 0;
padding: 0;
width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
}
/* Header */
.header {
padding: 40px 0;
text-align: center;
}
.header a {
color: #1e293b;
font-size: 19px;
font-weight: 900;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Logo */
.logo {
height: auto;
max-height: 48px;
width: auto;
}
/* Body */
.body {
background-color: #f8fafc;
margin: 0;
padding: 0;
width: 100%;
}
.inner-body {
background-color: #ffffff;
border-radius: 24px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
margin: 0 auto;
padding: 0;
width: 570px;
}
.content-cell {
padding: 48px;
}
/* Buttons */
.action {
margin: 32px auto;
text-align: center;
}
.button {
border-radius: 12px;
color: #fff !important;
display: inline-block;
font-size: 15px;
font-weight: 700;
padding: 12px 32px;
text-decoration: none;
transition: all 0.2s;
}
.button-primary {
background-color: #3b82f6;
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.3);
}
/* Panels */
.panel {
border-left: #3b82f6 solid 4px;
margin: 24px 0;
border-radius: 0 12px 12px 0;
overflow: hidden;
}
.panel-content {
background-color: #eff6ff;
color: #1e40af;
padding: 20px;
}
/* Footer */
.footer {
padding: 40px 0;
text-align: center;
}
.footer p {
color: #94a3b8;
font-size: 13px;
text-align: center;
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
body, .wrapper, .body {
background-color: #0f172a !important;
color: #94a3b8 !important;
}
.inner-body {
background-color: #1e293b !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5) !important;
}
h1, h2, h3, .header a {
color: #f1f5f9 !important;
}
p {
color: #94a3b8 !important;
}
.panel-content {
background-color: #1e293b !important;
color: #60a5fa !important;
border: 1px solid #334155 !important;
}
}

View File

@@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

View File

@@ -0,0 +1,9 @@
{!! strip_tags($header ?? '') !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer ?? '') !!}

View File

@@ -0,0 +1,27 @@
<x-mail::layout>
{{-- Header --}}
<x-slot:header>
<x-mail::header :url="config('app.url')">
{{ config('app.name') }}
</x-mail::header>
</x-slot:header>
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
<x-slot:subcopy>
<x-mail::subcopy>
{{ $subcopy }}
</x-mail::subcopy>
</x-slot:subcopy>
@endisset
{{-- Footer --}}
<x-slot:footer>
<x-mail::footer>
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
</x-mail::footer>
</x-slot:footer>
</x-mail::layout>

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}

View File

@@ -0,0 +1 @@
{{ $slot }}