Add wallet balance widget to the profile menu (after Billing)
Deploy Ladill QR Plus / deploy (push) Successful in 23s
Deploy Ladill QR Plus / deploy (push) Successful in 23s
Wallet balance peek rendered as a menu row directly under Billing, backed by a /wallet/balance endpoint (BillingClient) and guarded by Route::has. Also opens Afia via a direct window event for reliability. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b9c988df9d
commit
93a78cdba5
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Services\Billing\BillingClient;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight wallet balance for the avatar dropdown widget.
|
||||||
|
*/
|
||||||
|
class WalletBalanceController extends Controller
|
||||||
|
{
|
||||||
|
public function balance(Request $request, BillingClient $billing): JsonResponse
|
||||||
|
{
|
||||||
|
$publicId = (string) $request->user()->public_id;
|
||||||
|
|
||||||
|
$minor = Cache::remember("wallet_balance:{$publicId}", now()->addSeconds(30), function () use ($billing, $publicId) {
|
||||||
|
try {
|
||||||
|
return $billing->balanceMinor($publicId);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if ($minor === null) {
|
||||||
|
return response()->json(['available' => false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$currency = (string) config('billing.currency', 'GHS');
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'available' => true,
|
||||||
|
'balance_minor' => $minor,
|
||||||
|
'currency' => $currency,
|
||||||
|
'formatted' => $currency.' '.number_format($minor / 100, 2),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,11 @@ class UserProfileMenu
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (Route::has((string) config("billing.wallet_balance_route", "wallet.balance"))) {
|
||||||
|
$items[] = ["type" => "wallet"];
|
||||||
|
}
|
||||||
|
|
||||||
if (self::userIsAdmin($user)) {
|
if (self::userIsAdmin($user)) {
|
||||||
$adminHref = self::platformUrl('account', 'admin');
|
$adminHref = self::platformUrl('account', 'admin');
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ return [
|
|||||||
'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'),
|
'api_url' => env('BILLING_API_URL', 'https://ladill.com/api/billing'),
|
||||||
'api_key' => env('BILLING_API_KEY_QR'),
|
'api_key' => env('BILLING_API_KEY_QR'),
|
||||||
'service' => 'qr',
|
'service' => 'qr',
|
||||||
|
'wallet_balance_route' => 'wallet.balance',
|
||||||
|
'currency' => 'GHS',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
|
||||||
import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals';
|
import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals';
|
||||||
|
import { registerLadillSearchShortcut } from './ladill-search-shortcut';
|
||||||
|
|
||||||
registerLadillModalHelpers();
|
registerLadillModalHelpers();
|
||||||
|
registerLadillSearchShortcut();
|
||||||
|
|
||||||
|
|
||||||
import Alpine from 'alpinejs';
|
import Alpine from 'alpinejs';
|
||||||
@@ -195,6 +197,26 @@ Alpine.data('topbarSearch', (config = {}) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
// Wallet balance peek for the avatar dropdown.
|
||||||
|
Alpine.data('walletWidget', (config = {}) => ({
|
||||||
|
display: '…',
|
||||||
|
async load() {
|
||||||
|
if (! config.url) {
|
||||||
|
this.display = 'View wallet';
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await fetch(config.url, { headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' } });
|
||||||
|
const data = await res.json();
|
||||||
|
this.display = data.available ? data.formatted : 'View wallet';
|
||||||
|
} catch (e) {
|
||||||
|
this.display = 'View wallet';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
registerLadillConfirmStore(Alpine);
|
registerLadillConfirmStore(Alpine);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
function isEditableTarget(element) {
|
||||||
|
if (!(element instanceof HTMLElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = element.tagName;
|
||||||
|
|
||||||
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element.isContentEditable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVisibleInput(input) {
|
||||||
|
if (!(input instanceof HTMLElement)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const style = window.getComputedStyle(input);
|
||||||
|
|
||||||
|
return style.display !== 'none'
|
||||||
|
&& style.visibility !== 'hidden'
|
||||||
|
&& style.opacity !== '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSearchInput() {
|
||||||
|
const marked = [...document.querySelectorAll('[data-ladill-search-input]')];
|
||||||
|
const visibleMarked = marked.find(isVisibleInput);
|
||||||
|
|
||||||
|
if (visibleMarked) {
|
||||||
|
return visibleMarked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (marked.length > 0) {
|
||||||
|
return marked[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.querySelector('[x-data*="topbarSearch"] input');
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSearchInput() {
|
||||||
|
const input = findSearchInput();
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
const fallbackUrl = document.body?.dataset?.ladillSearchUrl;
|
||||||
|
|
||||||
|
if (fallbackUrl) {
|
||||||
|
window.location.href = fallbackUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVisibleInput(input)) {
|
||||||
|
const fallbackUrl = document.body?.dataset?.ladillSearchUrl;
|
||||||
|
|
||||||
|
if (fallbackUrl) {
|
||||||
|
window.location.href = fallbackUrl;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
if (input instanceof HTMLInputElement && input.type === 'text') {
|
||||||
|
input.select();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerLadillSearchShortcut() {
|
||||||
|
if (window.__ladillSearchShortcutRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__ladillSearchShortcutRegistered = true;
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key !== '/' && event.code !== 'Slash') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditableTarget(document.activeElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
focusSearchInput();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
</head>
|
</head>
|
||||||
<body class="font-sans antialiased bg-slate-100" x-data="{ sidebarOpen: false }">
|
<body class="font-sans antialiased bg-slate-100" x-data="{ sidebarOpen: false }" data-ladill-search-url="{{ route('qr.search') }}">
|
||||||
<div class="min-h-screen max-lg:min-h-dvh">
|
<div class="min-h-screen max-lg:min-h-dvh">
|
||||||
{{-- Mobile sidebar overlay --}}
|
{{-- Mobile sidebar overlay --}}
|
||||||
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
<div x-show="sidebarOpen" x-cloak class="fixed inset-0 z-30 bg-black/50 lg:hidden" @click="sidebarOpen = false"></div>
|
||||||
@@ -254,14 +254,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global "/" shortcut to focus search
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === '/' && !['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName) && !document.activeElement.isContentEditable) {
|
|
||||||
e.preventDefault();
|
|
||||||
const input = document.querySelector('[x-data*="topbarSearch"] input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
@include('partials.afia')
|
@include('partials.afia')
|
||||||
@include('partials.sso-keepalive')
|
@include('partials.sso-keepalive')
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@if ($handler === 'openAfia')
|
@if ($handler === 'openAfia')
|
||||||
@click="openAfia()"
|
@click="openAfia()"
|
||||||
@else
|
@else
|
||||||
@click="$dispatch('afia-open')"
|
@click="window.dispatchEvent(new CustomEvent('afia-open'))"
|
||||||
@endif
|
@endif
|
||||||
@class([
|
@class([
|
||||||
'inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 text-sm font-semibold text-white shadow-sm transition hover:opacity-95',
|
'inline-flex shrink-0 items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-700 via-blue-600 to-emerald-400 text-sm font-semibold text-white shadow-sm transition hover:opacity-95',
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<div class="px-4 pb-3">
|
<div class="px-4 pb-3">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<svg class="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -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>
|
<svg class="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -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 x-ref="input"
|
<input data-ladill-search-input x-ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
x-model="query"
|
x-model="query"
|
||||||
@focus="onFocus()"
|
@focus="onFocus()"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<h1 class="text-2xl font-semibold text-slate-900">{{ $heading }}</h1>
|
<h1 class="text-2xl font-semibold text-slate-900">{{ $heading }}</h1>
|
||||||
<div class="relative mt-3">
|
<div class="relative mt-3">
|
||||||
<svg class="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -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>
|
<svg class="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -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 x-ref="input"
|
<input data-ladill-search-input x-ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
x-model="query"
|
x-model="query"
|
||||||
@focus="onFocus()"
|
@focus="onFocus()"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
@keydown.escape.window="open = false">
|
@keydown.escape.window="open = false">
|
||||||
<div class="relative">
|
<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>
|
<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"
|
<input data-ladill-search-input type="text"
|
||||||
x-model="query"
|
x-model="query"
|
||||||
@focus="onFocus()"
|
@focus="onFocus()"
|
||||||
@input.debounce.200ms="search()"
|
@input.debounce.200ms="search()"
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div x-show="profileOpen" x-cloak x-transition
|
<div x-show="profileOpen" x-cloak x-transition
|
||||||
class="absolute right-0 z-50 mt-2 w-56 rounded-xl border border-slate-200 bg-white shadow-lg">
|
class="absolute right-0 z-50 mt-2 w-64 rounded-xl border border-slate-200 bg-white shadow-lg">
|
||||||
@include('partials.user-profile-menu', [
|
@include('partials.user-profile-menu', [
|
||||||
'items' => \App\Support\UserProfileMenu::items($user),
|
'items' => \App\Support\UserProfileMenu::items($user),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
<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 hidden w-full max-w-md lg:block" x-data="topbarSearch()" @click.outside="open = false" @keydown.escape.window="open = false">
|
||||||
<div class="relative">
|
<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>
|
<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"
|
<input data-ladill-search-input type="text"
|
||||||
x-model="query"
|
x-model="query"
|
||||||
@focus="onFocus()"
|
@focus="onFocus()"
|
||||||
@input.debounce.200ms="search()"
|
@input.debounce.200ms="search()"
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div x-show="profileOpen" x-cloak x-transition @click.outside="profileOpen = false"
|
<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">
|
class="absolute right-0 z-50 mt-2 w-64 rounded-xl border border-slate-200 bg-white shadow-lg">
|
||||||
@include('partials.user-profile-menu', [
|
@include('partials.user-profile-menu', [
|
||||||
'items' => \App\Support\UserProfileMenu::items($user),
|
'items' => \App\Support\UserProfileMenu::items($user),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
@if ($onNavigate) @click="{{ $onNavigate }}" @endif>
|
@if ($onNavigate) @click="{{ $onNavigate }}" @endif>
|
||||||
{{ $item['label'] }}
|
{{ $item['label'] }}
|
||||||
</a>
|
</a>
|
||||||
|
@elseif (($item['type'] ?? '') === 'wallet')
|
||||||
|
@includeIf('partials.wallet-widget')
|
||||||
@elseif (($item['type'] ?? '') === 'logout')
|
@elseif (($item['type'] ?? '') === 'logout')
|
||||||
@if ($variant !== 'sheet')
|
@if ($variant !== 'sheet')
|
||||||
<div class="{{ $dividerClass }}"></div>
|
<div class="{{ $dividerClass }}"></div>
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{{-- Wallet balance peek (links to the account wallet on account.ladill.com). --}}
|
||||||
|
@php
|
||||||
|
$balanceRoute = (string) config('billing.wallet_balance_route', 'wallet.balance');
|
||||||
|
$balanceUrl = \Illuminate\Support\Facades\Route::has($balanceRoute) ? route($balanceRoute) : null;
|
||||||
|
@endphp
|
||||||
|
@if ($balanceUrl)
|
||||||
|
<a href="{{ ladill_account_url('/wallet') }}"
|
||||||
|
x-data="walletWidget({ url: {{ \Illuminate\Support\Js::from($balanceUrl) }} })" x-init="load()"
|
||||||
|
class="mx-1 flex items-center justify-between gap-3 rounded-xl border border-slate-100 bg-gradient-to-r from-indigo-50 to-slate-50 px-3 py-2.5 transition hover:border-indigo-200 hover:from-indigo-100/70">
|
||||||
|
<span class="flex items-center gap-2.5 min-w-0">
|
||||||
|
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-100 text-indigo-600">
|
||||||
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.6" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M21 12a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 1 3 7.5m18 4.5v3.75A2.25 2.25 0 0 1 18.75 18H5.25A2.25 2.25 0 0 1 3 15.75V7.5m18 4.5h-3.75a1.5 1.5 0 0 0 0 3H21M3 7.5A2.25 2.25 0 0 1 5.25 5.25h11.25A2.25 2.25 0 0 1 18.75 7.5"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span class="min-w-0">
|
||||||
|
<span class="block text-[11px] font-medium text-slate-500">Wallet balance</span>
|
||||||
|
<span class="block truncate text-sm font-semibold text-slate-900" x-text="display"></span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg class="h-4 w-4 shrink-0 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m9 5 7 7-7 7"/></svg>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Auth\SsoLoginController;
|
use App\Http\Controllers\Auth\SsoLoginController;
|
||||||
|
use App\Http\Controllers\WalletBalanceController;
|
||||||
use App\Http\Controllers\NotificationController;
|
use App\Http\Controllers\NotificationController;
|
||||||
use App\Http\Controllers\Public\QrScanController;
|
use App\Http\Controllers\Public\QrScanController;
|
||||||
use App\Http\Controllers\Qr\AccountController;
|
use App\Http\Controllers\Qr\AccountController;
|
||||||
@@ -34,6 +35,7 @@ Route::get('/q/{shortCode}/business-cover', [QrScanController::class, 'businessC
|
|||||||
Route::get('/q/{shortCode}/app-icon', [QrScanController::class, 'appIcon'])->name('qr.public.app.icon');
|
Route::get('/q/{shortCode}/app-icon', [QrScanController::class, 'appIcon'])->name('qr.public.app.icon');
|
||||||
|
|
||||||
Route::middleware(['auth', 'platform.session'])->group(function () {
|
Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||||
|
Route::get('/wallet/balance', [WalletBalanceController::class, 'balance'])->name('wallet.balance');
|
||||||
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
Route::get('/notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
||||||
Route::get('/notifications/unread', [NotificationController::class, 'unread'])->name('notifications.unread');
|
Route::get('/notifications/unread', [NotificationController::class, 'unread'])->name('notifications.unread');
|
||||||
Route::post('/notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read');
|
Route::post('/notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.mark-read');
|
||||||
|
|||||||
Reference in New Issue
Block a user