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

12
.gitignore vendored
View File

@@ -33,3 +33,15 @@ yarn-error.log*
# Compiled assets
mix-manifest.json
# Local Deployment & Release
/deploy.sh
/zip-release.php
/zip-stage.php
/copy-release.php
/make-release.bat
/auth_backup.php
/.idea
/.vscode
*.bak
*.tmp

View File

@@ -40,8 +40,8 @@ A robust, modern platform for managing Root CAs, Intermediate CAs, and Leaf Cert
#### Option A: Terminal Access
```bash
# Clone and enter
git clone <your-repo-url>
cd app-tail
git clone https://github.com/twinpath/app.git
cd app
# Install dependencies
composer install
@@ -120,5 +120,31 @@ Returns Root and Intermediate CA certificates in JSON format.
`GET /api/v1/certificates`
Retrieves user-specific leaf certificates. Requires `X-API-KEY` header.
## 🔄 CI/CD & Automated Deployment
The project includes an automation script for seamless deployment on aaPanel:
### 1. Script Setup
1. Locate `deploy.sh.example` and rename it to `deploy.sh` on your server.
2. Edit `deploy.sh` and provide your specific paths and Telegram credentials.
3. Make the script executable: `chmod +x deploy.sh`.
### 2. aaPanel Webhook Integration
1. In aaPanel, install the **Webhook** app.
2. Create a new Webhook and paste the following command:
```bash
/bin/bash /www/wwwroot/your-project-path/deploy.sh
```
3. Copy the Webhook URL provided by aaPanel.
### 3. GitHub Integration
1. Go to your GitHub repository **Settings > Webhooks**.
2. Click **Add webhook**.
3. Paste your aaPanel Webhook URL into the **Payload URL**.
4. Set **Content type** to `application/json`.
5. Select **Just the push event** and click **Add webhook**.
Now, every time you push to the `main` branch, aaPanel will automatically pull the latest code, install dependencies, run migrations, and build assets.
## 📦 License
Refer to the [LICENSE](LICENSE) file for details.

32
RELEASE_NOTES.md Normal file
View File

@@ -0,0 +1,32 @@
# Release v1.0.0 - Initial Official Launch
We are excited to announce the first official release of the **Certificate Authority & API Management System**. This version provides a complete foundation for managing internal PKI and secure API access.
## ✨ Key Features
- **CA management**: Full lifecycle support for Root and Intermediate CAs.
- **Certificate Issuance**: Dynamic generation of leaf certificates with customizable SANs.
- **API Key Infrastructure**: Secure key generation, status toggling, and real-time usage tracking.
- **Advanced Dashboard**: Real-time metrics, issuance trends, and server latency monitoring.
- **Developer Experience**: Interactive API documentation with code snippets (cURL, JS, PHP, Python).
- **Hosting Friendly**: Included standalone Web Key Generator and manual database import scripts.
## 🛠️ Included in this Release
- **Source Code**: Full Laravel 12 / Tailwind v4 source.
- **Database Schema**: Pre-configured `install.sql` in the `database/` folder.
- **Key-Gen Tool**: Standalone utility in `public/key-gen.html`.
## 🚀 Installation Overview
For users with terminal access:
```bash
composer install && npm install && npm run build
php artisan migrate --seed
```
For Shared Hosting users:
1. Download the `app-v1.0.0-ready.zip` attachment.
2. Upload and extract to your server.
3. Import `database/install.sql` via phpMyAdmin.
4. Configure `.env` using our provided `key-gen.html`.
---
*Thank you for using DyDev TrustLab solutions!*

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Events;
use App\Models\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DashboardStatsUpdated implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
/**
* Create a new event instance.
*/
public function __construct($userId)
{
$this->userId = $userId;
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('user.' . $this->userId),
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PingResponse implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
public $timestamp;
/**
* Create a new event instance.
*/
public function __construct($userId, $timestamp)
{
$this->userId = $userId;
$this->timestamp = $timestamp;
}
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('user.' . $this->userId),
];
}
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'PingResponse';
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Events;
use App\Models\TicketReply;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketMessageSent implements ShouldBroadcastNow
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $reply;
/**
* Create a new event instance.
*/
public function __construct(TicketReply $reply)
{
$this->reply = $reply->load('user');
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('ticket.' . $this->reply->ticket_id),
];
}
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'ticket.message.sent';
}
/**
* Data to broadcast.
*
* @return array
*/
public function broadcastWith(): array
{
return [
'id' => $this->reply->id,
'message' => $this->reply->message,
'user_id' => $this->reply->user_id,
'user_name' => $this->reply->user->name,
'is_staff' => $this->reply->user->isAdmin(),
'created_at' => $this->reply->created_at->format('M d, Y H:i A'),
'attachment_url' => $this->reply->attachment_path ? \Storage::url($this->reply->attachment_path) : null,
];
}
}

View File

@@ -20,39 +20,73 @@ class MenuHelper
['name' => 'Create Certificate', 'route_name' => 'certificate.create', 'pro' => false],
],
],
[
'name' => 'API Keys',
'icon' => 'api-key',
'route_name' => 'api-keys.index',
],
];
}
public static function getTemplateItems()
{
return [
[
'name' => 'Calendar',
'icon' => 'calendar',
'route_name' => 'calendar',
'route_name' => 'templates.calendar',
],
[
'name' => 'Forms',
'icon' => 'forms',
'subItems' => [
['name' => 'Form Elements', 'route_name' => 'form-elements', 'pro' => false],
['name' => 'Form Elements', 'route_name' => 'templates.form-elements', 'pro' => false],
],
],
[
'name' => 'Tables',
'icon' => 'tables',
'subItems' => [
['name' => 'Basic Tables', 'route_name' => 'basic-tables', 'pro' => false]
['name' => 'Basic Tables', 'route_name' => 'templates.basic-tables', 'pro' => false]
],
],
[
'name' => 'API Keys',
'icon' => 'api-key',
'route_name' => 'api-keys.index',
],
[
'name' => 'Pages',
'icon' => 'pages',
'subItems' => [
['name' => 'Blank Page', 'route_name' => 'blank', 'pro' => false],
['name' => 'Blank Page', 'route_name' => 'templates.blank', 'pro' => false],
['name' => '404 Error', 'route_name' => 'error-404', 'pro' => false],
// ['name' => 'API Keys', 'route_name' => 'api-keys.index', 'pro' => false, 'new' => true]
],
],
[
'name' => 'Charts',
'icon' => 'charts',
'subItems' => [
['name' => 'Line Chart', 'route_name' => 'templates.line-chart', 'pro' => false],
['name' => 'Bar Chart', 'route_name' => 'templates.bar-chart', 'pro' => false]
],
],
[
'name' => 'UI Elements',
'icon' => 'ui-elements',
'subItems' => [
['name' => 'Alerts', 'route_name' => 'templates.alerts', 'pro' => false],
['name' => 'Avatar', 'route_name' => 'templates.avatars', 'pro' => false],
['name' => 'Badge', 'route_name' => 'templates.badges', 'pro' => false],
['name' => 'Buttons', 'route_name' => 'templates.buttons', 'pro' => false],
['name' => 'Images', 'route_name' => 'templates.images', 'pro' => false],
['name' => 'Videos', 'route_name' => 'templates.videos', 'pro' => false],
],
],
[
'name' => 'Authentication',
'icon' => 'authentication',
'subItems' => [
['name' => 'Sign In', 'route_name' => 'signin', 'pro' => false],
['name' => 'Sign Up', 'route_name' => 'signup', 'pro' => false],
],
],
];
}
@@ -72,45 +106,34 @@ class MenuHelper
];
}
public static function getOthersItems()
{
return [
[
'name' => 'Charts',
'icon' => 'charts',
'subItems' => [
['name' => 'Line Chart', 'route_name' => 'line-chart', 'pro' => false],
['name' => 'Bar Chart', 'route_name' => 'bar-chart', 'pro' => false]
],
],
[
'name' => 'UI Elements',
'icon' => 'ui-elements',
'subItems' => [
['name' => 'Alerts', 'route_name' => 'alerts', 'pro' => false],
['name' => 'Avatar', 'route_name' => 'avatars', 'pro' => false],
['name' => 'Badge', 'route_name' => 'badges', 'pro' => false],
['name' => 'Buttons', 'route_name' => 'buttons', 'pro' => false],
['name' => 'Images', 'route_name' => 'images', 'pro' => false],
['name' => 'Videos', 'route_name' => 'videos', 'pro' => false],
],
],
[
'name' => 'Authentication',
'icon' => 'authentication',
'subItems' => [
['name' => 'Sign In', 'route_name' => 'signin', 'pro' => false],
['name' => 'Sign Up', 'route_name' => 'signup', 'pro' => false],
],
],
];
}
public static function getMenuGroups()
{
$groups = [];
// Admin Menu
// Check if we are in the template section
if (request()->is('templates*')) {
$groups[] = [
'title' => 'Back',
'items' => [
[
'name' => 'Back to Dashboard',
'icon' => 'dashboard', // Or an arrow icon if available
'route_name' => 'dashboard',
]
]
];
$groups[] = [
'title' => 'Template Gallery',
'items' => self::getTemplateItems()
];
return $groups;
}
// --- Main Admin/User Sidebar ---
// Admin Management
if (auth()->check() && auth()->user()->isAdmin()) {
$groups[] = [
'title' => 'Admin Management',
@@ -125,19 +148,45 @@ class MenuHelper
'icon' => 'certificate',
'route_name' => 'admin.root-ca.index',
],
[
'name' => 'Ticket Management',
'icon' => 'support-ticket',
'route_name' => 'admin.tickets.index',
],
[
'name' => 'Legal Page Management',
'icon' => 'pages',
'route_name' => 'admin.legal-pages.index',
],
[
'name' => 'Inbox / Messages',
'icon' => 'email',
'route_name' => 'admin.contacts.index',
],
[
'name' => 'SMTP Tester',
'icon' => 'server-settings',
'route_name' => 'admin.smtp-tester.index',
],
]
];
}
// Filter Main Items based on role
// Standard Menus
// Filter Main Items based on role for non-admins (though getMainNavItems is now cleaner)
$mainItems = self::getMainNavItems();
// Add Support Tickets to Main Nav
$mainItems[] = [
'name' => 'Support Tickets',
'icon' => 'support-ticket',
'route_name' => 'support.index',
];
if (!auth()->check() || !auth()->user()->isAdmin()) {
$mainItems = array_values(array_filter($mainItems, function ($item) {
return in_array($item['name'], ['Dashboard', 'Certificate', 'API Keys']);
}));
// For customers, just show what's in mainItems (Dashboard, Certs, Keys, Support)
}
// Standard Menus
$groups[] = [
'title' => 'Menu',
'items' => $mainItems
@@ -148,11 +197,17 @@ class MenuHelper
'items' => self::getMyAccountItems()
];
// Others - Admin Only
// Link to Templates for Admins
if (auth()->check() && auth()->user()->isAdmin()) {
$groups[] = [
'title' => 'Others',
'items' => self::getOthersItems()
'title' => 'Development',
'items' => [
[
'name' => 'View Templates',
'icon' => 'pages',
'route_name' => 'templates.calendar'
]
]
];
}
@@ -216,6 +271,8 @@ class MenuHelper
'api-key' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 12C9.98528 12 12 9.98528 12 7.5C12 5.01472 9.98528 3 7.5 3C5.01472 3 3 5.01472 3 7.5C3 9.98528 5.01472 12 7.5 12Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.5 10.5L21 21L19.5 22.5L16.5 19.5L15 21L13.5 19.5L10.5 16.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
'server-settings' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect><rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>',
];
return $icons[$iconName] ?? '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>';

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\ContactSubmission;
use Illuminate\Http\Request;
use App\Mail\ContactReply;
use Illuminate\Support\Facades\Mail;
class ContactManagementController extends Controller
{
public function index()
{
$submissions = ContactSubmission::latest()->paginate(10);
return view('pages.admin.contacts.index', compact('submissions'));
}
public function show(ContactSubmission $contactSubmission)
{
if (!$contactSubmission->is_read) {
$contactSubmission->update(['is_read' => true]);
}
return view('pages.admin.contacts.show', compact('contactSubmission'));
}
public function reply(Request $request, ContactSubmission $contactSubmission)
{
$validated = $request->validate([
'subject' => 'required|string|max:255',
'message' => 'required|string',
]);
// Send using the support mailer
try {
$email = trim($contactSubmission->email);
\Illuminate\Support\Facades\Log::info('Attempting to send ContactReply', [
'to' => $email,
'subject' => $validated['subject'],
'message_length' => strlen($validated['message']),
'mailer' => 'support'
]);
Mail::mailer('support')
->to($email)
->send(new ContactReply(strip_tags($validated['subject']), strip_tags($validated['message'])));
\Illuminate\Support\Facades\Log::info('ContactReply sent command executed without exception.');
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::error('Failed to send ContactReply', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return back()->with('error', 'Failed to send reply: ' . $e->getMessage());
}
return back()->with('success', 'Reply sent successfully to ' . $contactSubmission->email);
}
public function destroy(ContactSubmission $contactSubmission)
{
$contactSubmission->delete();
return redirect()->route('admin.contacts.index')->with('success', 'Message deleted successfully.');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\LegalPage;
use App\Models\LegalPageRevision;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class LegalManagementController extends Controller
{
public function index()
{
$pages = LegalPage::with(['currentRevision'])->get();
return view('pages.admin.legal.index', compact('pages'));
}
public function edit(LegalPage $legalPage)
{
$legalPage->load('currentRevision');
return view('pages.admin.legal.edit', compact('legalPage'));
}
public function update(Request $request, LegalPage $legalPage)
{
$request->validate([
'content' => 'required',
'version' => 'required_unless:update_existing,true',
'change_log' => 'nullable|string',
]);
if ($request->has('update_existing') && $request->update_existing == 'true') {
$revision = $legalPage->currentRevision;
if ($revision) {
$revision->update([
'content' => $request->content,
'change_log' => $request->change_log ?? $revision->change_log,
'created_by' => Auth::id(),
]);
return redirect()->route('admin.legal-pages.index')->with('success', 'Current version updated successfully (no new revision created).');
}
}
// Deactivate old revisions
LegalPageRevision::where('legal_page_id', $legalPage->id)->update(['is_active' => false]);
// Create new revision
LegalPageRevision::create([
'legal_page_id' => $legalPage->id,
'content' => $request->content,
'version' => $request->version,
'change_log' => $request->change_log,
'is_active' => true,
'created_by' => Auth::id(),
]);
return redirect()->route('admin.legal-pages.index')->with('success', 'Legal page updated successfully with a new version.');
}
}

View File

@@ -10,6 +10,19 @@ use Illuminate\Support\Str;
class RootCaController extends Controller
{
public function setup()
{
if (CaCertificate::count() > 0) {
return redirect()->route('certificate.index')->with('error', 'CA already initialized.');
}
if (app(\App\Services\OpenSslService::class)->setupCa()) {
return redirect()->route('certificate.index')->with('success', 'Root and Intermediate CA successfully initialized.');
}
return redirect()->route('certificate.index')->with('error', 'Failed to initialize CA.');
}
public function index()
{
$certificates = CaCertificate::all()->map(function($cert) {

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Config;
use Exception;
class SmtpTesterController extends Controller
{
public function index()
{
$configs = [
'smtp' => [
'name' => 'System/Default Mailer (smtp)',
'host' => Config::get('mail.mailers.smtp.host'),
'port' => Config::get('mail.mailers.smtp.port'),
'username' => Config::get('mail.mailers.smtp.username'),
'encryption' => Config::get('mail.mailers.smtp.encryption'),
'from' => Config::get('mail.from.address'),
],
'support' => [
'name' => 'Support Mailer (support)',
'host' => Config::get('mail.mailers.support.host'),
'port' => Config::get('mail.mailers.support.port'),
'username' => Config::get('mail.mailers.support.username'),
'encryption' => Config::get('mail.mailers.support.encryption'),
'from' => Config::get('mail.mailers.support.username'), // usually same as username or configured
],
];
return view('pages.admin.smtp-tester.index', compact('configs'));
}
public function send(Request $request)
{
$request->validate([
'mailer' => 'required|in:smtp,support',
'email' => 'required|email',
]);
$mailer = $request->mailer;
$targetEmail = $request->email;
try {
$start = microtime(true);
$fromAddress = config("mail.mailers.{$mailer}.from.address") ?? config('mail.from.address');
$fromName = config("mail.mailers.{$mailer}.from.name") ?? config('mail.from.name');
$mode = $request->input('mode', 'raw');
if ($mode === 'mailable') {
Mail::mailer($mailer)
->to($targetEmail)
->send(new \App\Mail\ContactReply("SMTP Connection Test (Mailable Mode)", "This is a test message sent using the actual ContactReply mailable class.\n\nTime: " . now()));
} else {
Mail::mailer($mailer)->raw("This is a test email from the SMTP Tester (Raw Mode).\n\nMailer: $mailer\nFrom: $fromAddress ($fromName)\nTime: " . now(), function ($message) use ($targetEmail, $fromAddress, $fromName) {
$message->to($targetEmail)
->from($fromAddress, $fromName)
->subject('SMTP Connection Test (Raw Mode) - ' . config('app.name'));
});
}
$duration = round((microtime(true) - $start) * 1000, 2);
return back()->with('success', "Test email sent successfully via '{$mailer}' (Mode: {$mode}) in {$duration}ms!");
} catch (Exception $e) {
return back()->with('error', "Connection Failed: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Ticket;
use App\Models\TicketReply;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Notifications\NewTicketReply;
use App\Notifications\TicketStatusUpdated;
class TicketManagementController extends Controller
{
public function index(Request $request)
{
$query = Ticket::with('user');
if ($request->has('search')) {
$search = $request->search;
$query->where(function($q) use ($search) {
$q->where('ticket_number', 'like', "%{$search}%")
->orWhere('subject', 'like', "%{$search}%")
->orWhereHas('user', function($u) use ($search) {
$u->where('email', 'like', "%{$search}%");
});
});
}
if ($request->has('status') && $request->status !== 'all') {
$query->where('status', $request->status);
}
$tickets = $query->latest()->paginate(10)->withQueryString();
return view('pages.admin.tickets.index', [
'tickets' => $tickets,
'title' => 'Support Ticket Management'
]);
}
public function show(Ticket $ticket)
{
$ticket->load(['replies.user', 'user.certificates', 'user.tickets' => function($q) {
$q->latest()->limit(5);
}]);
return view('pages.admin.tickets.show', [
'ticket' => $ticket,
'title' => 'Manage Ticket #' . $ticket->ticket_number
]);
}
public function reply(Request $request, Ticket $ticket)
{
$validated = $request->validate([
'message' => 'required|string|max:5000',
'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx',
]);
$attachmentPath = null;
if ($request->hasFile('attachment')) {
$attachmentPath = $request->file('attachment')->store('attachments', 'public');
}
$reply = TicketReply::create([
'ticket_id' => $ticket->id,
'user_id' => Auth::id(),
'message' => strip_tags($validated['message']),
'attachment_path' => $attachmentPath,
]);
// Auto update status to "Answered" if it was Open
if ($ticket->status === 'open') {
$ticket->update(['status' => 'answered']);
}
$ticket->touch();
// Notify User
$ticket->user->notify(new NewTicketReply($reply));
// Broadcast for real-time
broadcast(new \App\Events\TicketMessageSent($reply))->toOthers();
if ($request->ajax() || $request->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Reply sent successfully.',
'reply' => [
'id' => $reply->id,
'message' => $reply->message,
'user_id' => $reply->user_id,
'user_name' => $reply->user->name,
'is_staff' => true,
'created_at' => $reply->created_at->format('M d, Y H:i A'),
'attachment_url' => $reply->attachment_path ? \Storage::url($reply->attachment_path) : null,
]
]);
}
return back()->with('success', 'Reply sent successfully.');
}
public function updateStatus(Request $request, Ticket $ticket)
{
$validated = $request->validate([
'status' => 'required|in:open,answered,closed',
'priority' => 'required|in:low,medium,high',
]);
$oldStatus = $ticket->status;
$ticket->update([
'status' => $validated['status'],
'priority' => $validated['priority'],
]);
if ($oldStatus !== $validated['status']) {
$ticket->user->notify(new TicketStatusUpdated($ticket));
}
return back()->with('success', 'Ticket updated successfully.');
}
}

View File

@@ -41,6 +41,8 @@ class ApiKeyController extends Controller
'key' => $key,
]);
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
return back()->with('success', 'API Key generated successfully.')
->with('generated_key', $key);
}
@@ -53,6 +55,8 @@ class ApiKeyController extends Controller
$apiKey->delete();
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
return back()->with('success', 'API Key deleted successfully.');
}
@@ -66,6 +70,8 @@ class ApiKeyController extends Controller
'is_active' => !$apiKey->is_active
]);
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
if (request()->wantsJson()) {
return response()->json(['success' => true, 'message' => 'API Key status updated successfully.']);
}
@@ -86,6 +92,8 @@ class ApiKeyController extends Controller
'last_used_at' => null, // Reset usage
]);
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
if (request()->wantsJson()) {
return response()->json([
'success' => true,

View File

@@ -76,10 +76,14 @@ class AuthController extends Controller
$roleInfo = \App\Models\Role::where('name', 'customer')->first();
// Generate default avatar
$avatarPath = $this->generateDefaultAvatar($validated['fname'] . ' ' . ($validated['lname'] ?? ''), $validated['email']);
$user = User::create([
'first_name' => $validated['fname'],
'last_name' => $validated['lname'],
'email' => $validated['email'],
'avatar' => $avatarPath,
'password' => Hash::make($validated['password']),
'role_id' => $roleInfo ? $roleInfo->id : null,
]);
@@ -92,164 +96,101 @@ class AuthController extends Controller
}
/**
* Unified social redirect method
* Redirect to social provider
*/
public function socialRedirect($provider, $context)
{
// Store context in session for callback
session(['social_auth_context' => $context]);
return Socialite::driver($provider)->redirect();
}
/**
* Unified social callback method
* Handle social provider callback
*/
public function socialCallback($provider)
public function socialCallback($provider, Request $request)
{
if (request()->has('error')) {
\Log::info('Social auth error: ' . request()->get('error'));
return redirect()->route('signin')->with('error', 'Authentication failed or was cancelled.');
}
$context = session('social_auth_context', 'signin');
session()->forget('social_auth_context');
try {
$socialUser = Socialite::driver($provider)->user();
\Log::info('Social user retrieved', ['provider' => $provider, 'email' => $socialUser->email]);
} catch (\Exception $e) {
\Log::error('Socialite error: ' . $e->getMessage());
return redirect()->route('signin')->with('error', 'Failed to authenticate with ' . ucfirst($provider) . '.');
\Log::error("Social login failed for {$provider}: " . get_class($e) . ' - ' . $e->getMessage());
\Log::error($e->getTraceAsString());
return redirect()->route('signin')->with('error', 'Authentication failed. Please try again.');
}
$context = session('social_auth_context', 'signin');
\Log::info('Social auth context', ['context' => $context, 'is_authenticated' => Auth::check()]);
session()->forget('social_auth_context');
// Find user by social ID or email
$user = User::where($provider . '_id', $socialUser->getId())
->orWhere('email', $socialUser->getEmail())
->first();
// Handle 'connect' context - user wants to link social account
if ($context === 'connect') {
if (!Auth::check()) {
\Log::warning('Connect attempt without authentication');
return redirect()->route('signin')->with('error', 'Please sign in first to connect your social account.');
}
\Log::info('Handling social connect from settings');
return $this->connectSocialAccount($provider, $socialUser);
}
// If user is already authenticated (shouldn't happen for signin/signup)
if (Auth::check()) {
\Log::info('User already authenticated, treating as connect');
return $this->connectSocialAccount($provider, $socialUser);
}
// Handle based on context
if ($context === 'signin') {
\Log::info('Handling social signin');
return $this->handleSocialSignin($provider, $socialUser);
} else {
\Log::info('Handling social signup');
return $this->handleSocialSignup($provider, $socialUser);
}
}
/**
* Connect social account to existing authenticated user
*/
protected function connectSocialAccount($provider, $socialUser)
{
$user = Auth::user();
$updateData = [
$provider . '_id' => $socialUser->id,
$provider . '_token' => $socialUser->token,
$provider . '_refresh_token' => $socialUser->refreshToken,
];
// Ensure update persists
$user->forceFill($updateData)->save();
$this->recordLogin($user, $provider);
\Log::info('Social account connected for user', ['user_id' => $user->id, 'provider' => $provider]);
return redirect()->route('settings')->with('success', ucfirst($provider) . ' account connected successfully.');
}
/**
* Handle social signin (only for existing users)
*/
protected function handleSocialSignin($provider, $socialUser)
{
// Try to find user by provider ID first
$user = User::where($provider . '_id', $socialUser->id)->first();
// If user not found by ID
if (!$user) {
// Check if user exists by email just to provide a helpful error message
if ($socialUser->email && User::where('email', $socialUser->email)->exists()) {
$this->revokeSocialToken($provider, $socialUser->token);
return redirect()->route('signin')->with('error', "An account with this email exists but is not connected to " . ucfirst($provider) . ". Please log in with your password and connect your social account from Settings.");
if ($user) {
// Context: Connect
if ($context === 'connect') {
if (Auth::check()) {
$currentUser = Auth::user();
if ($user->id !== $currentUser->id) {
return redirect()->route('settings')->with('error', "This {$provider} account is already linked to another user.");
}
}
}
// Genuinely no account found
$this->revokeSocialToken($provider, $socialUser->token);
return redirect()->route('signin')->with('error', 'No account found with this ' . ucfirst($provider) . ' account. Please sign up first.');
}
// Proceed with login for connected user
// Update login tracking
$this->recordLogin($user, $provider);
Auth::login($user);
return redirect()->route('dashboard');
}
/**
* Handle social signup (only for new users)
*/
protected function handleSocialSignup($provider, $socialUser)
{
// Check if user already exists by email
$existingUser = User::where('email', $socialUser->email)->first();
if ($existingUser) {
return redirect()->route('signup')->withErrors([
'email' => 'An account with this email already exists. Please sign in instead.',
// Update social tokens/ID if not set or changed
$user->update([
$provider . '_id' => $socialUser->getId(),
$provider . '_token' => $socialUser->token,
$provider . '_refresh_token' => $socialUser->refreshToken ?? $user->{$provider . '_refresh_token'},
'email_verified_at' => $user->email_verified_at ?? now(), // Auto-verify if not already
]);
// Login user if not already auth or if in auth context
if ($context !== 'connect') {
Auth::login($user);
$this->recordLogin($user, $provider);
return redirect()->intended('dashboard');
}
return redirect()->route('settings')->with('success', "{$provider} account connected successfully.");
} else {
// New User or Connect (but account doesn't exist)
if ($context === 'connect' && Auth::check()) {
$user = Auth::user();
$user->update([
$provider . '_id' => $socialUser->getId(),
$provider . '_token' => $socialUser->token,
$provider . '_refresh_token' => $socialUser->refreshToken,
]);
return redirect()->route('settings')->with('success', "{$provider} account connected successfully.");
}
if ($context === 'signin') {
return redirect()->route('signin')->with('error', 'No account found with this email. Please sign up first.');
}
// Signup flow - Store in session and redirect to password setup
$nameParts = explode(' ', $socialUser->getName() ?? '', 2);
$firstName = $nameParts[0] ?? '';
$lastName = $nameParts[1] ?? '';
$avatarPath = $this->downloadSocialAvatar($socialUser->getAvatar(), $socialUser->getEmail());
session([
'needs_password_setup' => true,
'social_signup_provider' => $provider,
'social_signup_email' => $socialUser->getEmail(),
'social_signup_first_name' => $firstName,
'social_signup_last_name' => $lastName,
'social_signup_avatar_path' => $avatarPath,
'social_signup_provider_id' => $socialUser->getId(),
'social_signup_provider_token' => $socialUser->token,
'social_signup_provider_refresh_token' => $socialUser->refreshToken ?? null,
]);
return redirect()->route('setup-password');
}
// Download and save avatar
$avatarPath = null;
if ($socialUser->avatar) {
$avatarPath = $this->downloadSocialAvatar($socialUser->avatar, $socialUser->email);
}
// Parse name
$nameParts = explode(' ', $socialUser->name ?? $socialUser->email, 2);
$firstName = $nameParts[0];
$lastName = $nameParts[1] ?? '';
// Store user data in session for password setup
session([
'needs_password_setup' => true,
'social_signup_provider' => $provider,
'social_signup_name' => $socialUser->name ?? $socialUser->email,
'social_signup_email' => $socialUser->email,
'social_signup_first_name' => $firstName,
'social_signup_last_name' => $lastName,
'social_signup_avatar' => $avatarPath ? asset('storage/' . $avatarPath) : null,
'social_signup_avatar_path' => $avatarPath,
'social_signup_provider_id' => $socialUser->id,
'social_signup_provider_token' => $socialUser->token,
'social_signup_provider_refresh_token' => $socialUser->refreshToken,
]);
\Log::info('Social signup - redirecting to password setup', ['email' => $socialUser->email]);
// Redirect to password setup page
return redirect()->route('setup-password');
}
/**
@@ -265,7 +206,10 @@ class AuthController extends Controller
$imageContent = file_get_contents($avatarUrl);
if ($imageContent === false) {
return null;
// Determine name for fallback
// We don't have the name here easily, so we use email part
$name = explode('@', $userEmail)[0];
return $this->generateDefaultAvatar($name, $userEmail);
}
// Save to storage
@@ -273,9 +217,26 @@ class AuthController extends Controller
return $filename;
} catch (\Exception $e) {
// If download fails, return null (user will have default avatar)
// If download fails, return generated avatar
\Log::error('Failed to download social avatar: ' . $e->getMessage());
return null;
$name = explode('@', $userEmail)[0];
return $this->generateDefaultAvatar($name, $userEmail);
}
}
/**
* Generate default avatar using Laravolt
*/
protected function generateDefaultAvatar($name, $email)
{
try {
$filename = 'avatars/' . Str::slug($email) . '_' . time() . '.png';
$avatar = \Laravolt\Avatar\Facade::create($name)->getImageObject()->encode(new \Intervention\Image\Encoders\PngEncoder());
Storage::disk('public')->put($filename, $avatar);
return $filename;
} catch (\Exception $e) {
\Log::error('Failed to generate default avatar: ' . $e->getMessage());
return null; // Ultimate fallback to null
}
}
@@ -330,6 +291,7 @@ class AuthController extends Controller
$provider . '_id' => $providerId,
$provider . '_token' => $providerToken,
$provider . '_refresh_token' => $providerRefreshToken,
'email_verified_at' => now(), // Auto-verify email from trusted social provider
]);
$this->recordLogin($user, $provider);

View File

@@ -252,6 +252,8 @@ BATCH;
'valid_to' => $result['valid_to'] ?? null,
]);
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
return redirect()->route('certificate.index')->with('success', 'Certificate generated successfully.');
} catch (\Exception $e) {
return redirect()->back()->withInput()->with('error', $e->getMessage());
@@ -285,6 +287,8 @@ BATCH;
'created_at' => now(), // Refresh timestamp
]);
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
return redirect()->route('certificate.index')->with('success', 'Certificate regenerated successfully.');
} catch (\Exception $e) {
return redirect()->route('certificate.index')->with('error', 'Failed to regenerate: ' . $e->getMessage());
@@ -348,6 +352,9 @@ BATCH;
{
$this->authorizeOwner($certificate);
$certificate->delete();
\App\Events\DashboardStatsUpdated::dispatch(Auth::id());
return redirect()->route('certificate.index')->with('success', 'Certificate deleted successfully.');
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Models\ContactSubmission;
use Illuminate\Http\Request;
class ContactController extends Controller
{
public function index()
{
return view('pages.public.contact');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|max:255',
'category' => 'required|string|in:Legal Inquiry,Technical Support,Partnership,Other',
'subject' => 'required|string|max:255',
'message' => 'required|string|max:2000',
]);
ContactSubmission::create([
'name' => strip_tags($validated['name']),
'email' => $validated['email'],
'category' => $validated['category'],
'subject' => strip_tags($validated['subject']),
'message' => strip_tags($validated['message']),
]);
return back()->with('success', 'Your message has been sent successfully. We will get back to you soon!');
}
}

View File

@@ -48,15 +48,104 @@ class DashboardController extends Controller
->count();
}
return view('pages.dashboard', compact(
'totalApiKeys',
'totalCertificates',
'activeCertificates',
'expiringSoonCount',
'recentCertificates',
'recentApiActivity',
'months',
'issuanceData'
));
// Format data for view
$formattedCertificates = $recentCertificates->map(fn($c) => [
'common_name' => $c->common_name,
'organization' => $c->organization,
'created_at' => $c->created_at->format('M d, Y'),
'valid_to' => $c->valid_to->format('M d, Y'),
'is_valid' => $c->valid_to > now(),
]);
$formattedApiActivity = $recentApiActivity->map(fn($k) => [
'name' => $k->name,
'last_used_diff' => $k->last_used_at?->diffForHumans() ?? 'None',
]);
return view('pages.dashboard', [
'totalApiKeys' => $totalApiKeys,
'totalCertificates' => $totalCertificates,
'activeCertificates' => $activeCertificates,
'expiringSoonCount' => $expiringSoonCount,
'recentCertificates' => $formattedCertificates,
'recentApiActivity' => $formattedApiActivity,
'months' => $months,
'issuanceData' => $issuanceData
]);
}
public function stats()
{
$user = Auth::user();
// Basic Counts
$totalApiKeys = $user->apiKeys()->count();
$totalCertificates = $user->certificates()->count();
$activeCertificates = $user->certificates()
->where('valid_to', '>', now())
->count();
$expiringSoonCount = $user->certificates()
->where('valid_to', '>', now())
->where('valid_to', '<=', now()->addDays(14))
->count();
// Recent Activity
$recentCertificates = $user->certificates()
->latest()
->limit(5)
->get()->map(function($cert) {
return [
'common_name' => $cert->common_name,
'organization' => $cert->organization,
'created_at' => $cert->created_at->format('M d, Y'),
'valid_to' => $cert->valid_to->format('M d, Y'),
'is_valid' => $cert->valid_to > now(),
];
});
$recentApiActivity = $user->apiKeys()
->whereNotNull('last_used_at')
->orderBy('last_used_at', 'desc')
->limit(5)
->get()->map(function($key) {
return [
'name' => $key->name,
'last_used_diff' => $key->last_used_at->diffForHumans(),
];
});
// Chart Data
$months = [];
$issuanceData = [];
for ($i = 5; $i >= 0; $i--) {
$date = now()->subMonths($i);
$months[] = $date->format('M');
$issuanceData[] = $user->certificates()
->whereYear('created_at', $date->year)
->whereMonth('created_at', $date->month)
->count();
}
return response()->json([
'totalApiKeys' => $totalApiKeys,
'totalCertificates' => $totalCertificates,
'activeCertificates' => $activeCertificates,
'expiringSoonCount' => $expiringSoonCount,
'recentCertificates' => $recentCertificates,
'recentApiActivity' => $recentApiActivity,
'months' => $months,
'issuanceData' => $issuanceData,
'maxIssuance' => max($issuanceData) ?: 1,
]);
}
public function ping()
{
$userId = Auth::id();
$timestamp = now()->getTimestampMs();
\App\Events\PingResponse::dispatch($userId, $timestamp);
return response()->noContent();
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\LegalPage;
use Illuminate\Http\Request;
class LegalController extends Controller
{
public function show($slug)
{
$page = LegalPage::where('slug', $slug)
->where('is_active', true)
->with(['currentRevision'])
->firstOrFail();
$revision = $page->currentRevision;
if (!$revision) {
abort(404, 'No active content found for this page.');
}
return view('pages.legal.show', compact('page', 'revision'));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function markAsRead($id)
{
$notification = Auth::user()->notifications()->findOrFail($id);
$notification->markAsRead();
// Redirect to the target URL if available
if (isset($notification->data['action_url'])) {
return redirect($notification->data['action_url']);
}
return back();
}
public function markAllRead()
{
Auth::user()->unreadNotifications->markAsRead();
return back()->with('success', 'All notifications marked as read.');
}
public function getUnread()
{
$notifications = Auth::user()->unreadNotifications->map(function ($notification) {
return [
'id' => $notification->id,
'data' => [
'title' => $notification->data['title'] ?? 'Notification',
'body' => $notification->data['body'] ?? '',
'icon' => $notification->data['icon'] ?? null,
'action_url' => $notification->data['action_url'] ?? null,
],
'created_at_human' => $notification->created_at->diffForHumans(),
'read_url' => route('notifications.read', $notification->id),
];
});
return response()->json([
'notifications' => $notifications,
'count' => $notifications->count()
]);
}
}

View File

@@ -17,6 +17,11 @@ class PageController extends Controller
return view('pages.blank', ['title' => 'Blank']);
}
public function landing()
{
return view('landing', ['title' => 'Home']);
}
public function error404()
{
return view('pages.errors.error-404', ['title' => 'Error 404']);

View File

@@ -0,0 +1,177 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Models\User;
use App\Models\Certificate;
use App\Models\Ticket;
use App\Models\CaCertificate;
use App\Helpers\MenuHelper;
class SearchController extends Controller
{
public function global(Request $request)
{
$query = $request->get('q');
$user = Auth::user();
if (!$query || strlen($query) < 2) {
return response()->json([
'Navigation' => $this->getNavigation($user, $query)
]);
}
$results = [];
// 1. Navigation Search
$navigation = $this->getNavigation($user, $query);
if (!empty($navigation)) {
$results['Navigation'] = $navigation;
}
if ($user->isAdmin()) {
// 2. Admin: User Search
$users = User::where('name', 'like', "%$query%")
->orWhere('email', 'like', "%$query%")
->limit(5)
->get()
->map(fn($u) => [
'label' => $u->name,
'sublabel' => $u->email,
'url' => route('admin.users.index') . '?search=' . $u->email,
'icon' => 'user'
]);
if ($users->count() > 0) $results['Users'] = $users;
// 3. Admin: Ticket Search
$tickets = Ticket::where('ticket_id', 'like', "%$query%")
->orWhere('subject', 'like', "%$query%")
->limit(5)
->get()
->map(fn($t) => [
'label' => $t->subject,
'sublabel' => '#' . $t->ticket_id,
'url' => route('admin.tickets.show', $t->id),
'icon' => 'ticket'
]);
if ($tickets->count() > 0) $results['Tickets'] = $tickets;
// 4. Admin: Root CA Search
$cas = CaCertificate::where('common_name', 'like', "%$query%")
->orWhere('uuid', 'like', "%$query%")
->limit(5)
->get()
->map(fn($ca) => [
'label' => $ca->common_name,
'sublabel' => 'Type: ' . strtoupper($ca->ca_type),
'url' => route('admin.root-ca.index'),
'icon' => 'shield'
]);
if ($cas->count() > 0) $results['Root CAs'] = $cas;
} else {
// 4. Customer: Certificate Search
$certs = Certificate::where('user_id', $user->id)
->where(function($q) use ($query) {
$q->where('common_name', 'like', "%$query%")
->orWhere('uuid', 'like', "%$query%");
})
->limit(5)
->get()
->map(fn($c) => [
'label' => $c->common_name,
'sublabel' => 'UUID: ' . substr($c->uuid, 0, 8) . '...',
'url' => route('certificate.index') . '?uuid=' . $c->uuid,
'icon' => 'certificate'
]);
if ($certs->count() > 0) $results['Certificates'] = $certs;
// 5. Customer: Ticket Search
$tickets = Ticket::where('user_id', $user->id)
->where(function($q) use ($query) {
$q->where('ticket_id', 'like', "%$query%")
->orWhere('subject', 'like', "%$query%");
})
->limit(5)
->get()
->map(fn($t) => [
'label' => $t->subject,
'sublabel' => '#' . $t->ticket_id,
'url' => route('support.show', $t->id),
'icon' => 'ticket'
]);
if ($tickets->count() > 0) $results['Tickets'] = $tickets;
}
return response()->json($results);
}
private function getNavigation($user, $query = null)
{
$menuGroups = MenuHelper::getMenuGroups();
$allNavs = [];
foreach ($menuGroups as $group) {
// Skip Template Gallery group explicitly just in case
if ($group['title'] === 'Template Gallery' || $group['title'] === 'Development') continue;
foreach ($group['items'] as $item) {
// Process main item
$this->processMenuItem($item, $allNavs);
// Process subItems if any
if (isset($item['subItems'])) {
foreach ($item['subItems'] as $subItem) {
$this->processMenuItem($subItem, $allNavs);
}
}
}
}
return collect($allNavs)
->filter(function($nav) use ($query) {
// Exclude templates
if (isset($nav['route_name']) && str_starts_with($nav['route_name'], 'templates.')) return false;
// Check query
if ($query && stripos($nav['label'], $query) === false) return false;
return true;
})
->unique('url') // Avoid duplicates
->values()
->take(8) // Give more suggestions
->toArray();
}
private function processMenuItem($item, &$allNavs)
{
if (!isset($item['route_name'])) return;
$allNavs[] = [
'label' => $item['name'],
'url' => route($item['route_name']),
'icon' => $this->mapIcon($item['icon'] ?? 'default'),
'route_name' => $item['route_name']
];
}
private function mapIcon($sidebarIcon)
{
// Map common sidebar icons to search icons
$map = [
'dashboard' => 'home',
'certificate' => 'certificate',
'support-ticket' => 'ticket',
'users' => 'users',
'user-profile' => 'user',
'settings' => 'settings',
'api-key' => 'key',
'server-settings' => 'shield',
];
return $map[$sidebarIcon] ?? 'default';
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use App\Models\User;
use App\Models\TicketReply;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use App\Notifications\NewTicketCreated;
use App\Notifications\NewTicketReply;
class TicketController extends Controller
{
public function index()
{
$tickets = Ticket::where('user_id', Auth::id())
->latest()
->paginate(10);
return view('pages.support.index', [
'tickets' => $tickets,
'title' => 'My Support Tickets'
]);
}
public function create()
{
return view('pages.support.create', [
'title' => 'Open New Ticket'
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'subject' => 'required|string|max:255',
'category' => 'required|string|in:Technical,Billing,General,Feature Request,Other',
'priority' => 'required|in:low,medium,high',
'message' => 'required|string|max:5000',
'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx',
]);
$ticket = Ticket::create([
'user_id' => Auth::id(),
'ticket_number' => 'TCK-' . date('Ymd') . '-' . strtoupper(Str::random(4)),
'subject' => strip_tags($validated['subject']),
'category' => $validated['category'],
'priority' => $validated['priority'],
'status' => 'open',
]);
$attachmentPath = null;
if ($request->hasFile('attachment')) {
$attachmentPath = $request->file('attachment')->store('attachments', 'public');
}
// Create initial message as a reply/description
TicketReply::create([
'ticket_id' => $ticket->id,
'user_id' => Auth::id(),
'message' => strip_tags($validated['message']),
'attachment_path' => $attachmentPath,
]);
// Notify Admins
$admins = User::whereHas('role', function($q) {
$q->where('name', 'admin');
})->get();
foreach ($admins as $admin) {
$admin->notify(new NewTicketCreated($ticket));
}
return redirect()->route('support.show', $ticket)
->with('success', 'Ticket created successfully.');
}
public function show(Ticket $ticket)
{
// Authorize
if ($ticket->user_id !== Auth::id()) {
abort(403);
}
$ticket->load(['replies.user', 'user']);
return view('pages.support.show', [
'ticket' => $ticket,
'title' => 'Ticket #' . $ticket->ticket_number
]);
}
public function reply(Request $request, Ticket $ticket)
{
if ($ticket->user_id !== Auth::id()) {
abort(403);
}
$validated = $request->validate([
'message' => 'required|string|max:5000',
'attachment' => 'nullable|file|max:2048|mimes:jpg,jpeg,png,pdf,doc,docx',
]);
$attachmentPath = null;
if ($request->hasFile('attachment')) {
$attachmentPath = $request->file('attachment')->store('attachments', 'public');
}
$reply = TicketReply::create([
'ticket_id' => $ticket->id,
'user_id' => Auth::id(),
'message' => strip_tags($validated['message']),
'attachment_path' => $attachmentPath,
]);
// Update ticket status if it was closed (optional, usually re-opens it)
if ($ticket->status === 'closed') {
$ticket->update(['status' => 'open']);
} else {
$ticket->touch(); // Update updated_at
}
// Notify Admins (Logic could be refined to notify specific assignee later)
$admins = User::whereHas('role', function($q) {
$q->where('name', 'admin');
})->get();
foreach ($admins as $admin) {
$admin->notify(new NewTicketReply($reply));
}
// Broadcast for real-time
broadcast(new \App\Events\TicketMessageSent($reply))->toOthers();
if ($request->ajax() || $request->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Reply sent successfully.',
'reply' => [
'id' => $reply->id,
'message' => $reply->message,
'user_id' => $reply->user_id,
'user_name' => $reply->user->name,
'created_at' => $reply->created_at->format('M d, Y H:i A'),
'attachment_url' => $reply->attachment_path ? \Storage::url($reply->attachment_path) : null,
]
]);
}
return back()->with('success', 'Reply sent successfully.');
}
public function close(Ticket $ticket)
{
if ($ticket->user_id !== Auth::id()) {
abort(403);
}
$ticket->update(['status' => 'closed']);
return back()->with('success', 'Ticket marked as closed.');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class ToolController extends Controller
{
/**
* Show the Telegram Chat ID Finder tool.
*/
public function chatIdFinder()
{
return view('pages.public.tools.chat-id-finder');
}
/**
* Show the App Key Generator tool.
*/
public function appKeyGenerator()
{
return view('pages.public.tools.app-key-generator');
}
}

View File

@@ -37,6 +37,9 @@ class CheckApiKey
// Update last used timestamp
$apiKey->update(['last_used_at' => now()]);
// Real-time update dashboard
\App\Events\DashboardStatsUpdated::dispatch($apiKey->user_id);
// Put the user in the request context
$request->merge(['authenticated_user' => $apiKey->user]);

64
app/Mail/ContactReply.php Normal file
View File

@@ -0,0 +1,64 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class ContactReply extends Mailable
{
use Queueable, SerializesModels;
public $replyMessage;
public $replySubject;
/**
* Create a new message instance.
*/
public function __construct($subject, $message)
{
$this->replySubject = $subject;
$this->replyMessage = $message;
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
from: new \Illuminate\Mail\Mailables\Address(
config('mail.mailers.support.from.address'),
config('mail.mailers.support.from.name')
),
subject: $this->replySubject,
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
markdown: 'emails.contact-reply',
with: [
'replyMessage' => $this->replyMessage,
]
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ContactSubmission extends Model
{
/** @use HasFactory<\Database\Factories\ContactSubmissionFactory> */
use HasFactory, HasUlids;
protected $fillable = [
'name',
'email',
'category',
'subject',
'message',
'is_read'
];
}

23
app/Models/LegalPage.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LegalPage extends Model
{
use HasFactory;
protected $fillable = ['title', 'slug', 'is_active'];
public function revisions()
{
return $this->hasMany(LegalPageRevision::class);
}
public function currentRevision()
{
return $this->hasOne(LegalPageRevision::class)->where('is_active', true)->latestOfMany();
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LegalPageRevision extends Model
{
use HasFactory;
protected $fillable = [
'legal_page_id',
'content',
'version',
'change_log',
'is_active',
'created_by'
];
public function legalPage()
{
return $this->belongsTo(LegalPage::class);
}
}

31
app/Models/Ticket.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class Ticket extends Model
{
use HasFactory, HasUuids;
protected $fillable = [
'user_id',
'ticket_number',
'subject',
'category',
'priority',
'status',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function replies()
{
return $this->hasMany(TicketReply::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
class TicketReply extends Model
{
use HasFactory, HasUuids;
protected $fillable = [
'ticket_id',
'user_id',
'message',
'attachment_path',
];
public function ticket()
{
return $this->belongsTo(Ticket::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -50,6 +50,7 @@ class User extends Authenticatable implements MustVerifyEmail
protected $fillable = [
'role_id',
'email',
'email_verified_at',
'password',
'avatar',
'first_name',
@@ -152,4 +153,9 @@ class User extends Authenticatable implements MustVerifyEmail
{
return $this->hasMany(Certificate::class);
}
public function tickets()
{
return $this->hasMany(Ticket::class);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Ticket;
class NewTicketCreated extends Notification implements ShouldQueue, ShouldBroadcast
{
use Queueable;
public $ticket;
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
public function via(object $notifiable): array
{
return ['database', 'broadcast'];
}
public function toArray(object $notifiable): array
{
return [
'icon' => 'ticket',
'title' => 'New Support Ticket',
'body' => "{$this->ticket->user->name} created ticket #{$this->ticket->ticket_number}: {$this->ticket->subject}",
'action_url' => route('admin.tickets.show', $this->ticket->id),
'type' => 'info',
'created_at' => now(),
];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'id' => $this->id,
'data' => $this->toArray($notifiable),
'created_at' => now(),
'created_at_human' => now()->diffForHumans(), // Pre-calculate for frontend
'read_url' => route('notifications.read', $this->id),
]);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\TicketReply;
class NewTicketReply extends Notification implements ShouldQueue, ShouldBroadcast
{
use Queueable;
public $reply;
public function __construct(TicketReply $reply)
{
$this->reply = $reply;
}
public function via(object $notifiable): array
{
return ['database', 'broadcast'];
}
public function toArray(object $notifiable): array
{
$ticket = $this->reply->ticket;
$url = $notifiable->isAdmin()
? route('admin.tickets.show', $ticket->id)
: route('support.show', $ticket->id);
return [
'icon' => 'chat',
'title' => "New Reply on Ticket #{$ticket->ticket_number}",
'body' => "{$this->reply->user->name}: " . \Illuminate\Support\Str::limit($this->reply->message, 50),
'action_url' => $url,
'type' => 'info',
'created_at' => now(),
];
}
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'id' => $this->id,
'data' => $this->toArray($notifiable),
'created_at' => now(),
'created_at_human' => now()->diffForHumans(),
'read_url' => route('notifications.read', $this->id),
]);
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class SystemAlert extends Notification implements ShouldBroadcast
{
use Queueable;
public $title;
public $message;
public $type;
/**
* Create a new notification instance.
*/
public function __construct($title, $message, $type = 'info')
{
$this->title = $title;
$this->message = $message;
$this->type = $type;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['database', 'broadcast'];
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'icon' => 'notification',
'title' => $this->title,
'body' => $this->message,
'type' => $this->type,
'action_url' => '#',
'created_at' => now(),
];
}
/**
* Get the broadcast representation of the notification.
*/
public function toBroadcast(object $notifiable): BroadcastMessage
{
return new BroadcastMessage([
'id' => $this->id,
'data' => $this->toArray($notifiable),
'created_at' => now(),
'created_at_human' => now()->diffForHumans(),
'read_url' => route('notifications.read', $this->id),
]);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Models\Ticket;
class TicketStatusUpdated extends Notification implements ShouldQueue
{
use Queueable;
public $ticket;
public function __construct(Ticket $ticket)
{
$this->ticket = $ticket;
}
public function via(object $notifiable): array
{
return ['database'];
}
public function toArray(object $notifiable): array
{
return [
'icon' => 'check-circle',
'title' => 'Ticket Status Updated',
'body' => "Your ticket #{$this->ticket->ticket_number} has been marked as " . ucfirst($this->ticket->status),
'action_url' => route('support.show', $this->ticket->id),
'type' => $this->ticket->status === 'closed' ? 'success' : 'info',
'created_at' => now(),
];
}
}

View File

@@ -8,6 +8,7 @@ return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
channels: __DIR__.'/../routes/channels.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {

View File

@@ -12,8 +12,10 @@
"require": {
"php": "^8.2",
"laravel/framework": "^12.0",
"laravel/reverb": "^1.0",
"laravel/socialite": "^5.24",
"laravel/tinker": "^2.10.1"
"laravel/tinker": "^2.10.1",
"laravolt/avatar": "^6.3"
},
"require-dev": {
"fakerphp/faker": "^1.23",

1242
composer.lock generated

File diff suppressed because it is too large Load Diff

82
config/broadcasting.php Normal file
View File

@@ -0,0 +1,82 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_CONNECTION', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over WebSockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'reverb' => [
'driver' => 'reverb',
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
'port' => env('PUSHER_PORT', 443),
'scheme' => env('PUSHER_SCHEME', 'https'),
'encrypted' => true,
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
],
'client_options' => [
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@@ -0,0 +1,253 @@
<?php
/*
|--------------------------------------------------------------------------
| HD Avatar Configuration
|--------------------------------------------------------------------------
| Configuration for high-definition avatar generation with enhanced
| performance through image export and storage optimization.
|
*/
return [
/*
|--------------------------------------------------------------------------
| HD Image Settings
|--------------------------------------------------------------------------
| Configuration for high-definition avatar generation
|
*/
'hd' => [
// Enable HD mode - when true, uses higher resolution settings
'enabled' => env('AVATAR_HD_ENABLED', true),
// HD dimensions (default: 512x512, supports up to 2048x2048)
'width' => env('AVATAR_HD_WIDTH', 512),
'height' => env('AVATAR_HD_HEIGHT', 512),
// HD font size (scales with dimensions)
'fontSize' => env('AVATAR_HD_FONT_SIZE', 192),
// Export quality settings
'quality' => [
'png' => env('AVATAR_HD_PNG_QUALITY', 95),
'jpg' => env('AVATAR_HD_JPG_QUALITY', 90),
'webp' => env('AVATAR_HD_WEBP_QUALITY', 85),
],
// Anti-aliasing for smoother edges
'antialiasing' => env('AVATAR_HD_ANTIALIASING', true),
// DPI for high-quality rendering
'dpi' => env('AVATAR_HD_DPI', 300),
],
/*
|--------------------------------------------------------------------------
| Export and Storage Settings
|--------------------------------------------------------------------------
| Configuration for image export and storage optimization
|
*/
'export' => [
// Default export format
'format' => env('AVATAR_EXPORT_FORMAT', 'png'), // png, jpg, webp
// Export path (relative to storage/app)
'path' => env('AVATAR_EXPORT_PATH', 'avatars'),
// Filename pattern: {name}, {initials}, {hash}, {timestamp}
'filename_pattern' => env('AVATAR_EXPORT_FILENAME', '{hash}_{timestamp}.{format}'),
// Enable multiple format export
'multiple_formats' => env('AVATAR_EXPORT_MULTIPLE', false),
// Progressive JPEG for better loading
'progressive_jpeg' => env('AVATAR_PROGRESSIVE_JPEG', true),
// WebP lossless compression
'webp_lossless' => env('AVATAR_WEBP_LOSSLESS', false),
],
/*
|--------------------------------------------------------------------------
| Performance and Caching
|--------------------------------------------------------------------------
| Enhanced caching and performance settings for HD avatars
|
*/
'performance' => [
// Enable file-based caching in addition to memory cache
'file_cache' => env('AVATAR_FILE_CACHE', true),
// Cache different sizes separately
'size_based_cache' => env('AVATAR_SIZE_CACHE', true),
// Preload fonts for better performance
'preload_fonts' => env('AVATAR_PRELOAD_FONTS', true),
// Background processing for large batches
'background_processing' => env('AVATAR_BACKGROUND_PROCESSING', false),
// Lazy loading support
'lazy_loading' => env('AVATAR_LAZY_LOADING', true),
// Compression levels
'compression' => [
'png' => env('AVATAR_PNG_COMPRESSION', 6), // 0-9
'webp' => env('AVATAR_WEBP_COMPRESSION', 80), // 0-100
],
],
/*
|--------------------------------------------------------------------------
| Storage Management
|--------------------------------------------------------------------------
| Configuration for storage optimization and cleanup
|
*/
'storage' => [
// Automatic cleanup of old files
'auto_cleanup' => env('AVATAR_AUTO_CLEANUP', true),
// Maximum age for cached files (in days)
'max_age_days' => env('AVATAR_MAX_AGE_DAYS', 30),
// Maximum storage size (in MB, 0 = unlimited)
'max_storage_mb' => env('AVATAR_MAX_STORAGE_MB', 500),
// Storage driver (local, s3, etc.)
'disk' => env('AVATAR_STORAGE_DISK', 'local'),
// CDN URL for serving images
'cdn_url' => env('AVATAR_CDN_URL', null),
// Enable storage metrics
'metrics' => env('AVATAR_STORAGE_METRICS', false),
],
/*
|--------------------------------------------------------------------------
| HD Themes
|--------------------------------------------------------------------------
| Enhanced themes with HD-specific optimizations
|
*/
'hd_themes' => [
'ultra-hd' => [
'width' => 1024,
'height' => 1024,
'fontSize' => 384,
'backgrounds' => [
'#667eea', '#764ba2', '#f093fb', '#f5576c',
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
'#ffecd2', '#fcb69f', '#a8edea', '#fed6e3',
],
'foregrounds' => ['#FFFFFF'],
'border' => [
'size' => 4,
'color' => 'foreground',
'radius' => 8,
],
],
'retina' => [
'width' => 512,
'height' => 512,
'fontSize' => 192,
'backgrounds' => [
'#667eea', '#764ba2', '#f093fb', '#f5576c',
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
],
'foregrounds' => ['#FFFFFF'],
],
'material-hd' => [
'width' => 384,
'height' => 384,
'fontSize' => 144,
'shape' => 'circle',
'backgrounds' => [
'#1976D2', '#388E3C', '#F57C00', '#7B1FA2',
'#5D4037', '#455A64', '#E64A19', '#00796B',
],
'foregrounds' => ['#FFFFFF'],
'border' => [
'size' => 2,
'color' => 'background',
'radius' => 0,
],
],
],
/*
|--------------------------------------------------------------------------
| Responsive Sizes
|--------------------------------------------------------------------------
| Predefined sizes for responsive avatar generation
|
*/
'responsive_sizes' => [
'thumbnail' => ['width' => 64, 'height' => 64, 'fontSize' => 24],
'small' => ['width' => 128, 'height' => 128, 'fontSize' => 48],
'medium' => ['width' => 256, 'height' => 256, 'fontSize' => 96],
'large' => ['width' => 512, 'height' => 512, 'fontSize' => 192],
'xl' => ['width' => 768, 'height' => 768, 'fontSize' => 288],
'xxl' => ['width' => 1024, 'height' => 1024, 'fontSize' => 384],
],
/*
|--------------------------------------------------------------------------
| Advanced Features
|--------------------------------------------------------------------------
| Additional HD avatar features
|
*/
'features' => [
// Generate avatar sprites for animations
'sprites' => env('AVATAR_SPRITES', false),
// Generate avatar variations (different colors/styles)
'variations' => env('AVATAR_VARIATIONS', false),
// Generate blur placeholder images
'placeholders' => env('AVATAR_PLACEHOLDERS', true),
// Generate different aspect ratios
'aspect_ratios' => env('AVATAR_ASPECT_RATIOS', false),
// Watermarking support
'watermark' => [
'enabled' => env('AVATAR_WATERMARK', false),
'text' => env('AVATAR_WATERMARK_TEXT', ''),
'opacity' => env('AVATAR_WATERMARK_OPACITY', 0.3),
'position' => env('AVATAR_WATERMARK_POSITION', 'bottom-right'),
],
],
/*
|--------------------------------------------------------------------------
| API Settings
|--------------------------------------------------------------------------
| Configuration for avatar API endpoints
|
*/
'api' => [
// Enable avatar API endpoints
'enabled' => env('AVATAR_API_ENABLED', true),
// Rate limiting (requests per minute)
'rate_limit' => env('AVATAR_API_RATE_LIMIT', 60),
// Enable CORS for API endpoints
'cors' => env('AVATAR_API_CORS', true),
// API authentication
'auth' => env('AVATAR_API_AUTH', false),
// Response headers
'headers' => [
'Cache-Control' => 'public, max-age=31536000', // 1 year
'Expires' => gmdate('D, d M Y H:i:s', time() + 31536000).' GMT',
],
],
];

174
config/laravolt/avatar.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
/*
* Set specific configuration variables here
*/
return [
/*
|--------------------------------------------------------------------------
| Image Driver
|--------------------------------------------------------------------------
| Avatar use Intervention Image library to process image.
| Meanwhile, Intervention Image supports "GD Library" and "Imagick" to process images
| internally. You may choose one of them according to your PHP
| configuration. By default PHP's "GD Library" implementation is used.
|
| Supported: "gd", "imagick"
|
*/
'driver' => env('IMAGE_DRIVER', 'gd'),
/*
|--------------------------------------------------------------------------
| Cache Configuration
|--------------------------------------------------------------------------
| Control caching behavior for avatars
|
*/
'cache' => [
// Set to true to enable caching, false to disable
'enabled' => env('AVATAR_CACHE_ENABLED', true),
// Cache prefix to avoid conflicts with other cached items
'key_prefix' => 'avatar_',
// Cache duration in seconds
// Set to null to cache forever, 0 to disable cache
// Default: 86400 (24 hours)
'duration' => env('AVATAR_CACHE_DURATION', 86400),
],
// Initial generator class
'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class,
// Whether all characters supplied must be replaced with their closest ASCII counterparts
'ascii' => false,
// Image shape: circle or square
'shape' => 'circle',
// Image width, in pixel
'width' => 100,
// Image height, in pixel
'height' => 100,
// Responsive SVG, height and width attributes are not added when true
'responsive' => false,
// Number of characters used as initials. If name consists of single word, the first N character will be used
'chars' => 2,
// font size
'fontSize' => 48,
// convert initial letter in uppercase
'uppercase' => false,
// Right to Left (RTL)
'rtl' => false,
// Fonts used to render text.
// If contains more than one fonts, randomly selected based on name supplied
'fonts' => [__DIR__.'/../fonts/OpenSans-Bold.ttf', __DIR__.'/../fonts/rockwell.ttf'],
// List of foreground colors to be used, randomly selected based on name supplied
'foregrounds' => [
'#FFFFFF',
],
// List of background colors to be used, randomly selected based on name supplied
'backgrounds' => [
'#f44336',
'#E91E63',
'#9C27B0',
'#673AB7',
'#3F51B5',
'#2196F3',
'#03A9F4',
'#00BCD4',
'#009688',
'#4CAF50',
'#8BC34A',
'#CDDC39',
'#FFC107',
'#FF9800',
'#FF5722',
],
'border' => [
'size' => 1,
// border color, available value are:
// 'foreground' (same as foreground color)
// 'background' (same as background color)
// or any valid hex ('#aabbcc')
'color' => 'background',
// border radius, currently only work for SVG
'radius' => 0,
],
// List of theme name to be used when rendering avatar
// Possible values are:
// 1. Theme name as string: 'colorful'
// 2. Or array of string name: ['grayscale-light', 'grayscale-dark']
// 3. Or wildcard "*" to use all defined themes
'theme' => ['colorful'],
// Predefined themes
// Available theme attributes are:
// shape, chars, backgrounds, foregrounds, fonts, fontSize, width, height, ascii, uppercase, and border.
'themes' => [
'grayscale-light' => [
'backgrounds' => ['#edf2f7', '#e2e8f0', '#cbd5e0'],
'foregrounds' => ['#a0aec0'],
],
'grayscale-dark' => [
'backgrounds' => ['#2d3748', '#4a5568', '#718096'],
'foregrounds' => ['#e2e8f0'],
],
'colorful' => [
'backgrounds' => [
'#f44336',
'#E91E63',
'#9C27B0',
'#673AB7',
'#3F51B5',
'#2196F3',
'#03A9F4',
'#00BCD4',
'#009688',
'#4CAF50',
'#8BC34A',
'#CDDC39',
'#FFC107',
'#FF9800',
'#FF5722',
],
'foregrounds' => ['#FFFFFF'],
],
'pastel' => [
'backgrounds' => [
'#ef9a9a',
'#F48FB1',
'#CE93D8',
'#B39DDB',
'#9FA8DA',
'#90CAF9',
'#81D4FA',
'#80DEEA',
'#80CBC4',
'#A5D6A7',
'#E6EE9C',
'#FFAB91',
'#FFCCBC',
'#D7CCC8',
],
'foregrounds' => [
'#FFF',
],
],
],
];

View File

@@ -50,6 +50,21 @@ return [
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'support' => [
'transport' => env('MAIL_SUPPORT_MAILER', 'smtp'),
'host' => env('MAIL_SUPPORT_HOST', '127.0.0.1'),
'port' => env('MAIL_SUPPORT_PORT', 2525),
'username' => env('MAIL_SUPPORT_USERNAME'),
'password' => env('MAIL_SUPPORT_PASSWORD'),
'encryption' => env('MAIL_SUPPORT_ENCRYPTION', 'tls'),
'from' => [
'address' => env('MAIL_SUPPORT_FROM_ADDRESS', 'support@lab.dyzulk.com'),
'name' => env('MAIL_SUPPORT_FROM_NAME', 'DyDev Support'),
],
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
],
'ses' => [
'transport' => 'ses',
],

95
config/reverb.php Normal file
View File

@@ -0,0 +1,95 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Reverb Server
|--------------------------------------------------------------------------
|
| This option controls the default server used by Reverb to handle
| incoming messages as well as broadcasting message to all your
| connected clients. At this time only "reverb" is supported.
|
*/
'default' => env('REVERB_SERVER', 'reverb'),
/*
|--------------------------------------------------------------------------
| Reverb Servers
|--------------------------------------------------------------------------
|
| Here you may define details for each of the supported Reverb servers.
| Each server has its own configuration options that are defined in
| the array below. You should ensure all the options are present.
|
*/
'servers' => [
'reverb' => [
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
'port' => env('REVERB_SERVER_PORT', 8080),
'path' => env('REVERB_SERVER_PATH', ''),
'hostname' => env('REVERB_HOST'),
'options' => [
'tls' => [],
],
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
'scaling' => [
'enabled' => env('REVERB_SCALING_ENABLED', false),
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
'server' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'database' => env('REDIS_DB', '0'),
'timeout' => env('REDIS_TIMEOUT', 60),
],
],
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
],
],
/*
|--------------------------------------------------------------------------
| Reverb Applications
|--------------------------------------------------------------------------
|
| Here you may define how Reverb applications are managed. If you choose
| to use the "config" provider, you may define an array of apps which
| your server will support, including their connection credentials.
|
*/
'apps' => [
'provider' => 'config',
'apps' => [
[
'key' => env('REVERB_APP_KEY'),
'secret' => env('REVERB_APP_SECRET'),
'app_id' => env('REVERB_APP_ID'),
'options' => [
'host' => env('REVERB_HOST'),
'port' => env('REVERB_PORT', 443),
'scheme' => env('REVERB_SCHEME', 'https'),
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
],
'allowed_origins' => ['*'],
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
'activity_timeout' => env('REVERB_APP_ACTIVITY_TIMEOUT', 30),
'max_connections' => env('REVERB_APP_MAX_CONNECTIONS'),
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
],
],
],
];

View File

@@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::dropIfExists('legal_page_revisions');
Schema::dropIfExists('legal_pages');
Schema::create('legal_pages', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
Schema::create('legal_page_revisions', function (Blueprint $table) {
$table->id();
$table->foreignId('legal_page_id')->constrained()->onDelete('cascade');
$table->longText('content');
$table->string('version')->default('1.0');
$table->text('change_log')->nullable();
$table->boolean('is_active')->default(true);
$table->unsignedBigInteger('created_by')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('legal_page_revisions');
Schema::dropIfExists('legal_pages');
}
};

View File

@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contact_submissions', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->string('name');
$table->string('email');
$table->string('category');
$table->string('subject');
$table->text('message');
$table->boolean('is_read')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contact_submissions');
}
};

View File

@@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('tickets', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('user_id', 32)->index();
$table->string('ticket_number')->unique();
$table->string('subject');
$table->string('category'); // Technical, Billing, etc.
$table->enum('priority', ['low', 'medium', 'high'])->default('medium');
$table->enum('status', ['open', 'answered', 'closed'])->default('open');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
Schema::create('ticket_replies', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('ticket_id')->constrained()->onDelete('cascade');
$table->string('user_id', 32)->index();
$table->text('message');
$table->string('attachment_path')->nullable();
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('ticket_replies');
Schema::dropIfExists('tickets');
}
};

View File

@@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notifications');
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('notifications', function (Blueprint $table) {
$table->string('notifiable_id')->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('notifications', function (Blueprint $table) {
$table->unsignedBigInteger('notifiable_id')->change();
});
}
};

View File

@@ -23,7 +23,14 @@ class DatabaseSeeder extends Seeder
['label' => 'Customer']
);
// Create Admin User
// Helper to generate avatar
$generateAvatar = function ($name, $email) {
$filename = 'avatars/' . \Illuminate\Support\Str::slug($email) . '_' . time() . '.png';
$avatar = \Laravolt\Avatar\Facade::create($name)->getImageObject()->encode(new \Intervention\Image\Encoders\PngEncoder());
\Illuminate\Support\Facades\Storage::disk('public')->put($filename, $avatar);
return $filename;
};
// Create Admin User
User::firstOrCreate(
['email' => 'admin@dyzulk.com'],
@@ -33,18 +40,20 @@ class DatabaseSeeder extends Seeder
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'role_id' => $adminRole->id,
'email_verified_at' => now(),
'avatar' => $generateAvatar('Admin User', 'admin@dyzulk.com'),
]
);
// Create Regular User
// Create Regular User
User::firstOrCreate(
['email' => 'test@example.com'],
['email' => 'user@dyzulk.com'],
[
'first_name' => 'Test',
'last_name' => 'User',
'first_name' => 'User',
'last_name' => 'Customer',
'password' => \Illuminate\Support\Facades\Hash::make('password'),
'role_id' => $customerRole->id,
'email_verified_at' => now(),
'avatar' => $generateAvatar('User Customer', 'user@dyzulk.com'),
]
);
@@ -58,5 +67,8 @@ class DatabaseSeeder extends Seeder
// 'role_id' => $customerRole->id,
// ]
// );
$this->call([
LegalPageSeeder::class,
]);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\LegalPage;
use App\Models\LegalPageRevision;
use Illuminate\Database\Seeder;
class LegalPageSeeder extends Seeder
{
public function run(): void
{
// 1. Terms and Conditions
$terms = LegalPage::firstOrCreate(
['slug' => 'terms-and-conditions'],
['title' => 'Terms and Conditions']
);
LegalPageRevision::create([
'legal_page_id' => $terms->id,
'version' => '1.0.0',
'change_log' => 'Initial revision',
'is_active' => true,
'content' => "# Terms and Conditions\n\nWelcome to DyDev TrustLab. These terms outline the rules and regulations for the use of our services.\n\n## 1. Acceptable Use\nBy accessing this website, we assume you accept these terms and conditions. Do not continue to use TrustLab if you do not agree to all of the terms and conditions stated on this page.\n\n## 2. Intellectual Property\nUnless otherwise stated, TrustLab and/or its licensors own the intellectual property rights for all material on TrustLab.\n\n## Contact Us\nIf you have any questions about these Terms, please contact us at **info@dydev.com** or via our [Contact Form](/contact)."
]);
// 2. Privacy Policy
$privacy = LegalPage::firstOrCreate(
['slug' => 'privacy-policy'],
['title' => 'Privacy Policy']
);
LegalPageRevision::create([
'legal_page_id' => $privacy->id,
'version' => '1.0.0',
'change_log' => 'Initial revision',
'is_active' => true,
'content' => "# Privacy Policy\n\nYour privacy is important to us. It is TrustLab's policy to respect your privacy regarding any information we may collect from you across our website.\n\n## 1. Information We Collect\nWe only ask for personal information when we truly need it to provide a service to you.\n\n## 2. Data Security\nWe protect your data within commercially acceptable means to prevent loss and theft.\n\n## Contact Us\nIf you have any questions about how we handle user data and personal information, please contact us at **privacy@dydev.com** or via our [Contact Form](/contact)."
]);
}
}

84
deploy.sh.example Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# ==========================================================
# Script Deployment Otomatis (GitHub -> aaPanel)
# Proyek: YOUR_PROJECT_NAME
# ==========================================
# --- 1. KONFIGURASI PATH & NOTIFIKASI ---
PROJECT_PATH="/www/wwwroot/your-domain.com"
PHP_BIN="/www/server/php/83/bin/php"
NODE_BIN="/www/server/nodejs/v24.12.0/bin/node"
# Fix Enviroment for Composer & Git
export HOME=/root
export COMPOSER_HOME=/root/.composer
git config --global --add safe.directory $PROJECT_PATH
# Telegram Config
BOT_TOKEN="YOUR_BOT_TOKEN"
CHAT_ID="YOUR_CHAT_ID"
function send_telegram() {
local message=$1
curl -s -X POST "https://api.telegram.org/bot$BOT_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID" \
-d "text=$message" \
-d "parse_mode=Markdown" > /dev/null
}
echo "==== STARTING DEPLOYMENT: $(date) ===="
send_telegram "🚀 *Deployment Started* for your-domain.com on $(date)"
cd $PROJECT_PATH || {
send_telegram "❌ *Deployment Failed*: Project path not found!";
exit 1;
}
# --- 2. UPDATE KODE DARI GITHUB ---
echo "[1/6] Fetching latest code from GitHub..."
git fetch --all
git reset --hard origin/main
# --- 3. MANAJEMEN DEPENDENSI (PHP) ---
echo "[2/6] Updating PHP dependencies (Composer)..."
# Mencari lokasi composer secara dinamis
if [ -f "/usr/local/bin/composer" ]; then
COMPOSER_PATH="/usr/local/bin/composer"
elif [ -f "/usr/bin/composer" ]; then
COMPOSER_PATH="/usr/bin/composer"
else
COMPOSER_PATH=$(which composer)
fi
$PHP_BIN $COMPOSER_PATH install --no-dev --optimize-autoloader --no-interaction || $PHP_BIN $COMPOSER_PATH update --no-dev --optimize-autoloader --no-interaction
# --- 4. DATABASE MIGRATION ---
echo "[3/6] Running database migrations..."
# Cek apakah vendor ada sebelum jalankan artisan
if [ ! -d "vendor" ]; then
echo "ERROR: Folder vendor tidak ditemukan. Composer install gagal."
send_telegram "❌ *Deployment Failed*: Vendor folder missing (Composer error)!"
exit 1
fi
$PHP_BIN artisan migrate --force
# --- 5. BUILD FRONTEND ASSETS ---
echo "[4/6] Installing & Building assets (Vite)..."
export PATH=$PATH:$(dirname $NODE_BIN)
npm install
npm run build
# --- 6. OPTIMASI LARAVEL ---
echo "[5/6] Running optimizations..."
$PHP_BIN artisan optimize
$PHP_BIN artisan view:cache
$PHP_BIN artisan storage:link --force
# --- 7. PERMISSION & CLEANUP ---
echo "[6/6] Fixing permissions..."
chown -R www:www $PROJECT_PATH
chmod -R 775 $PROJECT_PATH/storage $PROJECT_PATH/bootstrap/cache
echo "==== DEPLOYMENT COMPLETED SUCCESSFULLY ===="
send_telegram "✅ *Deployment Success*: your-domain.com is now updated!"

212
package-lock.json generated
View File

@@ -1,9 +1,12 @@
{
"name": "app",
"name": "app-ca-management",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "app-ca-management",
"version": "1.0.0",
"dependencies": {
"@floating-ui/dom": "^1.7.4",
"@fullcalendar/core": "^6.1.19",
@@ -20,10 +23,13 @@
"swiper": "^12.0.3"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.12",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.2.6",
"laravel-vite-plugin": "^2.0.0",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.1.12",
"vite": "^7.0.4"
}
@@ -896,6 +902,14 @@
"win32"
]
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@svgdotjs/svg.draggable.js": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz",
@@ -1214,6 +1228,19 @@
"node": ">= 10"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/vite": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.12.tgz",
@@ -1452,6 +1479,38 @@
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
"dev": true,
"license": "MIT",
"bin": {
"cssesc": "bin/cssesc"
},
"engines": {
"node": ">=4"
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -1494,6 +1553,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/engine.io-client": {
"version": "6.6.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
@@ -1843,6 +1928,20 @@
"integrity": "sha512-8VmL3Uuen08Es9xb2N6Wdc32TrQDGPXYCIdTB126jAhTJsYd/4r4Mc63VQA3qHxG0p4zeCI8sFO5XRsdjljMJg==",
"license": "MIT"
},
"node_modules/laravel-echo": {
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-2.2.6.tgz",
"integrity": "sha512-KuCldOrE8qbm0CVDBgc6FiX3VuReDu1C1xaS891KqwEUg9NT/Op03iiZqTWeVd0/WJ4H95q2pe9QEDJlwb/FPw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20"
},
"peerDependencies": {
"pusher-js": "*",
"socket.io-client": "*"
}
},
"node_modules/laravel-vite-plugin": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-2.0.1.tgz",
@@ -2184,6 +2283,14 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -2252,6 +2359,20 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
@@ -2278,6 +2399,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/pusher-js": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0.tgz",
"integrity": "sha512-wp3HqIIUc1GRyu1XrP6m2dgyE9MoCsXVsWNlohj0rjSkLf+a0jLvEyVubdg58oMk7bhjBWnFClgp8jfAa6Ak4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tweetnacl": "^1.0.3"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -2351,6 +2482,38 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/socket.io-client": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -2497,6 +2660,20 @@
"dev": true,
"license": "0BSD"
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"dev": true,
"license": "Unlicense"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
@@ -2614,6 +2791,39 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@@ -9,10 +9,13 @@
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.12",
"axios": "^1.11.0",
"concurrently": "^9.0.1",
"laravel-echo": "^2.2.6",
"laravel-vite-plugin": "^2.0.0",
"pusher-js": "^8.4.0",
"tailwindcss": "^4.1.12",
"vite": "^7.0.4"
},

262
public/chat-id.html Normal file
View File

@@ -0,0 +1,262 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Telegram Chat ID Finder | DyDev Admin</title>
<meta name="description" content="Find your Telegram Chat ID using your Bot Token. Secure client-side tool to find personal or group chat IDs easily.">
<meta name="keywords" content="telegram chat id finder, find chat id, telegram bot api, bot token chat id">
<meta name="robots" content="noindex, nofollow">
<!-- Open Graph -->
<meta property="og:title" content="Telegram Chat ID Finder | DyDev Admin">
<meta property="og:description" content="Quickly find your Telegram Chat ID for automated deployments and notifications.">
<meta property="og:type" content="website">
<meta property="og:image" content="/images/og-share.png">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Telegram Chat ID Finder | DyDev Admin">
<meta name="twitter:description" content="Quickly find your Telegram Chat ID for automated deployments and notifications.">
<meta name="twitter:image" content="/images/og-share.png">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#3C50E0',
}
}
}
}
</script>
<style>
[x-cloak] { display: none !important; }
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.animate-pulse-soft { animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
</style>
</head>
<body
x-data="{
darkMode: false,
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);
}
}"
x-init="
darkMode = JSON.parse(localStorage.getItem('darkMode')) || false;
$watch('darkMode', value => localStorage.setItem('darkMode', JSON.stringify(value)));
"
:class="{'dark bg-gray-900': darkMode === true}"
class="bg-gray-50 text-gray-800 transition-colors duration-300"
>
<div class="relative z-1 flex min-h-screen flex-col items-center justify-start overflow-hidden p-6 md:pt-20">
<!-- Back to Home -->
<div class="w-full max-w-[600px] mb-4">
<a href="/" 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="mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to home
</a>
</div>
<!-- Decoration -->
<div class="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
<svg class="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<defs>
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="currentColor" stroke-width="0.1"/>
</pattern>
</defs>
<rect width="100" height="100" fill="url(#grid)" />
</svg>
</div>
<!-- Main Content -->
<div class="mx-auto w-full max-w-[600px] bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-xl border border-gray-100 dark:border-gray-700 relative z-10">
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary rounded-lg text-white">
<svg xmlns="http://www.w3.org/2000/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="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>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">
Telegram Chat ID Finder
</h1>
</div>
<button
@click="darkMode = !darkMode"
class="p-2 rounded-lg bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
<template x-if="!darkMode">
<svg xmlns="http://www.w3.org/2000/svg" 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>
</template>
<template x-if="darkMode">
<svg xmlns="http://www.w3.org/2000/svg" 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 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M16.95 16.95l.707.707M7.05 7.05l.707.707M12 8a4 4 0 100 8 4 4 0 000-8z" />
</svg>
</template>
</button>
</div>
<p class="mb-8 text-sm text-gray-600 dark:text-gray-400 text-left">
Enter your <strong>Bot Token</strong> from @BotFather to see recent activity and find your <code>CHAT_ID</code>.
</p>
<div class="space-y-4 mb-8">
<div>
<label class="block text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2 text-left">Telegram Bot Token</label>
<div class="relative">
<input
type="text"
x-model="botToken"
placeholder="123456789:ABCDE..."
class="w-full bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-3 font-mono text-sm text-primary dark:text-blue-400 focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all"
@keydown.enter="findChats()"
/>
<button
@click="findChats()"
class="absolute right-2 top-2 bottom-2 px-4 bg-primary text-white text-xs font-bold rounded-lg hover:bg-opacity-90 transition-all flex items-center justify-center gap-2"
:disabled="loading"
>
<span x-show="!loading">Get 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>
</div>
<!-- Error Message -->
<div x-show="error" x-cloak class="mb-6 p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-800 text-red-600 dark:text-red-400 text-sm text-left flex gap-3">
<svg xmlns="http://www.w3.org/2000/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>
<!-- Results Table -->
<div x-show="chats.length > 0" x-cloak class="space-y-4">
<h3 class="text-sm font-semibold text-gray-800 dark:text-white text-left">Detected Chat IDs:</h3>
<div class="overflow-hidden rounded-xl border border-gray-100 dark:border-gray-700">
<table class="w-full text-left text-sm">
<thead class="bg-gray-50 dark:bg-gray-900/50">
<tr>
<th class="px-4 py-3 font-semibold text-gray-600 dark:text-gray-400">Name</th>
<th class="px-4 py-3 font-semibold text-gray-600 dark:text-gray-400">ID</th>
<th class="px-4 py-3 text-right"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-700">
<template x-for="chat in chats" :key="chat.id">
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/30 transition-colors">
<td class="px-4 py-4">
<div class="font-medium text-gray-800 dark:text-white" x-text="chat.name"></div>
<div class="text-xs text-gray-500 dark:text-gray-400" x-text="chat.username"></div>
</td>
<td class="px-4 py-4 font-mono text-primary dark:text-blue-400" x-text="chat.id"></td>
<td class="px-4 py-4 text-right">
<button
@click="copyToClipboard(chat.id, chat.id)"
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-400 hover:text-primary transition-all relative"
>
<svg x-show="copiedId !== chat.id" xmlns="http://www.w3.org/2000/svg" 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="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 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg>
<svg x-show="copiedId === chat.id" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="mt-8 pt-6 border-t border-gray-100 dark:border-gray-700 text-left">
<h3 class="text-xs font-bold text-gray-800 dark:text-white mb-2 uppercase tracking-tight">Instructions:</h3>
<ol class="text-xs text-gray-500 dark:text-gray-400 space-y-2 list-decimal ml-4">
<li>Send a random message (e.g., "Hello") to your Telegram Bot.</li>
<li>Paste your <strong>Bot Token</strong> above and click <strong>Get Updates</strong>.</li>
<li>Your <code>CHAT_ID</code> will appear in the table. Copy it and use it in your <code>deploy.sh</code> or <code>.env</code> file.</li>
</ol>
</div>
</div>
<p class="mt-12 text-center text-sm text-gray-500 dark:text-gray-400">
&copy; 2025 - DyDev TrustLab
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<svg width="494" height="158" viewBox="0 0 494 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Digit 4 -->
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#7592FF" stroke="#7592FF"/>
<rect x="75.8726" y="3.16748" width="32.6255" height="154.31" rx="6.26271" fill="#7592FF" stroke="#7592FF"/>
<rect x="16.7939" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3442)" fill="#7592FF" stroke="#7592FF"/>
<!-- Digit 0 -->
<rect x="163.27" y="12.8696" width="166.462" height="130.311" rx="28" stroke="#7592FF" stroke-width="24"/>
<rect x="213.874" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="246.752" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="268.972" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="202.425" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<rect x="218.167" y="83.1932" width="57.5655" height="20.7141" rx="2.63433" fill="#7592FF" stroke="#7592FF" stroke-width="0.752667"/>
<!-- Digit 3 -->
<rect x="460.859" y="11.188" width="32.6255" height="132.562" rx="6.26271" fill="#7592FF" stroke="#7592FF"/>
<rect x="371.731" y="33.4458" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4458)" fill="#7592FF" stroke="#7592FF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" fill="#7592FF" stroke="#7592FF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" fill="#7592FF" stroke="#7592FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,20 @@
<svg width="494" height="158" viewBox="0 0 494 158" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Digit 4 -->
<rect x="0.0405273" y="0.522461" width="32.6255" height="77.5957" rx="6.26271" fill="#465FFF" stroke="#465FFF"/>
<rect x="75.8726" y="3.16748" width="32.6255" height="154.31" rx="6.26271" fill="#465FFF" stroke="#465FFF"/>
<rect x="16.7939" y="91.3442" width="32.6255" height="77.5957" rx="6.26271" transform="rotate(-90 16.7939 91.3442)" fill="#465FFF" stroke="#465FFF"/>
<!-- Digit 0 -->
<rect x="163.27" y="12.8696" width="166.462" height="130.311" rx="28" stroke="#465FFF" stroke-width="24"/>
<rect x="213.874" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="257.523" y="42.0487" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="268.972" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="202.425" y="98.578" width="22.1453" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<rect x="218.167" y="83.1932" width="57.5655" height="20.7141" rx="2.63433" fill="#465FFF" stroke="#465FFF" stroke-width="0.752667"/>
<!-- Digit 3 -->
<rect x="460.859" y="11.188" width="32.6255" height="132.562" rx="6.26271" fill="#465FFF" stroke="#465FFF"/>
<rect x="371.731" y="33.4458" width="32.6255" height="107.028" rx="6.26271" transform="rotate(-90 371.731 33.4458)" fill="#465FFF" stroke="#465FFF"/>
<rect x="371.731" y="155.18" width="30" height="107.028" rx="6.26271" transform="rotate(-90 371.731 155.18)" fill="#465FFF" stroke="#465FFF"/>
<rect x="388.096" y="93.7812" width="32.6255" height="91.6638" rx="6.26271" transform="rotate(-90 388.096 93.7812)" fill="#465FFF" stroke="#465FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,53 +1,41 @@
<svg width="231" height="48" viewBox="0 0 231 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0.425781 12.6316C0.425781 5.65535 6.08113 0 13.0574 0H35.7942C42.7704 0 48.4258 5.65535 48.4258 12.6316V35.3684C48.4258 42.3446 42.7704 48 35.7942 48H13.0574C6.08113 48 0.425781 42.3446 0.425781 35.3684V12.6316Z" fill="#465FFF"/>
<g filter="url(#filter0_d_3903_56743)">
<path d="M13.0615 12.6323C13.0615 11.237 14.1926 10.106 15.5878 10.106C16.9831 10.106 18.1142 11.237 18.1142 12.6323V35.3691C18.1142 36.7644 16.9831 37.8954 15.5878 37.8954C14.1926 37.8954 13.0615 36.7644 13.0615 35.3691V12.6323Z" fill="white"/>
</g>
<g filter="url(#filter1_d_3903_56743)">
<path d="M22.5391 22.7353C22.5391 21.3401 23.6701 20.209 25.0654 20.209C26.4606 20.209 27.5917 21.3401 27.5917 22.7353V35.3669C27.5917 36.7621 26.4606 37.8932 25.0654 37.8932C23.6701 37.8932 22.5391 36.7621 22.5391 35.3669V22.7353Z" fill="white" fill-opacity="0.9" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter2_d_3903_56743)">
<path d="M32.0078 16.4189C32.0078 15.0236 33.1389 13.8926 34.5341 13.8926C35.9294 13.8926 37.0604 15.0236 37.0604 16.4189V35.3663C37.0604 36.7615 35.9294 37.8926 34.5341 37.8926C33.1389 37.8926 32.0078 36.7615 32.0078 35.3663V16.4189Z" fill="white" fill-opacity="0.7" shape-rendering="crispEdges"/>
</g>
<path d="M66.4258 15.1724H74.0585V37.0363H78.6239V15.1724H86.2567V10.9637H66.4258V15.1724Z" fill="white"/>
<path d="M91.3521 37.5C94.0984 37.5 96.4881 36.2516 97.2371 34.4326L97.5581 37.0363H101.375V26.3362C101.375 21.4498 98.4498 18.8818 93.7061 18.8818C88.9267 18.8818 85.788 21.3785 85.788 25.1948H89.4974C89.4974 23.3402 90.9241 22.2701 93.4921 22.2701C95.7035 22.2701 97.1301 23.2332 97.1301 25.6229V26.0152L91.8514 26.4075C87.6784 26.7285 85.3243 28.7616 85.3243 32.0073C85.3243 35.3243 87.607 37.5 91.3521 37.5ZM92.7788 34.2186C90.8171 34.2186 89.747 33.4339 89.747 31.8289C89.747 30.4022 90.7814 29.5106 93.4921 29.2609L97.1658 28.9756V29.9029C97.1658 32.6136 95.4538 34.2186 92.7788 34.2186Z" fill="white"/>
<path d="M107.825 15.8857C109.252 15.8857 110.429 14.7087 110.429 13.2464C110.429 11.784 109.252 10.6427 107.825 10.6427C106.327 10.6427 105.15 11.784 105.15 13.2464C105.15 14.7087 106.327 15.8857 107.825 15.8857ZM105.649 37.0363H110.001V19.4168H105.649V37.0363Z" fill="white"/>
<path d="M118.883 37.0363V10.5H114.568V37.0363H118.883Z" fill="white"/>
<path d="M126.337 37.0363L128.441 31.0086H138.179L140.283 37.0363H145.098L135.682 10.9637H131.009L121.593 37.0363H126.337ZM132.757 18.7391C133.007 18.0258 133.221 17.2411 133.328 16.7417C133.399 17.2768 133.649 18.0614 133.863 18.7391L136.859 27.1565H129.797L132.757 18.7391Z" fill="white"/>
<path d="M154.165 37.5C156.84 37.5 159.122 36.323 160.192 34.29L160.478 37.0363H164.472V10.5H160.157V21.6638C159.051 19.9161 156.875 18.8818 154.414 18.8818C149.1 18.8818 145.89 22.8052 145.89 28.2979C145.89 33.755 149.064 37.5 154.165 37.5ZM155.128 33.5053C152.096 33.5053 150.241 31.2939 150.241 28.1552C150.241 25.0165 152.096 22.7695 155.128 22.7695C158.159 22.7695 160.121 24.9808 160.121 28.1552C160.121 31.3296 158.159 33.5053 155.128 33.5053Z" fill="white"/>
<path d="M173.359 37.0363V27.0495C173.359 24.1962 175.035 22.8408 177.104 22.8408C179.172 22.8408 180.492 24.1605 180.492 26.6215V37.0363H184.843V27.0495C184.843 24.1605 186.448 22.8052 188.553 22.8052C190.621 22.8052 191.977 24.1248 191.977 26.6572V37.0363H196.292V25.5159C196.292 21.4498 193.938 18.8818 189.658 18.8818C186.983 18.8818 184.915 20.2015 184.023 22.2345C183.096 20.2015 181.241 18.8818 178.566 18.8818C176.034 18.8818 174.25 20.0231 173.359 21.4855L173.002 19.4168H169.007V37.0363H173.359Z" fill="white"/>
<path d="M202.74 15.8857C204.167 15.8857 205.344 14.7087 205.344 13.2464C205.344 11.784 204.167 10.6427 202.74 10.6427C201.242 10.6427 200.065 11.784 200.065 13.2464C200.065 14.7087 201.242 15.8857 202.74 15.8857ZM200.564 37.0363H204.916V19.4168H200.564V37.0363Z" fill="white"/>
<path d="M213.763 37.0363V27.5489C213.763 24.6955 215.403 22.8408 218.078 22.8408C220.325 22.8408 221.788 24.2675 221.788 27.2279V37.0363H226.139V26.1935C226.139 21.6281 223.856 18.8818 219.434 18.8818C217.044 18.8818 214.904 19.9161 213.798 21.6995L213.442 19.4168H209.411V37.0363H213.763Z" fill="white"/>
<defs>
<filter id="filter0_d_3903_56743" x="12.0615" y="9.60596" width="7.05273" height="29.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
<filter id="filter1_d_3903_56743" x="21.5391" y="19.709" width="7.05273" height="19.6843" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
<filter id="filter2_d_3903_56743" x="31.0078" y="13.3926" width="7.05273" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3903_56743"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3903_56743" result="shape"/>
</filter>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW (Evaluation Version) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="231px" height="48px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 117.53 24.42"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:none}
.fil2 {fill:#FEFEFE;fill-rule:nonzero}
.fil3 {fill:white;fill-rule:nonzero}
.fil1 {fill:url(#id0)}
]]>
</style>
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="-3.57" y1="-13.11" x2="32.97" y2="45.61">
<stop offset="0" style="stop-opacity:1; stop-color:#364A94"/>
<stop offset="1" style="stop-opacity:1; stop-color:#7D7AB8"/>
</linearGradient>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_671452448">
<rect class="fil0" width="117.53" height="24.42"/>
<g>
<path class="fil1" d="M19.42 23.85l-14.37 0c-2.46,0 -4.52,-2.06 -4.52,-4.52l0 -14.37c0,-2.47 2.06,-4.52 4.52,-4.52l14.37 0c2.47,0 4.52,2.05 4.52,4.52l0 14.37c0,2.46 -2.05,4.52 -4.52,4.52z"/>
<path class="fil2" d="M12.85 14.4l-2.05 0 -3.69 -4.1 3.69 -4.11 2.05 0 -3.69 4.11 3.69 4.1zm-2.46 0l-1.64 0 -3.7 -4.1 3.7 -4.11 1.23 0 -3.29 4.11c1.24,1.23 2.47,2.87 3.7,4.1l0 0z"/>
<path class="fil2" d="M12.03 9.89l2.06 0 3.69 4.1 -3.69 4.52 -2.06 0 4.11 -4.11 -4.11 -4.51zm2.88 0l1.23 0 4.1 4.51 -4.1 4.11 -1.23 0 3.69 -4.52c-1.23,-1.23 -2.46,-2.46 -3.69,-4.1z"/>
</g>
<path class="fil3" d="M34.94 6.54c1.3,0 2.44,0.27 3.41,0.77 0.96,0.51 1.71,1.25 2.24,2.18 0.54,0.93 0.81,2 0.81,3.21 0,1.21 -0.27,2.28 -0.81,3.21 -0.53,0.93 -1.28,1.66 -2.24,2.17 -0.99,0.51 -2.13,0.77 -3.41,0.77l-4.61 0 0 -12.31 4.61 0zm-0.19 9.72c1.14,0 2.02,-0.32 2.65,-0.93 0.63,-0.62 0.95,-1.49 0.95,-2.63 0,-1.14 -0.32,-2.02 -0.95,-2.65 -0.63,-0.63 -1.51,-0.95 -2.65,-0.95l-1.42 0 0 7.16 1.42 0z"/>
<polygon id="_1" class="fil3" points="51.91,9.07 45.78,23.5 42.55,23.5 44.8,18.52 40.82,9.07 44.16,9.07 46.43,15.19 48.67,9.07 "/>
<path id="_2" class="fil3" d="M56.45 6.54c1.3,0 2.44,0.27 3.41,0.77 0.96,0.51 1.71,1.25 2.24,2.18 0.54,0.93 0.81,2 0.81,3.21 0,1.21 -0.27,2.28 -0.81,3.21 -0.53,0.93 -1.28,1.66 -2.24,2.17 -0.99,0.51 -2.13,0.77 -3.41,0.77l-4.61 0 0 -12.31 4.61 0zm-0.19 9.72c1.14,0 2.02,-0.32 2.65,-0.93 0.63,-0.62 0.95,-1.49 0.95,-2.63 0,-1.14 -0.32,-2.02 -0.95,-2.65 -0.63,-0.63 -1.51,-0.95 -2.65,-0.95l-1.42 0 0 7.16 1.42 0z"/>
<path id="_3" class="fil3" d="M72.65 13.8c0,0.28 -0.01,0.58 -0.05,0.88l-6.78 0c0.05,0.61 0.24,1.07 0.57,1.4 0.36,0.32 0.78,0.48 1.28,0.48 0.76,0 1.27,-0.32 1.56,-0.95l3.2 0c-0.16,0.65 -0.46,1.23 -0.9,1.73 -0.42,0.51 -0.94,0.92 -1.59,1.21 -0.65,0.3 -1.37,0.44 -2.16,0.44 -0.97,0 -1.81,-0.21 -2.56,-0.61 -0.75,-0.4 -1.33,-1 -1.75,-1.76 -0.42,-0.75 -0.64,-1.64 -0.64,-2.66 0,-1.02 0.22,-1.91 0.62,-2.67 0.42,-0.75 1,-1.35 1.75,-1.75 0.76,-0.4 1.61,-0.61 2.58,-0.61 0.95,0 1.79,0.19 2.52,0.59 0.74,0.41 1.32,0.97 1.72,1.7 0.42,0.74 0.63,1.6 0.63,2.58zm-3.06 -0.79c0,-0.51 -0.18,-0.93 -0.53,-1.22 -0.35,-0.3 -0.79,-0.46 -1.32,-0.46 -0.5,0 -0.93,0.14 -1.28,0.44 -0.33,0.3 -0.54,0.7 -0.63,1.24l3.76 0z"/>
<polygon id="_4" class="fil3" points="77.47,16.05 79.6,9.07 82.79,9.07 79.32,18.85 75.62,18.85 72.14,9.07 75.35,9.07 "/>
<path id="_5" class="fil3" d="M94.27 16.68l-4.59 0 -0.74 2.17 -3.14 0 4.46 -12.31 3.47 0 4.45 12.31 -3.17 0 -0.74 -2.17zm-0.77 -2.32l-1.53 -4.5 -1.5 4.5 3.03 0z"/>
<path id="_6" class="fil3" d="M107.75 10.51c0,0.71 -0.15,1.36 -0.49,1.94 -0.33,0.6 -0.82,1.07 -1.51,1.44 -0.68,0.37 -1.52,0.54 -2.52,0.54l-1.86 0 0 4.42 -3 0 0 -12.31 4.86 0c0.98,0 1.8,0.18 2.49,0.51 0.68,0.33 1.19,0.81 1.52,1.4 0.34,0.6 0.51,1.28 0.51,2.06zm-4.75 1.54c0.58,0 1,-0.14 1.28,-0.4 0.28,-0.27 0.42,-0.65 0.42,-1.14 0,-0.5 -0.14,-0.88 -0.42,-1.14 -0.28,-0.27 -0.7,-0.41 -1.28,-0.41l-1.63 0 0 3.09 1.63 0z"/>
<path id="_7" class="fil3" d="M117.53 10.51c0,0.71 -0.15,1.36 -0.49,1.94 -0.33,0.6 -0.82,1.07 -1.5,1.44 -0.69,0.37 -1.53,0.54 -2.53,0.54l-1.86 0 0 4.42 -3 0 0 -12.31 4.86 0c0.98,0 1.81,0.18 2.49,0.51 0.69,0.33 1.19,0.81 1.53,1.4 0.33,0.6 0.5,1.28 0.5,2.06zm-4.75 1.54c0.58,0 1,-0.14 1.28,-0.4 0.28,-0.27 0.42,-0.65 0.42,-1.14 0,-0.5 -0.14,-0.88 -0.42,-1.14 -0.28,-0.27 -0.7,-0.41 -1.28,-0.41l-1.63 0 0 3.09 1.63 0z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1,53 +1,41 @@
<svg width="154" height="32" viewBox="0 0 154 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#465FFF"/>
<g filter="url(#filter0_d_1608_324)">
<path d="M8.42383 8.42152C8.42383 7.49135 9.17787 6.7373 10.108 6.7373C11.0382 6.7373 11.7922 7.49135 11.7922 8.42152V23.5794C11.7922 24.5096 11.0382 25.2636 10.108 25.2636C9.17787 25.2636 8.42383 24.5096 8.42383 23.5794V8.42152Z" fill="white"/>
</g>
<g filter="url(#filter1_d_1608_324)">
<path d="M14.7422 15.1569C14.7422 14.2267 15.4962 13.4727 16.4264 13.4727C17.3566 13.4727 18.1106 14.2267 18.1106 15.1569V23.5779C18.1106 24.5081 17.3566 25.2621 16.4264 25.2621C15.4962 25.2621 14.7422 24.5081 14.7422 23.5779V15.1569Z" fill="white" fill-opacity="0.9" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter2_d_1608_324)">
<path d="M21.0547 10.9459C21.0547 10.0158 21.8087 9.26172 22.7389 9.26172C23.6691 9.26172 24.4231 10.0158 24.4231 10.9459V23.5775C24.4231 24.5077 23.6691 25.2617 22.7389 25.2617C21.8087 25.2617 21.0547 24.5077 21.0547 23.5775V10.9459Z" fill="white" fill-opacity="0.7" shape-rendering="crispEdges"/>
</g>
<path d="M44 10.1149H49.0885V24.6909H52.1321V10.1149H57.2206V7.30912H44V10.1149Z" fill="white"/>
<path d="M60.6175 25C62.4484 25 64.0416 24.1678 64.5409 22.9551L64.7549 24.6909H67.2992V17.5575C67.2992 14.2999 65.3494 12.5878 62.1869 12.5878C59.0006 12.5878 56.9081 14.2523 56.9081 16.7966H59.3811C59.3811 15.5601 60.3322 14.8468 62.0442 14.8468C63.5184 14.8468 64.4696 15.4888 64.4696 17.0819V17.3435L60.9504 17.605C58.1684 17.819 56.599 19.1744 56.599 21.3382C56.599 23.5495 58.1208 25 60.6175 25ZM61.5686 22.8124C60.2609 22.8124 59.5475 22.2893 59.5475 21.2193C59.5475 20.2682 60.2371 19.6737 62.0442 19.5073L64.4934 19.317V19.9353C64.4934 21.7424 63.352 22.8124 61.5686 22.8124Z" fill="white"/>
<path d="M71.5995 10.5905C72.5506 10.5905 73.3353 9.80581 73.3353 8.83091C73.3353 7.85601 72.5506 7.09511 71.5995 7.09511C70.6008 7.09511 69.8161 7.85601 69.8161 8.83091C69.8161 9.80581 70.6008 10.5905 71.5995 10.5905ZM70.149 24.6909H73.0499V12.9445H70.149V24.6909Z" fill="white"/>
<path d="M78.9718 24.6909V7H76.0946V24.6909H78.9718Z" fill="white"/>
<path d="M83.9408 24.6909L85.3437 20.6724H91.8352L93.2381 24.6909H96.4481L90.1707 7.30912H87.0558L80.7784 24.6909H83.9408ZM88.2209 12.4927C88.3873 12.0172 88.53 11.4941 88.6013 11.1612C88.6489 11.5178 88.8153 12.041 88.958 12.4927L90.9554 18.1044H86.2473L88.2209 12.4927Z" fill="white"/>
<path d="M102.493 25C104.276 25 105.798 24.2153 106.511 22.86L106.701 24.6909H109.364V7H106.487V14.4425C105.75 13.2774 104.3 12.5878 102.659 12.5878C99.1161 12.5878 96.9761 15.2034 96.9761 18.8653C96.9761 22.5033 99.0923 25 102.493 25ZM103.135 22.3369C101.113 22.3369 99.877 20.8626 99.877 18.7701C99.877 16.6777 101.113 15.1797 103.135 15.1797C105.156 15.1797 106.464 16.6539 106.464 18.7701C106.464 20.8864 105.156 22.3369 103.135 22.3369Z" fill="white"/>
<path d="M115.289 24.6909V18.033C115.289 16.1308 116.406 15.2272 117.785 15.2272C119.164 15.2272 120.044 16.107 120.044 17.7477V24.6909H122.945V18.033C122.945 16.107 124.015 15.2034 125.418 15.2034C126.797 15.2034 127.701 16.0832 127.701 17.7715V24.6909H130.578V17.0106C130.578 14.2999 129.008 12.5878 126.155 12.5878C124.372 12.5878 122.993 13.4676 122.398 14.823C121.78 13.4676 120.543 12.5878 118.76 12.5878C117.072 12.5878 115.883 13.3487 115.289 14.3236L115.051 12.9445H112.388V24.6909H115.289Z" fill="white"/>
<path d="M134.876 10.5905C135.827 10.5905 136.612 9.80581 136.612 8.83091C136.612 7.85601 135.827 7.09511 134.876 7.09511C133.877 7.09511 133.093 7.85601 133.093 8.83091C133.093 9.80581 133.877 10.5905 134.876 10.5905ZM133.426 24.6909H136.327V12.9445H133.426V24.6909Z" fill="white"/>
<path d="M142.225 24.6909V18.3659C142.225 16.4637 143.318 15.2272 145.102 15.2272C146.6 15.2272 147.575 16.1783 147.575 18.1519V24.6909H150.476V17.4624C150.476 14.4188 148.954 12.5878 146.005 12.5878C144.412 12.5878 142.985 13.2774 142.248 14.4663L142.011 12.9445H139.324V24.6909H142.225Z" fill="white"/>
<defs>
<filter id="filter0_d_1608_324" x="7.42383" y="6.2373" width="5.36841" height="20.5264" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1608_324"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1608_324" result="shape"/>
</filter>
<filter id="filter1_d_1608_324" x="13.7422" y="12.9727" width="5.36841" height="13.7896" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1608_324"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1608_324" result="shape"/>
</filter>
<filter id="filter2_d_1608_324" x="20.0547" y="8.76172" width="5.36841" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1608_324"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1608_324" result="shape"/>
</filter>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW (Evaluation Version) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="154px" height="32px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 52.24 10.85"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:none}
.fil2 {fill:#FEFEFE;fill-rule:nonzero}
.fil3 {fill:white;fill-rule:nonzero}
.fil1 {fill:url(#id0)}
]]>
</style>
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="-1.9" y1="-6.28" x2="15.04" y2="20.95">
<stop offset="0" style="stop-opacity:1; stop-color:#364A94"/>
<stop offset="1" style="stop-opacity:1; stop-color:#7D7AB8"/>
</linearGradient>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_755716664">
<rect class="fil0" width="52.24" height="10.85"/>
<g>
<path class="fil1" d="M8.76 10.85l-6.67 0c-1.14,0 -2.09,-0.95 -2.09,-2.09l0 -6.67c0,-1.14 0.95,-2.09 2.09,-2.09l6.67 0c1.14,0 2.09,0.95 2.09,2.09l0 6.67c0,1.14 -0.95,2.09 -2.09,2.09z"/>
<path class="fil2" d="M5.71 6.47l-0.95 0 -1.71 -1.9 1.71 -1.9 0.95 0 -1.71 1.9 1.71 1.9zm-1.14 0l-0.76 0 -1.72 -1.9 1.72 -1.9 0.57 0 -1.52 1.9c0.57,0.57 1.14,1.33 1.71,1.9l0 0z"/>
<path class="fil2" d="M5.33 4.38l0.95 0 1.72 1.9 -1.72 2.1 -0.95 0 1.91 -1.91 -1.91 -2.09zm1.33 0l0.58 0 1.9 2.09 -1.9 1.91 -0.58 0 1.72 -2.1c-0.57,-0.57 -1.14,-1.14 -1.72,-1.9z"/>
</g>
<path class="fil3" d="M17 3.14c0.55,0 1.04,0.11 1.45,0.33 0.41,0.22 0.73,0.53 0.96,0.93 0.23,0.4 0.34,0.85 0.34,1.37 0,0.51 -0.11,0.97 -0.34,1.37 -0.23,0.39 -0.55,0.71 -0.96,0.92 -0.42,0.22 -0.9,0.33 -1.45,0.33l-1.97 0 0 -5.25 1.97 0zm-0.08 4.15c0.48,0 0.86,-0.14 1.13,-0.4 0.27,-0.26 0.4,-0.64 0.4,-1.12 0,-0.49 -0.13,-0.86 -0.4,-1.13 -0.27,-0.27 -0.65,-0.41 -1.13,-0.41l-0.61 0 0 3.06 0.61 0z"/>
<polygon id="_1" class="fil3" points="24.24,4.22 21.62,10.38 20.25,10.38 21.2,8.25 19.5,4.22 20.93,4.22 21.9,6.83 22.86,4.22 "/>
<path id="_2" class="fil3" d="M26.18 3.14c0.55,0 1.04,0.11 1.45,0.33 0.41,0.22 0.73,0.53 0.96,0.93 0.23,0.4 0.34,0.85 0.34,1.37 0,0.51 -0.11,0.97 -0.34,1.37 -0.23,0.39 -0.55,0.71 -0.96,0.92 -0.42,0.22 -0.91,0.33 -1.45,0.33l-1.97 0 0 -5.25 1.97 0zm-0.08 4.15c0.48,0 0.86,-0.14 1.12,-0.4 0.27,-0.26 0.41,-0.64 0.41,-1.12 0,-0.49 -0.14,-0.86 -0.41,-1.13 -0.26,-0.27 -0.64,-0.41 -1.12,-0.41l-0.61 0 0 3.06 0.61 0z"/>
<path id="_3" class="fil3" d="M33.09 6.24c0,0.12 -0.01,0.25 -0.02,0.37l-2.9 0c0.02,0.26 0.11,0.46 0.25,0.6 0.15,0.14 0.33,0.2 0.54,0.2 0.33,0 0.54,-0.13 0.67,-0.4l1.36 0c-0.07,0.28 -0.19,0.52 -0.38,0.74 -0.18,0.22 -0.4,0.39 -0.68,0.52 -0.28,0.12 -0.58,0.18 -0.92,0.18 -0.41,0 -0.77,-0.09 -1.09,-0.26 -0.32,-0.17 -0.57,-0.42 -0.75,-0.75 -0.18,-0.32 -0.27,-0.7 -0.27,-1.13 0,-0.44 0.09,-0.82 0.26,-1.14 0.18,-0.32 0.43,-0.58 0.75,-0.75 0.32,-0.17 0.69,-0.26 1.1,-0.26 0.4,0 0.76,0.08 1.08,0.25 0.31,0.18 0.56,0.42 0.73,0.73 0.18,0.31 0.27,0.68 0.27,1.1zm-1.31 -0.34c0,-0.21 -0.07,-0.39 -0.22,-0.52 -0.15,-0.13 -0.34,-0.2 -0.57,-0.2 -0.21,0 -0.39,0.06 -0.54,0.19 -0.14,0.13 -0.23,0.3 -0.27,0.53l1.6 0z"/>
<polygon id="_4" class="fil3" points="35.15,7.2 36.05,4.22 37.41,4.22 35.93,8.39 34.35,8.39 32.87,4.22 34.24,4.22 "/>
<path id="_5" class="fil3" d="M42.31 7.47l-1.96 0 -0.31 0.92 -1.34 0 1.9 -5.25 1.48 0 1.9 5.25 -1.35 0 -0.32 -0.92zm-0.33 -0.99l-0.65 -1.92 -0.64 1.92 1.29 0z"/>
<path id="_6" class="fil3" d="M48.06 4.83c0,0.31 -0.06,0.59 -0.21,0.83 -0.14,0.26 -0.35,0.46 -0.64,0.62 -0.29,0.15 -0.65,0.23 -1.08,0.23l-0.79 0 0 1.88 -1.28 0 0 -5.25 2.07 0c0.42,0 0.77,0.08 1.07,0.22 0.29,0.14 0.51,0.34 0.65,0.6 0.14,0.25 0.21,0.54 0.21,0.87zm-2.02 0.66c0.24,0 0.42,-0.06 0.54,-0.17 0.12,-0.11 0.18,-0.28 0.18,-0.49 0,-0.21 -0.06,-0.37 -0.18,-0.48 -0.12,-0.12 -0.3,-0.18 -0.54,-0.18l-0.7 0 0 1.32 0.7 0z"/>
<path id="_7" class="fil3" d="M52.24 4.83c0,0.31 -0.07,0.59 -0.21,0.83 -0.14,0.26 -0.35,0.46 -0.64,0.62 -0.3,0.15 -0.66,0.23 -1.08,0.23l-0.8 0 0 1.88 -1.27 0 0 -5.25 2.07 0c0.42,0 0.77,0.08 1.06,0.22 0.29,0.14 0.51,0.34 0.65,0.6 0.14,0.25 0.22,0.54 0.22,0.87zm-2.03 0.66c0.25,0 0.43,-0.06 0.55,-0.17 0.12,-0.11 0.18,-0.28 0.18,-0.49 0,-0.21 -0.06,-0.37 -0.18,-0.48 -0.12,-0.12 -0.3,-0.18 -0.55,-0.18l-0.7 0 0 1.32 0.7 0z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,44 +1,32 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#465FFF"/>
<g filter="url(#filter0_d_1884_16361)">
<path d="M8.42383 8.42152C8.42383 7.49135 9.17787 6.7373 10.108 6.7373C11.0382 6.7373 11.7922 7.49135 11.7922 8.42152V23.5794C11.7922 24.5096 11.0382 25.2636 10.108 25.2636C9.17787 25.2636 8.42383 24.5096 8.42383 23.5794V8.42152Z" fill="white"/>
</g>
<g filter="url(#filter1_d_1884_16361)">
<path d="M14.7422 15.1569C14.7422 14.2267 15.4962 13.4727 16.4264 13.4727C17.3566 13.4727 18.1106 14.2267 18.1106 15.1569V23.5779C18.1106 24.5081 17.3566 25.2621 16.4264 25.2621C15.4962 25.2621 14.7422 24.5081 14.7422 23.5779V15.1569Z" fill="white" fill-opacity="0.9" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter2_d_1884_16361)">
<path d="M21.0547 10.9459C21.0547 10.0158 21.8087 9.26172 22.7389 9.26172C23.6691 9.26172 24.4231 10.0158 24.4231 10.9459V23.5775C24.4231 24.5077 23.6691 25.2617 22.7389 25.2617C21.8087 25.2617 21.0547 24.5077 21.0547 23.5775V10.9459Z" fill="white" fill-opacity="0.7" shape-rendering="crispEdges"/>
</g>
<defs>
<filter id="filter0_d_1884_16361" x="7.42383" y="6.2373" width="5.36841" height="20.5264" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1884_16361"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1884_16361" result="shape"/>
</filter>
<filter id="filter1_d_1884_16361" x="13.7422" y="12.9727" width="5.36841" height="13.7891" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1884_16361"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1884_16361" result="shape"/>
</filter>
<filter id="filter2_d_1884_16361" x="20.0547" y="8.76172" width="5.36841" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1884_16361"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1884_16361" result="shape"/>
</filter>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW (Evaluation Version) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="32px" height="32px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 2.26 2.26"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:none}
.fil2 {fill:#FEFEFE;fill-rule:nonzero}
.fil1 {fill:url(#id0)}
]]>
</style>
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="-0.4" y1="-1.31" x2="3.13" y2="4.35">
<stop offset="0" style="stop-opacity:1; stop-color:#364A94"/>
<stop offset="1" style="stop-opacity:1; stop-color:#7D7AB8"/>
</linearGradient>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_683990024">
<rect class="fil0" width="2.26" height="2.26"/>
<g>
<path class="fil1" d="M1.82 2.26l-1.38 0c-0.24,0 -0.44,-0.2 -0.44,-0.44l0 -1.38c0,-0.24 0.2,-0.44 0.44,-0.44l1.38 0c0.24,0 0.44,0.2 0.44,0.44l0 1.38c0,0.24 -0.2,0.44 -0.44,0.44z"/>
<path class="fil2" d="M1.19 1.35l-0.2 0 -0.36 -0.4 0.36 -0.4 0.2 0 -0.36 0.4 0.36 0.4zm-0.24 0l-0.16 0 -0.35 -0.4 0.35 -0.4 0.12 0 -0.32 0.4c0.12,0.12 0.24,0.28 0.36,0.4l0 0z"/>
<path class="fil2" d="M1.11 0.91l0.2 0 0.35 0.4 -0.35 0.43 -0.2 0 0.39 -0.39 -0.39 -0.44zm0.27 0l0.12 0 0.4 0.44 -0.4 0.39 -0.12 0 0.36 -0.43c-0.12,-0.12 -0.24,-0.24 -0.36,-0.4z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/images/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -1,53 +1,41 @@
<svg width="154" height="32" viewBox="0 0 154 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8.42105C0 3.77023 3.77023 0 8.42105 0H23.5789C28.2298 0 32 3.77023 32 8.42105V23.5789C32 28.2298 28.2298 32 23.5789 32H8.42105C3.77023 32 0 28.2298 0 23.5789V8.42105Z" fill="#465FFF"/>
<g filter="url(#filter0_d_1624_24907)">
<path d="M8.42383 8.42152C8.42383 7.49135 9.17787 6.7373 10.108 6.7373V6.7373C11.0382 6.7373 11.7922 7.49135 11.7922 8.42152V23.5794C11.7922 24.5096 11.0382 25.2636 10.108 25.2636V25.2636C9.17787 25.2636 8.42383 24.5096 8.42383 23.5794V8.42152Z" fill="white"/>
</g>
<g filter="url(#filter1_d_1624_24907)">
<path d="M14.7422 15.1569C14.7422 14.2267 15.4962 13.4727 16.4264 13.4727V13.4727C17.3566 13.4727 18.1106 14.2267 18.1106 15.1569V23.5779C18.1106 24.5081 17.3566 25.2621 16.4264 25.2621V25.2621C15.4962 25.2621 14.7422 24.5081 14.7422 23.5779V15.1569Z" fill="white" fill-opacity="0.9" shape-rendering="crispEdges"/>
</g>
<g filter="url(#filter2_d_1624_24907)">
<path d="M21.0547 10.9459C21.0547 10.0158 21.8087 9.26172 22.7389 9.26172V9.26172C23.6691 9.26172 24.4231 10.0158 24.4231 10.9459V23.5775C24.4231 24.5077 23.6691 25.2617 22.7389 25.2617V25.2617C21.8087 25.2617 21.0547 24.5077 21.0547 23.5775V10.9459Z" fill="white" fill-opacity="0.7" shape-rendering="crispEdges"/>
</g>
<path d="M44 10.1149H49.0885V24.6909H52.1321V10.1149H57.2206V7.30912H44V10.1149Z" fill="#101828"/>
<path d="M60.6175 25C62.4484 25 64.0416 24.1678 64.5409 22.9551L64.7549 24.6909H67.2992V17.5575C67.2992 14.2999 65.3494 12.5878 62.1869 12.5878C59.0006 12.5878 56.9081 14.2523 56.9081 16.7966H59.3811C59.3811 15.5601 60.3322 14.8468 62.0442 14.8468C63.5185 14.8468 64.4696 15.4888 64.4696 17.0819V17.3435L60.9504 17.605C58.1684 17.819 56.599 19.1744 56.599 21.3382C56.599 23.5495 58.1208 25 60.6175 25ZM61.5686 22.8124C60.2609 22.8124 59.5475 22.2893 59.5475 21.2193C59.5475 20.2682 60.2371 19.6737 62.0442 19.5073L64.4934 19.317V19.9353C64.4934 21.7424 63.352 22.8124 61.5686 22.8124Z" fill="#101828"/>
<path d="M71.5995 10.5905C72.5506 10.5905 73.3353 9.80581 73.3353 8.83091C73.3353 7.85601 72.5506 7.09511 71.5995 7.09511C70.6008 7.09511 69.8161 7.85601 69.8161 8.83091C69.8161 9.80581 70.6008 10.5905 71.5995 10.5905ZM70.149 24.6909H73.0499V12.9445H70.149V24.6909Z" fill="#101828"/>
<path d="M78.9718 24.6909V7H76.0946V24.6909H78.9718Z" fill="#101828"/>
<path d="M83.9408 24.6909L85.3437 20.6724H91.8352L93.2381 24.6909H96.4481L90.1707 7.30912H87.0558L80.7784 24.6909H83.9408ZM88.2209 12.4927C88.3873 12.0172 88.53 11.4941 88.6013 11.1612C88.6489 11.5178 88.8153 12.041 88.958 12.4927L90.9554 18.1044H86.2473L88.2209 12.4927Z" fill="#101828"/>
<path d="M102.493 25C104.276 25 105.798 24.2153 106.511 22.86L106.701 24.6909H109.364V7H106.487V14.4425C105.75 13.2774 104.3 12.5878 102.659 12.5878C99.1161 12.5878 96.9761 15.2034 96.9761 18.8653C96.9761 22.5033 99.0923 25 102.493 25ZM103.135 22.3369C101.113 22.3369 99.877 20.8626 99.877 18.7701C99.877 16.6777 101.113 15.1797 103.135 15.1797C105.156 15.1797 106.463 16.6539 106.463 18.7701C106.463 20.8864 105.156 22.3369 103.135 22.3369Z" fill="#101828"/>
<path d="M115.289 24.6909V18.033C115.289 16.1308 116.406 15.2272 117.785 15.2272C119.164 15.2272 120.044 16.107 120.044 17.7477V24.6909H122.945V18.033C122.945 16.107 124.015 15.2034 125.418 15.2034C126.797 15.2034 127.701 16.0832 127.701 17.7715V24.6909H130.578V17.0106C130.578 14.2999 129.008 12.5878 126.155 12.5878C124.372 12.5878 122.993 13.4676 122.398 14.823C121.78 13.4676 120.543 12.5878 118.76 12.5878C117.072 12.5878 115.883 13.3487 115.289 14.3236L115.051 12.9445H112.388V24.6909H115.289Z" fill="#101828"/>
<path d="M134.876 10.5905C135.827 10.5905 136.612 9.80581 136.612 8.83091C136.612 7.85601 135.827 7.09511 134.876 7.09511C133.877 7.09511 133.093 7.85601 133.093 8.83091C133.093 9.80581 133.877 10.5905 134.876 10.5905ZM133.426 24.6909H136.327V12.9445H133.426V24.6909Z" fill="#101828"/>
<path d="M142.225 24.6909V18.3659C142.225 16.4637 143.318 15.2272 145.102 15.2272C146.6 15.2272 147.575 16.1783 147.575 18.1519V24.6909H150.476V17.4624C150.476 14.4188 148.954 12.5878 146.005 12.5878C144.412 12.5878 142.985 13.2774 142.248 14.4663L142.011 12.9445H139.324V24.6909H142.225Z" fill="#101828"/>
<defs>
<filter id="filter0_d_1624_24907" x="7.42383" y="6.2373" width="5.36841" height="20.5263" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1624_24907"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1624_24907" result="shape"/>
</filter>
<filter id="filter1_d_1624_24907" x="13.7422" y="12.9727" width="5.36841" height="13.7895" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1624_24907"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1624_24907" result="shape"/>
</filter>
<filter id="filter2_d_1624_24907" x="20.0547" y="8.76172" width="5.36841" height="18" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="0.5"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1624_24907"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1624_24907" result="shape"/>
</filter>
</defs>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW (Evaluation Version) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="154px" height="32px" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 52.24 10.85"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:none}
.fil3 {fill:black;fill-rule:nonzero}
.fil2 {fill:#FEFEFE;fill-rule:nonzero}
.fil1 {fill:url(#id0)}
]]>
</style>
<linearGradient id="id0" gradientUnits="userSpaceOnUse" x1="-1.9" y1="-6.28" x2="15.04" y2="20.95">
<stop offset="0" style="stop-opacity:1; stop-color:#364A94"/>
<stop offset="1" style="stop-opacity:1; stop-color:#7D7AB8"/>
</linearGradient>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_588111768">
<rect class="fil0" width="52.24" height="10.85"/>
<g>
<path class="fil1" d="M8.76 10.85l-6.67 0c-1.14,0 -2.09,-0.95 -2.09,-2.09l0 -6.67c0,-1.14 0.95,-2.09 2.09,-2.09l6.67 0c1.14,0 2.09,0.95 2.09,2.09l0 6.67c0,1.14 -0.95,2.09 -2.09,2.09z"/>
<path class="fil2" d="M5.71 6.47l-0.95 0 -1.71 -1.9 1.71 -1.9 0.95 0 -1.71 1.9 1.71 1.9zm-1.14 0l-0.76 0 -1.72 -1.9 1.72 -1.9 0.57 0 -1.52 1.9c0.57,0.57 1.14,1.33 1.71,1.9l0 0z"/>
<path class="fil2" d="M5.33 4.38l0.95 0 1.72 1.9 -1.72 2.1 -0.95 0 1.91 -1.91 -1.91 -2.09zm1.33 0l0.58 0 1.9 2.09 -1.9 1.91 -0.58 0 1.72 -2.1c-0.57,-0.57 -1.14,-1.14 -1.72,-1.9z"/>
</g>
<path class="fil3" d="M17 2.77c0.55,0 1.04,0.11 1.45,0.33 0.41,0.22 0.73,0.53 0.96,0.93 0.23,0.39 0.34,0.85 0.34,1.37 0,0.51 -0.11,0.97 -0.34,1.37 -0.23,0.39 -0.55,0.71 -0.96,0.92 -0.42,0.22 -0.9,0.33 -1.45,0.33l-1.97 0 0 -5.25 1.97 0zm-0.08 4.15c0.48,0 0.86,-0.14 1.13,-0.4 0.27,-0.26 0.4,-0.64 0.4,-1.12 0,-0.49 -0.13,-0.86 -0.4,-1.13 -0.27,-0.27 -0.65,-0.41 -1.13,-0.41l-0.61 0 0 3.06 0.61 0z"/>
<polygon id="_1" class="fil3" points="24.24,3.85 21.62,10.01 20.25,10.01 21.2,7.88 19.5,3.85 20.93,3.85 21.9,6.46 22.86,3.85 "/>
<path id="_2" class="fil3" d="M26.18 2.77c0.55,0 1.04,0.11 1.45,0.33 0.41,0.22 0.73,0.53 0.96,0.93 0.23,0.39 0.34,0.85 0.34,1.37 0,0.51 -0.11,0.97 -0.34,1.37 -0.23,0.39 -0.55,0.71 -0.96,0.92 -0.42,0.22 -0.91,0.33 -1.45,0.33l-1.97 0 0 -5.25 1.97 0zm-0.08 4.15c0.48,0 0.86,-0.14 1.12,-0.4 0.27,-0.26 0.41,-0.64 0.41,-1.12 0,-0.49 -0.14,-0.86 -0.41,-1.13 -0.26,-0.27 -0.64,-0.41 -1.12,-0.41l-0.61 0 0 3.06 0.61 0z"/>
<path id="_3" class="fil3" d="M33.09 5.87c0,0.12 -0.01,0.24 -0.02,0.37l-2.9 0c0.02,0.26 0.11,0.46 0.25,0.6 0.15,0.14 0.33,0.2 0.54,0.2 0.33,0 0.54,-0.13 0.67,-0.4l1.36 0c-0.07,0.28 -0.19,0.52 -0.38,0.74 -0.18,0.22 -0.4,0.39 -0.68,0.52 -0.28,0.12 -0.58,0.18 -0.92,0.18 -0.41,0 -0.77,-0.09 -1.09,-0.26 -0.32,-0.17 -0.57,-0.43 -0.75,-0.75 -0.18,-0.32 -0.27,-0.7 -0.27,-1.13 0,-0.44 0.09,-0.82 0.26,-1.14 0.18,-0.32 0.43,-0.58 0.75,-0.75 0.32,-0.17 0.69,-0.26 1.1,-0.26 0.4,0 0.76,0.08 1.08,0.25 0.31,0.17 0.56,0.41 0.73,0.73 0.18,0.31 0.27,0.68 0.27,1.1zm-1.31 -0.34c0,-0.22 -0.07,-0.4 -0.22,-0.52 -0.15,-0.13 -0.34,-0.2 -0.57,-0.2 -0.21,0 -0.39,0.06 -0.54,0.19 -0.14,0.13 -0.23,0.3 -0.27,0.53l1.6 0z"/>
<polygon id="_4" class="fil3" points="35.15,6.83 36.05,3.85 37.41,3.85 35.93,8.02 34.35,8.02 32.87,3.85 34.24,3.85 "/>
<path id="_5" class="fil3" d="M42.31 7.09l-1.96 0 -0.31 0.93 -1.34 0 1.9 -5.25 1.48 0 1.9 5.25 -1.35 0 -0.32 -0.93zm-0.33 -0.98l-0.65 -1.93 -0.64 1.93 1.29 0z"/>
<path id="_6" class="fil3" d="M48.06 4.46c0,0.31 -0.06,0.59 -0.21,0.83 -0.14,0.26 -0.35,0.46 -0.64,0.62 -0.29,0.15 -0.65,0.23 -1.08,0.23l-0.79 0 0 1.88 -1.28 0 0 -5.25 2.07 0c0.42,0 0.77,0.08 1.07,0.22 0.29,0.14 0.51,0.34 0.65,0.6 0.14,0.25 0.21,0.54 0.21,0.87zm-2.02 0.66c0.24,0 0.42,-0.06 0.54,-0.17 0.12,-0.11 0.18,-0.28 0.18,-0.49 0,-0.21 -0.06,-0.37 -0.18,-0.48 -0.12,-0.12 -0.3,-0.18 -0.54,-0.18l-0.7 0 0 1.32 0.7 0z"/>
<path id="_7" class="fil3" d="M52.24 4.46c0,0.31 -0.07,0.59 -0.21,0.83 -0.14,0.26 -0.35,0.46 -0.64,0.62 -0.3,0.15 -0.66,0.23 -1.08,0.23l-0.8 0 0 1.88 -1.27 0 0 -5.25 2.07 0c0.42,0 0.77,0.08 1.06,0.22 0.29,0.14 0.51,0.34 0.65,0.6 0.14,0.25 0.22,0.54 0.22,0.87zm-2.03 0.66c0.25,0 0.43,-0.06 0.55,-0.17 0.12,-0.11 0.18,-0.28 0.18,-0.49 0,-0.21 -0.06,-0.37 -0.18,-0.48 -0.12,-0.12 -0.3,-0.18 -0.55,-0.18l-0.7 0 0 1.32 0.7 0z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
public/images/og-share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -3,7 +3,22 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Laravel Key Generator | DyDev Admin</title>
<title>Laravel APP_KEY Generator | DyDev Admin</title>
<meta name="description" content="Generate a secure, random 32-byte Laravel APP_KEY for your .env file instantly. Secure client-side generation.">
<meta name="keywords" content="laravel key generator, app_key gen, secure key generator, laravel security">
<meta name="robots" content="noindex, nofollow">
<!-- Open Graph -->
<meta property="og:title" content="Laravel APP_KEY Generator | DyDev Admin">
<meta property="og:description" content="Quick and secure client-side Laravel key generation for your production environments.">
<meta property="og:type" content="website">
<meta property="og:image" content="/images/og-share.png">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Laravel APP_KEY Generator | DyDev Admin">
<meta name="twitter:description" content="Quick and secure client-side Laravel key generation for your production environments.">
<meta name="twitter:image" content="/images/og-share.png">
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
@@ -50,6 +65,16 @@
class="bg-gray-50 text-gray-800 transition-colors duration-300"
>
<div class="relative z-1 flex min-h-screen flex-col items-center justify-center overflow-hidden p-6">
<!-- Back to Home -->
<div class="w-full max-w-[500px] mb-4">
<a href="/" 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="mr-2" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor">
<path d="M12.7083 5L7.5 10.2083L12.7083 15.4167" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Back to home
</a>
</div>
<!-- Decoration -->
<div class="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
<svg class="h-full w-full" viewBox="0 0 100 100" preserveAspectRatio="none">

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

Some files were not shown because too many files have changed in this diff Show More