Deploy Ladill Merchant / deploy (push) Successful in 32s
Wire UserProfileMenu and shared Blade partial into topbar, mobile nav, and layouts so menu items match the account portal (Ladill Mail excluded). Co-authored-by: Cursor <cursoragent@cursor.com>
523 lines
36 KiB
PHP
523 lines
36 KiB
PHP
@php
|
|
$user = auth()->user();
|
|
$initials = collect(explode(' ', trim((string) $user?->name)))
|
|
->filter()
|
|
->take(2)
|
|
->map(fn ($part) => strtoupper(substr($part, 0, 1)))
|
|
->implode('');
|
|
|
|
$rcCartCount = 0;
|
|
$domainCartCount = 0;
|
|
$cartCount = 0;
|
|
$cartRoute = route('user.products.cart');
|
|
if ($user) {
|
|
$rcCartCount = \App\Models\RcServiceOrder::query()
|
|
->where('user_id', $user->id)
|
|
->whereIn('status', [
|
|
\App\Models\RcServiceOrder::STATUS_CART,
|
|
\App\Models\RcServiceOrder::STATUS_PENDING_PAYMENT,
|
|
])
|
|
->count();
|
|
|
|
$domainCartCount = \App\Models\DomainOrder::query()
|
|
->where('user_id', $user->id)
|
|
->whereIn('status', [
|
|
\App\Models\DomainOrder::STATUS_CART,
|
|
\App\Models\DomainOrder::STATUS_PENDING_PAYMENT,
|
|
])
|
|
->count();
|
|
|
|
$cartCount = $rcCartCount + $domainCartCount;
|
|
|
|
if ($rcCartCount === 0 && $domainCartCount > 0) {
|
|
$cartRoute = route('user.domains.cart');
|
|
}
|
|
}
|
|
@endphp
|
|
|
|
<header class="flex items-center justify-between h-16 border-b border-slate-200 bg-white px-6"
|
|
x-data="afiaChat({
|
|
chatUrl: {{ \Illuminate\Support\Js::from(route('user.ai.chat')) }},
|
|
csrfToken: {{ \Illuminate\Support\Js::from(csrf_token()) }},
|
|
currentPage: {{ \Illuminate\Support\Js::from(request()->path()) }},
|
|
})"
|
|
@keydown.escape.window="handleEscape()">
|
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
<button @click="sidebarOpen = !sidebarOpen" class="lg:hidden shrink-0 text-slate-500 hover:text-slate-700">
|
|
<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="M4 6h16M4 12h16M4 18h16"/></svg>
|
|
</button>
|
|
|
|
{{-- Search Bar --}}
|
|
<div class="relative hidden w-full max-w-md lg:block" x-data="topbarSearch()" @click.outside="open = false" @keydown.escape.window="open = false">
|
|
<div class="relative">
|
|
<svg class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
|
|
<input type="text"
|
|
x-model="query"
|
|
@focus="onFocus()"
|
|
@input.debounce.200ms="search()"
|
|
@keydown.arrow-down.prevent="moveDown()"
|
|
@keydown.arrow-up.prevent="moveUp()"
|
|
@keydown.enter.prevent="go()"
|
|
placeholder="Search domains, hosting, email, SMTP…"
|
|
class="w-full rounded-xl border border-slate-200 bg-slate-50 py-2 pl-9 pr-10 text-sm text-slate-700 placeholder-slate-400 transition focus:border-indigo-300 focus:bg-white focus:ring-2 focus:ring-indigo-100 focus:outline-none">
|
|
<kbd class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 rounded-md border border-slate-200 bg-white px-1.5 py-0.5 text-[10px] font-medium text-slate-400 sm:inline-block">/</kbd>
|
|
</div>
|
|
|
|
{{-- Results dropdown --}}
|
|
<div x-show="open && (results.length > 0 || (query.length >= 2 && !loading))"
|
|
x-cloak
|
|
x-transition.opacity.duration.150ms
|
|
class="absolute left-0 top-full z-30 mt-1.5 w-full overflow-hidden rounded-xl border border-slate-200 bg-white shadow-lg">
|
|
<template x-if="loading">
|
|
<div class="px-4 py-3 text-center text-xs text-slate-400">Searching…</div>
|
|
</template>
|
|
<template x-if="!loading && results.length === 0 && query.length >= 2">
|
|
<div class="px-4 py-3 text-center text-xs text-slate-500">No results for "<span x-text="query" class="font-medium"></span>"</div>
|
|
</template>
|
|
<template x-if="!loading && results.length > 0">
|
|
<ul class="max-h-72 overflow-y-auto py-1">
|
|
<template x-for="(item, idx) in results" :key="item.url">
|
|
<li>
|
|
<a :href="item.url"
|
|
:class="idx === active ? 'bg-indigo-50 text-indigo-700' : 'text-slate-700'"
|
|
@mouseenter="active = idx"
|
|
class="flex items-center gap-3 px-4 py-2.5 text-sm transition hover:bg-indigo-50">
|
|
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg"
|
|
:class="{
|
|
'bg-blue-100 text-blue-600': item.type === 'domain',
|
|
'bg-teal-100 text-teal-600': item.type === 'hosting',
|
|
'bg-sky-100 text-sky-600': item.type === 'email',
|
|
'bg-violet-100 text-violet-600': item.type === 'smtp',
|
|
'bg-amber-100 text-amber-600': item.type === 'ticket',
|
|
'bg-orange-100 text-orange-600': item.type === 'order',
|
|
'bg-slate-100 text-slate-600': !['domain','hosting','email','smtp','ticket','order'].includes(item.type),
|
|
}">
|
|
<template x-if="item.type === 'domain'">
|
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"/></svg>
|
|
</template>
|
|
<template x-if="item.type !== 'domain'">
|
|
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"/></svg>
|
|
</template>
|
|
</span>
|
|
<span class="min-w-0 flex-1">
|
|
<span class="block truncate font-medium leading-tight" x-text="item.title"></span>
|
|
<span class="block truncate text-xs text-slate-400" x-text="item.subtitle"></span>
|
|
</span>
|
|
</a>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<div class="relative hidden lg:block"
|
|
x-data="notificationDropdown({
|
|
unreadUrl: {{ \Illuminate\Support\Js::from(route('user.notifications.unread')) }},
|
|
markReadUrl: {{ \Illuminate\Support\Js::from(route('user.notifications.mark-read', ['id' => '__ID__'])) }},
|
|
markAllReadUrl: {{ \Illuminate\Support\Js::from(route('user.notifications.mark-all-read')) }},
|
|
indexUrl: {{ \Illuminate\Support\Js::from(route('user.notifications.index')) }},
|
|
csrfToken: {{ \Illuminate\Support\Js::from(csrf_token()) }},
|
|
})"
|
|
@click.outside="open = false">
|
|
<button type="button"
|
|
@click="toggle()"
|
|
class="relative inline-flex items-center justify-center rounded-full border border-slate-200 p-2 text-slate-600 transition hover:bg-slate-50"
|
|
aria-label="Notifications">
|
|
<svg class="h-5 w-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
|
</svg>
|
|
<span x-show="unreadCount > 0"
|
|
x-cloak
|
|
x-text="unreadCount > 9 ? '9+' : unreadCount"
|
|
class="absolute -right-1 -top-1 inline-flex min-w-5 items-center justify-center rounded-full bg-rose-500 px-1.5 py-0.5 text-[10px] font-semibold text-white"></span>
|
|
</button>
|
|
|
|
{{-- Notification dropdown --}}
|
|
<div x-show="open"
|
|
x-cloak
|
|
x-transition:enter="transition ease-out duration-150"
|
|
x-transition:enter-start="opacity-0 scale-95"
|
|
x-transition:enter-end="opacity-100 scale-100"
|
|
x-transition:leave="transition ease-in duration-100"
|
|
x-transition:leave-start="opacity-100 scale-100"
|
|
x-transition:leave-end="opacity-0 scale-95"
|
|
class="absolute right-0 z-50 mt-2 w-80 origin-top-right rounded-xl border border-slate-200 bg-white shadow-lg">
|
|
<div class="flex items-center justify-between border-b border-slate-100 px-4 py-3">
|
|
<h3 class="text-sm font-semibold text-slate-900">Notifications</h3>
|
|
<button type="button"
|
|
x-show="unreadCount > 0"
|
|
@click="markAllRead()"
|
|
class="text-xs font-medium text-indigo-600 hover:text-indigo-700">
|
|
Mark all read
|
|
</button>
|
|
</div>
|
|
|
|
<div class="max-h-80 overflow-y-auto">
|
|
<template x-if="loading">
|
|
<div class="px-4 py-6 text-center">
|
|
<svg class="mx-auto h-5 w-5 animate-spin text-slate-400" 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 12h4z"></path>
|
|
</svg>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!loading && notifications.length === 0">
|
|
<div class="px-4 py-8 text-center">
|
|
<svg class="mx-auto h-10 w-10 text-slate-300" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
|
</svg>
|
|
<p class="mt-2 text-sm text-slate-500">No notifications yet</p>
|
|
</div>
|
|
</template>
|
|
|
|
<template x-if="!loading && notifications.length > 0">
|
|
<div class="divide-y divide-slate-100">
|
|
<template x-for="notification in notifications" :key="notification.id">
|
|
<a :href="notification.url || '#'"
|
|
@click="markRead(notification.id)"
|
|
class="flex items-start gap-3 px-4 py-3 transition hover:bg-slate-50">
|
|
<span class="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full"
|
|
:class="getIconBg(notification.icon)">
|
|
<template x-if="notification.icon === 'website'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3M4.5 19.5h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15A2.25 2.25 0 0 0 2.25 6.75v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>
|
|
</template>
|
|
<template x-if="notification.icon === 'domain'">
|
|
<span class="inline-flex" :class="getIconColor(notification.icon)">
|
|
{!! \App\Support\DomainGlobeIcon::svg('h-4 w-4') !!}
|
|
</span>
|
|
</template>
|
|
<template x-if="notification.icon === 'hosting'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"/></svg>
|
|
</template>
|
|
<template x-if="notification.icon === 'email'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>
|
|
</template>
|
|
<template x-if="notification.icon === 'billing'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 0 0 2.25-2.25V6.75A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25v10.5A2.25 2.25 0 0 0 4.5 19.5Z"/></svg>
|
|
</template>
|
|
<template x-if="notification.icon === 'lead'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0"/></svg>
|
|
</template>
|
|
<template x-if="notification.icon === 'success'">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>
|
|
</template>
|
|
<template x-if="!['website','domain','hosting','email','billing','lead','success'].includes(notification.icon)">
|
|
<svg class="h-4 w-4" :class="getIconColor(notification.icon)" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9a6 6 0 1 0-12 0v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.08 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
|
</template>
|
|
</span>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-slate-900 truncate" x-text="notification.title"></p>
|
|
<p class="text-xs text-slate-500 line-clamp-2" x-text="notification.message"></p>
|
|
<p class="mt-1 text-[11px] text-slate-400" x-text="notification.created_at"></p>
|
|
</div>
|
|
</a>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<div class="border-t border-slate-100 px-4 py-2.5">
|
|
<a :href="indexUrl" class="block text-center text-xs font-medium text-slate-600 hover:text-slate-900">
|
|
View all notifications
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="hidden h-8 w-px bg-slate-200 lg:block"></div>
|
|
|
|
<a href="{{ $cartRoute }}"
|
|
class="hidden items-center gap-2 rounded-full border border-slate-200 px-3 py-1.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 lg:inline-flex">
|
|
<span>Cart</span>
|
|
@if ($cartCount > 0)
|
|
<span class="inline-flex min-w-5 items-center justify-center rounded-full bg-slate-900 px-1.5 py-0.5 text-[11px] font-semibold text-white">{{ $cartCount }}</span>
|
|
@endif
|
|
</a>
|
|
|
|
<div class="relative hidden lg:block" x-data="{ profileOpen: false }" @click.outside="profileOpen = false">
|
|
<button type="button"
|
|
@click="profileOpen = !profileOpen"
|
|
class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-2 py-1.5 text-slate-700 hover:bg-slate-50">
|
|
@if ($user?->avatarUrl())
|
|
<img src="{{ $user->avatarUrl() }}" alt="Avatar" class="h-8 w-8 rounded-full object-cover ring-1 ring-slate-200">
|
|
@else
|
|
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full bg-slate-900 text-xs font-semibold text-white">
|
|
{{ $initials !== '' ? $initials : 'U' }}
|
|
</span>
|
|
@endif
|
|
<svg class="h-4 w-4 text-slate-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m19 9-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div x-show="profileOpen" x-cloak x-transition @click.outside="profileOpen = false"
|
|
class="absolute right-0 z-50 mt-2 w-56 rounded-xl border border-slate-200 bg-white shadow-lg">
|
|
@include('partials.user-profile-menu', [
|
|
'items' => \App\Support\UserProfileMenu::items($user),
|
|
])
|
|
</div>
|
|
</div>
|
|
|
|
@include('partials.mobile-header-cart', [
|
|
'cartUrl' => $cartRoute,
|
|
'cartCount' => $cartCount,
|
|
])
|
|
|
|
<button type="button"
|
|
@click="openAfia()"
|
|
class="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 px-4 py-2 text-sm font-semibold text-white shadow-sm transition hover:opacity-95">
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 2.25c.414 0 .75.336.75.75a5.25 5.25 0 0 0 5.25 5.25.75.75 0 0 1 0 1.5A5.25 5.25 0 0 0 12.75 15a.75.75 0 0 1-1.5 0A5.25 5.25 0 0 0 6 9.75a.75.75 0 0 1 0-1.5A5.25 5.25 0 0 0 11.25 3a.75.75 0 0 1 .75-.75Zm6.75 11.25a.75.75 0 0 1 .75.75A2.25 2.25 0 0 0 21.75 16.5a.75.75 0 0 1 0 1.5A2.25 2.25 0 0 0 19.5 20.25a.75.75 0 0 1-1.5 0A2.25 2.25 0 0 0 15.75 18a.75.75 0 0 1 0-1.5A2.25 2.25 0 0 0 18 14.25a.75.75 0 0 1 .75-.75ZM5.25 14.25a.75.75 0 0 1 .75.75 3 3 0 0 0 3 3 .75.75 0 0 1 0 1.5 3 3 0 0 0-3 3 .75.75 0 0 1-1.5 0 3 3 0 0 0-3-3 .75.75 0 0 1 0-1.5 3 3 0 0 0 3-3 .75.75 0 0 1 .75-.75Z"/>
|
|
</svg>
|
|
<span>AI</span>
|
|
</button>
|
|
|
|
{{-- All-apps launcher (shared, config-driven; see resources/views/partials/launcher) --}}
|
|
@include('partials.launcher')
|
|
</div>
|
|
|
|
{{-- Afia backdrop --}}
|
|
<div x-show="afiaOpen"
|
|
style="display: none;"
|
|
x-transition.opacity.duration.200ms
|
|
class="fixed inset-0 z-40 bg-black/20 backdrop-blur-[2px]"
|
|
@click="closeAfia()"></div>
|
|
|
|
{{-- Afia slide-over panel --}}
|
|
<section x-show="afiaOpen"
|
|
style="display: none;"
|
|
x-transition:enter="transition transform ease-out duration-250"
|
|
x-transition:enter-start="translate-x-full"
|
|
x-transition:enter-end="translate-x-0"
|
|
x-transition:leave="transition transform ease-in duration-200"
|
|
x-transition:leave-start="translate-x-0"
|
|
x-transition:leave-end="translate-x-full"
|
|
class="fixed inset-y-0 right-0 z-50 flex w-full max-w-md flex-col bg-white shadow-2xl sm:rounded-l-2xl">
|
|
|
|
{{-- Panel header --}}
|
|
<div class="flex items-center justify-between px-5 py-3.5">
|
|
<div class="flex items-center gap-2.5">
|
|
<span class="relative flex h-8 w-8 shrink-0 items-center justify-center">
|
|
<svg viewBox="0 0 36 36" class="h-8 w-8" aria-hidden="true">
|
|
<defs>
|
|
<radialGradient id="afia-orb-h" cx="40%" cy="35%" r="60%">
|
|
<stop offset="0%" stop-color="#a5b4fc"/>
|
|
<stop offset="50%" stop-color="#6366f1"/>
|
|
<stop offset="100%" stop-color="#3730a3"/>
|
|
</radialGradient>
|
|
</defs>
|
|
<circle cx="18" cy="18" r="14" fill="url(#afia-orb-h)">
|
|
<animate attributeName="r" values="14;14.8;14" dur="3s" ease="ease-in-out" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="18" cy="18" r="11" fill="none" stroke="#c7d2fe" stroke-width=".6" opacity=".5">
|
|
<animate attributeName="r" values="11;12.5;11" dur="4s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".5;.15;.5" dur="4s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="18" cy="18" r="4" fill="white" opacity=".25">
|
|
<animate attributeName="r" values="4;5.5;4" dur="2.5s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".25;.1;.25" dur="2.5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle r="1.2" fill="#c7d2fe" opacity=".8">
|
|
<animateMotion dur="3s" repeatCount="indefinite" path="M18,8 A10,10 0 1,1 17.99,8" rotate="auto"/>
|
|
</circle>
|
|
<circle r=".8" fill="#e0e7ff" opacity=".6">
|
|
<animateMotion dur="4.5s" repeatCount="indefinite" path="M18,6 A12,12 0 1,0 18.01,6" rotate="auto"/>
|
|
</circle>
|
|
</svg>
|
|
<span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-white bg-emerald-400"></span>
|
|
</span>
|
|
<div class="leading-tight">
|
|
<p class="text-sm font-semibold text-slate-900">Afia</p>
|
|
<p class="text-[11px] text-slate-400">Online</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<a href="{{ route('user.ai.index') }}"
|
|
class="rounded-lg px-2.5 py-1.5 text-[11px] font-semibold text-slate-500 transition hover:bg-slate-100 hover:text-slate-700">
|
|
History
|
|
</a>
|
|
<button type="button"
|
|
@click="closeAfia()"
|
|
class="rounded-lg p-1.5 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600"
|
|
aria-label="Close">
|
|
<svg class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-px bg-slate-100"></div>
|
|
|
|
{{-- Chat body --}}
|
|
<div class="flex-1 overflow-y-auto" x-ref="afiaScroll">
|
|
{{-- Welcome card (shown only before conversation starts) --}}
|
|
<div x-show="afiaMessages.length <= 1" class="px-5 py-8">
|
|
<div class="text-center">
|
|
<span class="mx-auto flex h-20 w-20 items-center justify-center">
|
|
<svg viewBox="0 0 80 80" class="h-20 w-20" aria-hidden="true">
|
|
<defs>
|
|
<radialGradient id="afia-orb-w" cx="38%" cy="35%" r="55%">
|
|
<stop offset="0%" stop-color="#c7d2fe"/>
|
|
<stop offset="40%" stop-color="#818cf8"/>
|
|
<stop offset="100%" stop-color="#3730a3"/>
|
|
</radialGradient>
|
|
<filter id="afia-glow-w">
|
|
<feGaussianBlur stdDeviation="3" result="b"/>
|
|
<feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge>
|
|
</filter>
|
|
</defs>
|
|
<circle cx="40" cy="40" r="28" fill="url(#afia-orb-w)" filter="url(#afia-glow-w)">
|
|
<animate attributeName="r" values="28;30;28" dur="3s" ease="ease-in-out" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="40" cy="40" r="22" fill="none" stroke="#e0e7ff" stroke-width=".7" opacity=".4">
|
|
<animate attributeName="r" values="22;25;22" dur="4s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".4;.1;.4" dur="4s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="40" cy="40" r="34" fill="none" stroke="#c7d2fe" stroke-width=".4" opacity=".25">
|
|
<animate attributeName="r" values="34;37;34" dur="5s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".25;.05;.25" dur="5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="40" cy="40" r="8" fill="white" opacity=".18">
|
|
<animate attributeName="r" values="8;11;8" dur="2.5s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".18;.06;.18" dur="2.5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle r="2" fill="#c7d2fe" opacity=".9">
|
|
<animateMotion dur="3s" repeatCount="indefinite" path="M40,14 A26,26 0 1,1 39.99,14" rotate="auto"/>
|
|
</circle>
|
|
<circle r="1.5" fill="#e0e7ff" opacity=".7">
|
|
<animateMotion dur="5s" repeatCount="indefinite" path="M40,10 A30,30 0 1,0 40.01,10" rotate="auto"/>
|
|
</circle>
|
|
<circle r="1" fill="#a5b4fc" opacity=".5">
|
|
<animateMotion dur="7s" repeatCount="indefinite" path="M40,6 A34,34 0 1,1 39.99,6" rotate="auto"/>
|
|
</circle>
|
|
</svg>
|
|
</span>
|
|
<h3 class="mt-5 text-xl font-semibold text-slate-900">How can I help you?</h3>
|
|
<p class="mt-1.5 text-sm text-slate-400">I can help with websites, domains, hosting, email, and more.</p>
|
|
</div>
|
|
|
|
<div class="mt-6 space-y-2">
|
|
<template x-for="suggestion in afiaSuggestions" :key="suggestion">
|
|
<button type="button"
|
|
@click="useSuggestion(suggestion)"
|
|
:disabled="afiaLoading"
|
|
class="group flex w-full items-center gap-3 rounded-xl border border-slate-150 bg-white px-4 py-3 text-left text-[13px] font-medium text-slate-700 shadow-sm transition hover:border-indigo-200 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-50">
|
|
<span class="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg bg-indigo-50 text-indigo-500 transition group-hover:bg-indigo-100">
|
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5"/></svg>
|
|
</span>
|
|
<span x-text="suggestion"></span>
|
|
</button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Messages --}}
|
|
<div x-show="afiaMessages.length > 1" class="space-y-1 px-4 py-4">
|
|
<template x-for="(message, idx) in afiaMessages" :key="idx">
|
|
<div class="flex gap-2.5 py-1.5" :class="message.role === 'user' ? 'flex-row-reverse' : ''">
|
|
{{-- Avatar --}}
|
|
<template x-if="message.role === 'assistant'">
|
|
<span class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center">
|
|
<svg viewBox="0 0 24 24" class="h-6 w-6" aria-hidden="true">
|
|
<defs>
|
|
<radialGradient id="afia-orb-m" cx="40%" cy="35%" r="60%">
|
|
<stop offset="0%" stop-color="#a5b4fc"/>
|
|
<stop offset="50%" stop-color="#6366f1"/>
|
|
<stop offset="100%" stop-color="#3730a3"/>
|
|
</radialGradient>
|
|
</defs>
|
|
<circle cx="12" cy="12" r="10" fill="url(#afia-orb-m)">
|
|
<animate attributeName="r" values="10;10.6;10" dur="3s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="12" cy="12" r="3" fill="white" opacity=".2">
|
|
<animate attributeName="r" values="3;4;3" dur="2.5s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".2;.08;.2" dur="2.5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle r=".8" fill="#c7d2fe" opacity=".8">
|
|
<animateMotion dur="3s" repeatCount="indefinite" path="M12,4 A8,8 0 1,1 11.99,4"/>
|
|
</circle>
|
|
</svg>
|
|
</span>
|
|
</template>
|
|
|
|
{{-- Bubble --}}
|
|
<div class="max-w-[82%] rounded-2xl px-3.5 py-2.5 text-[13px] leading-relaxed"
|
|
:class="message.role === 'user'
|
|
? 'bg-slate-900 text-white rounded-br-md'
|
|
: 'bg-slate-50 text-slate-700 rounded-bl-md'">
|
|
<p class="whitespace-pre-line" x-text="message.text"></p>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
{{-- Typing indicator --}}
|
|
<div x-show="afiaLoading" class="flex gap-2.5 py-1.5">
|
|
<span class="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center">
|
|
<svg viewBox="0 0 24 24" class="h-6 w-6" aria-hidden="true">
|
|
<defs>
|
|
<radialGradient id="afia-orb-t" cx="40%" cy="35%" r="60%">
|
|
<stop offset="0%" stop-color="#a5b4fc"/>
|
|
<stop offset="50%" stop-color="#6366f1"/>
|
|
<stop offset="100%" stop-color="#3730a3"/>
|
|
</radialGradient>
|
|
</defs>
|
|
<circle cx="12" cy="12" r="10" fill="url(#afia-orb-t)">
|
|
<animate attributeName="r" values="10;10.6;10" dur="3s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle cx="12" cy="12" r="3" fill="white" opacity=".2">
|
|
<animate attributeName="r" values="3;4;3" dur="2.5s" repeatCount="indefinite"/>
|
|
<animate attributeName="opacity" values=".2;.08;.2" dur="2.5s" repeatCount="indefinite"/>
|
|
</circle>
|
|
<circle r=".8" fill="#c7d2fe" opacity=".8">
|
|
<animateMotion dur="3s" repeatCount="indefinite" path="M12,4 A8,8 0 1,1 11.99,4"/>
|
|
</circle>
|
|
</svg>
|
|
</span>
|
|
<div class="rounded-2xl rounded-bl-md bg-slate-50 px-4 py-3">
|
|
<div class="flex items-center gap-1">
|
|
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay: 0ms"></span>
|
|
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay: 150ms"></span>
|
|
<span class="h-1.5 w-1.5 animate-bounce rounded-full bg-slate-400" style="animation-delay: 300ms"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Error banner --}}
|
|
<div x-show="afiaError" x-cloak class="mx-4 mb-2 rounded-lg bg-rose-50 px-3 py-2 text-xs text-rose-600" x-text="afiaError"></div>
|
|
|
|
{{-- Input area --}}
|
|
<div class="border-t border-slate-100 bg-white px-4 pb-4 pt-3">
|
|
<form @submit.prevent="sendMessage()" class="flex items-end gap-2">
|
|
<div class="relative flex-1">
|
|
<input type="text"
|
|
x-ref="afiaInput"
|
|
x-model="afiaInput"
|
|
:disabled="afiaLoading"
|
|
placeholder="Message Afia..."
|
|
class="w-full rounded-xl border border-slate-200 bg-slate-50 px-4 py-2.5 pr-10 text-sm text-slate-700 placeholder-slate-400 transition focus:border-indigo-300 focus:bg-white focus:outline-none focus:ring-2 focus:ring-indigo-100/80">
|
|
</div>
|
|
<button type="submit"
|
|
:disabled="afiaLoading || !afiaInput.trim()"
|
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-slate-900 text-white transition hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-40"
|
|
aria-label="Send">
|
|
<svg x-show="!afiaLoading" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke-width="2.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18" />
|
|
</svg>
|
|
<svg x-show="afiaLoading" class="h-4 w-4 animate-spin" 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 12h4z"></path>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
<p class="mt-2 text-center text-[10px] text-slate-400">Afia can make mistakes. Verify important info.</p>
|
|
</div>
|
|
</section>
|
|
</header>
|