Update pricing UX, billing flows, and API rules

This commit is contained in:
Dwindi Ramadhana
2026-02-12 00:52:40 +07:00
parent cf065fab1e
commit a905256353
202 changed files with 22348 additions and 301 deletions

View File

@@ -0,0 +1,27 @@
<x-guest-layout>
<div class="mb-4 text-sm text-slate-600 dark:text-slate-300">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,25 @@
<x-guest-layout>
<div class="mb-4 text-sm text-slate-600 dark:text-slate-300">
{{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
</div>
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('password.email') }}">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Email Password Reset Link') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,53 @@
<x-guest-layout>
@section('title', 'Login - '.config('app.name', 'Dewemoji'))
<!-- Session Status -->
<x-auth-session-status class="mb-4" :status="session('status')" />
<form method="POST" action="{{ route('login') }}" class="space-y-5">
@csrf
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Remember Me -->
<div class="block mt-4">
<label for="remember_me" class="inline-flex items-center">
<input id="remember_me" type="checkbox" class="rounded border-slate-300 text-blue-600 shadow-sm focus:ring-blue-500/40 dark:border-slate-700 dark:bg-slate-900/70" name="remember">
<span class="ms-2 text-sm text-slate-600 dark:text-slate-300">{{ __('Remember me') }}</span>
</label>
</div>
<div class="flex flex-col gap-3">
<x-primary-button class="w-full justify-center">
{{ __('Log in') }}
</x-primary-button>
@if (Route::has('password.request'))
<a class="text-center text-sm text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white underline rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500/40" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
</div>
<div class="text-center text-sm text-slate-600 dark:text-slate-300">
Don't have an account?
<a class="font-semibold text-blue-600 hover:text-blue-500 dark:text-blue-300 dark:hover:text-blue-200" href="{{ route('register') }}">Register</a>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,53 @@
<x-guest-layout>
@section('title', 'Register - '.config('app.name', 'Dewemoji'))
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500/40" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,39 @@
<x-guest-layout>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<!-- Email Address -->
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full" type="password" name="password" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button>
{{ __('Reset Password') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,31 @@
<x-guest-layout>
<div class="mb-4 text-sm text-slate-600 dark:text-slate-300">
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500/40">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'force-white inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -0,0 +1,35 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-500 dark:text-red-400 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@@ -0,0 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block text-xs uppercase tracking-[0.2em] text-slate-500 dark:text-slate-300']) }}>
{{ $value ?? $slot }}
</label>

View File

@@ -0,0 +1,78 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
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"
>
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'force-white inline-flex items-center px-6 py-3 rounded-xl font-semibold text-xs uppercase tracking-widest transition ease-in-out duration-150 bg-slate-900 text-white hover:text-white focus:text-white active:text-white hover:bg-slate-800 focus:bg-slate-800 active:bg-slate-950 focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:ring-offset-2 focus:ring-offset-white dark:bg-white/10 dark:text-white dark:hover:bg-white/20 dark:focus:ring-offset-slate-900']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,3 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'rounded-xl border border-slate-300/80 bg-white/90 px-4 py-2 text-sm text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-brand-ocean/40 dark:border-white/10 dark:bg-slate-900/70 dark:text-slate-100 dark:placeholder-slate-500']) }}>

View File

@@ -0,0 +1,26 @@
{{-- LEGACY VIEW (NOT USED)
This file is the default Breeze dashboard and is NOT wired to any routes.
The active dashboard layout lives in resources/views/dashboard/app.blade.php
and pages in resources/views/dashboard/*.blade.php
--}}
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="mb-6 rounded-lg border border-amber-300 bg-amber-50 p-4 text-sm text-amber-900">
<strong>Legacy template:</strong> This view is not used by the current dashboard routes.
The live dashboard uses <code>resources/views/dashboard/app.blade.php</code>.
</div>
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,64 @@
@extends('dashboard.app')
@section('page_title', 'Audit Logs')
@section('page_subtitle', 'Track administrative actions and changes.')
@section('dashboard_content')
<div class="rounded-2xl glass-card p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Audit trail</div>
<div class="mt-2 text-lg font-semibold text-white">Recent admin actions</div>
</div>
<form method="GET" class="flex w-full flex-wrap gap-3 md:w-auto md:justify-end">
<input type="search" name="q" value="{{ $filters['q'] ?? '' }}" placeholder="Search email or action"
class="w-full md:w-72 rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface">
<select name="action" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All actions</option>
@foreach ($actions ?? [] as $action)
<option value="{{ $action }}" @selected(($filters['action'] ?? '') === $action)>{{ $action }}</option>
@endforeach
</select>
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Filter</button>
</form>
</div>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.15em] text-gray-400">
<tr>
<th class="py-3 pr-4">Admin</th>
<th class="py-3 pr-4">Action</th>
<th class="py-3 pr-4">IP</th>
<th class="py-3 pr-4">Time</th>
<th class="py-3 pr-4">Payload</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
@forelse ($logs ?? [] as $log)
<tr>
<td class="py-4 pr-4">
<div class="font-semibold text-white">{{ $log->admin_email ?? '—' }}</div>
<div class="text-xs text-gray-400">#{{ $log->admin_id ?? 'n/a' }}</div>
</td>
<td class="py-4 pr-4">{{ $log->action }}</td>
<td class="py-4 pr-4 text-xs">{{ $log->ip_address ?? '—' }}</td>
<td class="py-4 pr-4 text-xs">{{ $log->created_at?->toDateTimeString() ?? '—' }}</td>
<td class="py-4 pr-4 text-xs">
<pre class="whitespace-pre-wrap rounded-xl border border-slate-900/60 bg-slate-950 px-3 py-2 text-slate-100 dark:border-black/30 dark:bg-black/70">{{ json_encode($log->payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="py-6 text-center text-sm text-gray-400">No audit logs yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $logs->links('vendor.pagination.dashboard') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,145 @@
@extends('dashboard.app')
@section('page_title', 'Admin Pricing')
@section('page_subtitle', 'Manage plan pricing and rollout changes.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Plans', 'value' => number_format(($plans ?? collect())->count()), 'note' => 'Active tiers'],
['label' => 'Pending changes', 'value' => number_format(($changes ?? collect())->count()), 'note' => 'Latest snapshots'],
['label' => 'Currency', 'value' => optional(($plans ?? collect())->first())->currency ?? 'IDR', 'note' => 'Default'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<form method="POST" action="{{ route('dashboard.admin.pricing.update') }}" class="mt-8 grid gap-6 lg:grid-cols-3" data-loading-form>
@csrf
@forelse ($plans ?? [] as $plan)
<div class="rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold uppercase tracking-[0.2em] text-gray-400">{{ $plan->code }}</div>
<span class="rounded-full bg-slate-200 px-3 py-1 text-xs font-semibold text-slate-700 dark:bg-white/10 dark:text-gray-200">{{ $plan->status }}</span>
</div>
<div class="mt-4 grid gap-3 text-sm text-gray-300">
<label class="block">
Name
<input name="plans[{{ $loop->index }}][name]" value="{{ $plan->name }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<label class="block">
Code
<input name="plans[{{ $loop->index }}][code]" value="{{ $plan->code }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<div class="grid grid-cols-2 gap-3">
<label class="block">
IDR price
<input name="plans[{{ $loop->index }}][amount_idr]" type="number" value="{{ $plan->amount }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface">
</label>
<label class="block">
USD price
<input name="plans[{{ $loop->index }}][amount_usd]" type="number" step="0.01" value="{{ $plan->meta['prices']['USD'] ?? '' }}" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="e.g. 9.99">
</label>
</div>
<div class="grid grid-cols-2 gap-3">
<label class="block">
Period
<select name="plans[{{ $loop->index }}][period]" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
<option value="" @selected(empty($plan->period))>None</option>
<option value="month" @selected($plan->period === 'month')>Monthly</option>
<option value="year" @selected($plan->period === 'year')>Annual</option>
</select>
</label>
<label class="block">
Status
<select name="plans[{{ $loop->index }}][status]" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
<option value="active" @selected($plan->status === 'active')>Active</option>
<option value="inactive" @selected($plan->status === 'inactive')>Inactive</option>
</select>
</label>
</div>
<div class="rounded-xl border border-white/10 px-3 py-2 text-xs text-gray-400 theme-surface">
<div class="font-semibold text-gray-300">PayPal Plan IDs</div>
<div class="mt-2 space-y-1">
<div>Sandbox: <span class="text-gray-200">{{ $plan->meta['paypal']['sandbox']['plan']['id'] ?? '—' }}</span></div>
<div>Live: <span class="text-gray-200">{{ $plan->meta['paypal']['live']['plan']['id'] ?? '—' }}</span></div>
<div>Last sync: <span class="text-gray-200">{{ $plan->meta['paypal']['live']['plan']['synced_at'] ?? $plan->meta['paypal']['sandbox']['plan']['synced_at'] ?? '—' }}</span></div>
</div>
</div>
</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 bg-white/5 p-6 text-sm text-gray-400">
No pricing plans yet. Use reset to defaults to create base plans.
</div>
@endforelse
<div class="lg:col-span-3 flex flex-wrap gap-3">
<button class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors" data-loading-btn>Save pricing</button>
<button form="pricing-reset" class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors" data-loading-btn>Reset to defaults</button>
<button form="pricing-paypal-sync" class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors" data-loading-btn>Sync PayPal plans</button>
</div>
</form>
<form id="pricing-reset" method="POST" action="{{ route('dashboard.admin.pricing.reset') }}" data-loading-form>
@csrf
</form>
<form id="pricing-paypal-sync" method="POST" action="{{ route('dashboard.admin.pricing.paypal_sync') }}" data-loading-form>
@csrf
</form>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Change log</div>
<div class="mt-2 text-lg font-semibold text-white">Recent pricing updates</div>
</div>
<form method="POST" action="{{ route('dashboard.admin.pricing.snapshot') }}" data-loading-form>
@csrf
<button class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors" data-loading-btn>Create update</button>
</form>
</div>
<div class="mt-6 space-y-4 text-sm text-gray-300">
@forelse ($changes ?? [] as $change)
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<div>
<div class="font-semibold text-white">Pricing change #{{ $change->id }}</div>
<div class="text-xs text-gray-400">{{ $change->created_at?->toDateString() }} · {{ $change->admin_ref ?? 'system' }}</div>
</div>
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-200">Published</span>
</div>
@empty
<div class="text-sm text-gray-400">No pricing changes recorded yet.</div>
@endforelse
</div>
</div>
<script>
(() => {
const forms = document.querySelectorAll('[data-loading-form]');
const buttons = document.querySelectorAll('[data-loading-btn]');
const setBusy = (on) => {
buttons.forEach(btn => {
btn.disabled = on;
btn.dataset.label = btn.dataset.label || btn.textContent;
btn.textContent = on ? 'Processing…' : btn.dataset.label;
btn.classList.toggle('opacity-70', on);
btn.classList.toggle('cursor-wait', on);
});
};
forms.forEach(form => {
form.addEventListener('submit', () => setBusy(true));
});
})();
</script>
@endsection

View File

@@ -0,0 +1,93 @@
@extends('dashboard.app')
@section('page_title', 'Admin Settings')
@section('page_subtitle', 'System configuration and feature flags.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Billing mode', 'value' => ($settings['billing_mode'] ?? config('dewemoji.billing.mode')), 'note' => isset($settings['billing_mode']) ? 'DB' : 'Env'],
['label' => 'Rate limit', 'value' => config('dewemoji.rate_limit_enabled') ? 'On' : 'Off', 'note' => 'Global'],
['label' => 'Public access', 'value' => ((bool) ($settings['public_enforce'] ?? config('dewemoji.public_access.enforce_whitelist'))) ? 'Whitelist' : 'Open', 'note' => 'Origin policy'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Feature flags</div>
<div class="mt-2 text-lg font-semibold text-white">System toggles</div>
<div class="mt-6 space-y-4 text-sm text-gray-300">
@foreach ([
['label' => 'Maintenance mode', 'desc' => 'Temporarily block public API access', 'state' => ((bool) ($settings['maintenance_enabled'] ?? false)) ? 'On' : 'Off'],
['label' => 'Extension verification', 'desc' => 'Require GCM token for extension calls', 'state' => config('dewemoji.extension_verification.enabled') ? 'On' : 'Off'],
['label' => 'Metrics', 'desc' => 'Allow internal metrics endpoints', 'state' => config('dewemoji.metrics.enabled') ? 'On' : 'Off'],
] as $row)
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<div>
<div class="font-semibold text-white">{{ $row['label'] }}</div>
<div class="text-xs text-gray-400">{{ $row['desc'] }}</div>
</div>
<span class="rounded-full {{ $row['state'] === 'On' ? 'bg-emerald-100 text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-200' : 'bg-slate-200 text-slate-700 dark:bg-white/10 dark:text-gray-200' }} px-3 py-1 text-xs font-semibold">
{{ $row['state'] }}
</span>
</div>
@endforeach
</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Public access</div>
<div class="mt-2 text-lg font-semibold text-white">Allowlist configuration</div>
<form method="POST" action="{{ route('dashboard.admin.settings.update') }}" class="mt-5 space-y-4">
@csrf
<label class="block text-sm text-gray-300">
Billing mode
<select name="billing_mode" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-sm text-gray-200 theme-surface">
<option value="sandbox" @selected(($settings['billing_mode'] ?? config('dewemoji.billing.mode')) === 'sandbox')>Sandbox</option>
<option value="live" @selected(($settings['billing_mode'] ?? config('dewemoji.billing.mode')) === 'live')>Live</option>
</select>
</label>
<label class="flex items-center gap-2 text-sm text-gray-300">
<input type="checkbox" name="maintenance_enabled" value="1" class="rounded border-white/20 bg-transparent"
@checked((bool) ($settings['maintenance_enabled'] ?? false))>
Maintenance mode
</label>
<label class="flex items-center gap-2 text-sm text-gray-300">
<input type="checkbox" name="public_enforce" value="1" class="rounded border-white/20 bg-transparent"
@checked((bool) ($settings['public_enforce'] ?? config('dewemoji.public_access.enforce_whitelist')))>
Enforce whitelist
</label>
<label class="block text-sm text-gray-300">
Allowed origins
<textarea name="public_origins" rows="4" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface" placeholder="https://dewemoji.com, https://app.dewemoji.com">{{ isset($settings['public_origins']) ? implode(', ', (array) $settings['public_origins']) : '' }}</textarea>
</label>
<label class="block text-sm text-gray-300">
Extension IDs
<input name="public_extension_ids" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface" placeholder="chrome-extension-id, edge-extension-id"
value="{{ isset($settings['public_extension_ids']) ? implode(', ', (array) $settings['public_extension_ids']) : '' }}" />
</label>
<label class="block text-sm text-gray-300">
Hourly limit
<input name="public_hourly_limit" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface" placeholder="5000"
value="{{ $settings['public_hourly_limit'] ?? '' }}" />
</label>
<button type="submit" class="w-full rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors">
Save settings
</button>
<p class="text-xs text-gray-400">Changes update the settings store immediately.</p>
</form>
</div>
</div>
@endsection

View File

@@ -0,0 +1,65 @@
@extends('dashboard.app')
@section('page_title', 'Subscription Details')
@section('page_subtitle', 'Plan status, provider info, and timeline.')
@section('dashboard_content')
<a href="{{ route('dashboard.admin.subscriptions') }}" class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white">
<i data-lucide="arrow-left" class="w-4 h-4"></i><span>Back to subscriptions</span>
</a>
<div class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Subscriber</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $subscription->user?->name ?? '—' }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $subscription->user?->email ?? '—' }}</div>
@if ($subscription->user)
<a href="{{ route('dashboard.admin.users.show', $subscription->user->id) }}" class="mt-3 inline-flex items-center gap-2 text-xs text-brand-ocean hover:text-brand-oceanSoft">
<i data-lucide="external-link" class="w-3 h-3"></i><span>View user</span>
</a>
@endif
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Plan</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $subscription->plan }}</div>
<div class="mt-2 text-sm text-gray-400">Status: {{ $subscription->status }}</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Provider</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $subscription->provider ?? 'admin' }}</div>
<div class="mt-2 text-sm text-gray-400">Ref: {{ $subscription->provider_ref ?? '—' }}</div>
</div>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Timeline</div>
<div class="mt-2 text-lg font-semibold text-white">Subscription dates</div>
<div class="mt-4 space-y-3 text-sm text-gray-300">
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<span>Started</span>
<span class="text-gray-200">{{ $subscription->started_at?->toDateString() ?? '—' }}</span>
</div>
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<span>Expires</span>
<span class="text-gray-200">{{ $subscription->expires_at?->toDateString() ?? '—' }}</span>
</div>
<div class="flex items-center justify-between rounded-xl border border-white/10 px-4 py-3 theme-surface">
<span>Created</span>
<span class="text-gray-200">{{ $subscription->created_at?->toDateString() ?? '—' }}</span>
</div>
</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Actions</div>
<div class="mt-2 text-lg font-semibold text-white">Manage subscription</div>
<div class="mt-4 space-y-3 text-sm text-gray-300">
<form method="POST" action="{{ route('dashboard.admin.subscriptions.revoke') }}" class="space-y-3">
@csrf
<input type="hidden" name="subscription_id" value="{{ $subscription->id }}">
<button class="w-full rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Revoke subscription</button>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,196 @@
@extends('dashboard.app')
@section('page_title', 'Admin Subscriptions')
@section('page_subtitle', 'Grant, revoke, and audit Pro access.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Active', 'value' => number_format(\App\Models\Subscription::where('status', 'active')->count()), 'note' => 'Subscriptions'],
['label' => 'Revoked', 'value' => number_format(\App\Models\Subscription::where('status', 'revoked')->count()), 'note' => 'All time'],
['label' => 'Pending', 'value' => number_format(\App\Models\Subscription::where('status', 'pending')->count()), 'note' => 'Manual review'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Filters</div>
<div class="mt-2 text-lg font-semibold text-white">Refine subscriptions</div>
</div>
<form method="GET" class="flex w-full flex-wrap gap-3 md:w-auto md:justify-end">
<input type="hidden" name="sort" value="{{ $sort ?? 'id' }}">
<input type="hidden" name="dir" value="{{ $dir ?? 'desc' }}">
<input type="number" name="user_id" value="{{ $filters['user_id'] ?? '' }}" placeholder="User ID"
class="w-full md:w-32 rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface">
<input type="search" name="email" value="{{ $filters['email'] ?? '' }}" placeholder="User email"
class="w-full md:w-64 rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 theme-surface">
<select name="status" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All statuses</option>
<option value="active" @selected(($filters['status'] ?? '') === 'active')>Active</option>
<option value="pending" @selected(($filters['status'] ?? '') === 'pending')>Pending</option>
<option value="revoked" @selected(($filters['status'] ?? '') === 'revoked')>Revoked</option>
<option value="cancelled" @selected(($filters['status'] ?? '') === 'cancelled')>Cancelled</option>
<option value="suspended" @selected(($filters['status'] ?? '') === 'suspended')>Suspended</option>
</select>
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Filter</button>
</form>
</div>
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Grant access</div>
<div class="mt-2 text-lg font-semibold text-white">Create subscription</div>
<form method="POST" action="{{ route('dashboard.admin.subscriptions.grant') }}" class="mt-5 grid gap-4 text-sm text-gray-300">
@csrf
<div class="grid gap-3 md:grid-cols-2">
<label class="block">
User email
<input name="email" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="user@email.com">
</label>
<label class="block">
User ID
<input name="user_id" type="number" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="Optional">
</label>
</div>
<div class="grid gap-3 md:grid-cols-2">
<label class="block">
Plan
<input name="plan" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" value="personal">
</label>
<label class="block">
Status
<select name="status" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="revoked">Revoked</option>
</select>
</label>
</div>
<div class="grid gap-3 md:grid-cols-2">
<label class="block">
Started at
<input name="started_at" type="date" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
</label>
<label class="block">
Expires at
<input name="expires_at" type="date" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 theme-surface">
</label>
</div>
<button class="rounded-xl bg-white/10 border border-white/10 px-4 py-2 text-sm font-semibold text-white hover:bg-white/20 transition-colors">Grant</button>
</form>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Revoke access</div>
<div class="mt-2 text-lg font-semibold text-white">Cancel subscription</div>
<form method="POST" action="{{ route('dashboard.admin.subscriptions.revoke') }}" class="mt-5 grid gap-4 text-sm text-gray-300">
@csrf
<label class="block">
Subscription ID
<input name="subscription_id" type="number" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="Optional">
</label>
<label class="block">
User email
<input name="email" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="user@email.com">
</label>
<label class="block">
User ID
<input name="user_id" type="number" class="mt-2 w-full rounded-xl border border-white/10 px-3 py-2 text-gray-200 placeholder-gray-500 theme-surface" placeholder="Optional">
</label>
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Revoke</button>
</form>
</div>
</div>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Subscription list</div>
<div class="mt-2 text-lg font-semibold text-white">Recent subscriptions</div>
</div>
</div>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.15em] text-gray-400">
<tr>
@php
$sortParam = $sort ?? 'id';
$dirParam = $dir ?? 'desc';
$toggle = fn ($field) => ($sortParam === $field && $dirParam === 'asc') ? 'desc' : 'asc';
$sortUrl = fn ($field) => request()->fullUrlWithQuery(['sort' => $field, 'dir' => $toggle($field)]);
@endphp
<th class="py-3 pr-4">Subscriber</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('plan') }}" class="inline-flex items-center gap-1 hover:text-white">
Plan
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('status') }}" class="inline-flex items-center gap-1 hover:text-white">
Status
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('expires_at') }}" class="inline-flex items-center gap-1 hover:text-white">
Next billing
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
@forelse ($subscriptions ?? [] as $row)
<tr>
<td class="py-4 pr-4">
<div class="font-semibold text-white">{{ $row->user?->name ?? '—' }}</div>
<div class="text-xs text-gray-400">{{ $row->user?->email ?? '—' }}</div>
</td>
<td class="py-4 pr-4">{{ $row->plan }}</td>
<td class="py-4 pr-4">
@php
$inactive = in_array($row->status, ['revoked', 'cancelled', 'suspended'], true);
$pill = $inactive
? ['bg' => 'bg-rose-100 dark:bg-rose-500/20', 'text' => 'text-rose-800 dark:text-rose-200']
: ($row->status === 'pending'
? ['bg' => 'bg-amber-100 dark:bg-amber-500/20', 'text' => 'text-amber-800 dark:text-amber-200']
: ['bg' => 'bg-emerald-100 dark:bg-emerald-500/20', 'text' => 'text-emerald-800 dark:text-emerald-200']);
@endphp
<span class="rounded-full {{ $pill['bg'] }} px-3 py-1 text-xs font-semibold {{ $pill['text'] }}">
{{ $row->status }}
</span>
</td>
<td class="py-4 pr-4 text-xs">{{ $row->expires_at?->toDateString() ?? '—' }}</td>
<td class="py-4 pr-4 text-right">
<a href="{{ route('dashboard.admin.subscriptions.show', $row->id) }}" class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 hover:bg-white/5 transition-colors">View</a>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="py-6 text-center text-sm text-gray-400">No subscriptions found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $subscriptions->links('vendor.pagination.dashboard') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,88 @@
@extends('dashboard.app')
@section('page_title', 'User Details')
@section('page_subtitle', 'Profile, access, and subscription history.')
@section('dashboard_content')
<a href="{{ route('dashboard.admin.users') }}" class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white">
<i data-lucide="arrow-left" class="w-4 h-4"></i><span>Back to users</span>
</a>
<div class="mt-6 flex flex-wrap items-center justify-between gap-4">
<div class="text-sm text-gray-400">User ID: {{ $user->id }}</div>
<form method="POST" action="{{ route('dashboard.admin.users.delete', $user->id) }}" data-confirm="Delete this user? This removes their data and cannot be undone." data-confirm-title="Delete user" data-confirm-ok="Delete">
@csrf
@method('DELETE')
<button class="rounded-full border border-rose-200 px-4 py-2 text-xs font-semibold text-rose-700 hover:bg-rose-50 transition-colors dark:border-rose-500/40 dark:text-rose-200 dark:hover:bg-rose-500/10">Delete user</button>
</form>
</div>
<div class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Account</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $user->name ?? '—' }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $user->email }}</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Access</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $user->role ?? 'user' }}</div>
<div class="mt-2 text-sm text-gray-400">Tier: {{ $user->tier ?? 'free' }}</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Joined</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $user->created_at?->toDateString() ?? '—' }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $user->created_at?->diffForHumans() ?? '' }}</div>
</div>
</div>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Subscriptions</div>
<div class="mt-2 text-lg font-semibold text-white">Recent access records</div>
</div>
</div>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.15em] text-gray-400">
<tr>
<th class="py-3 pr-4">Plan</th>
<th class="py-3 pr-4">Status</th>
<th class="py-3 pr-4">Started</th>
<th class="py-3 pr-4">Expires</th>
<th class="py-3 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
@forelse ($subscriptions as $row)
<tr>
<td class="py-4 pr-4">{{ $row->plan }}</td>
<td class="py-4 pr-4">
@php
$inactive = in_array($row->status, ['revoked', 'cancelled', 'suspended'], true);
$pill = $inactive
? ['bg' => 'bg-rose-100 dark:bg-rose-500/20', 'text' => 'text-rose-800 dark:text-rose-200']
: ($row->status === 'pending'
? ['bg' => 'bg-amber-100 dark:bg-amber-500/20', 'text' => 'text-amber-800 dark:text-amber-200']
: ['bg' => 'bg-emerald-100 dark:bg-emerald-500/20', 'text' => 'text-emerald-800 dark:text-emerald-200']);
@endphp
<span class="rounded-full {{ $pill['bg'] }} px-3 py-1 text-xs font-semibold {{ $pill['text'] }}">
{{ $row->status }}
</span>
</td>
<td class="py-4 pr-4 text-xs">{{ $row->started_at?->toDateString() ?? '—' }}</td>
<td class="py-4 pr-4 text-xs">{{ $row->expires_at?->toDateString() ?? '—' }}</td>
<td class="py-4 pr-4 text-right">
<a href="{{ route('dashboard.admin.subscriptions.show', $row->id) }}" class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 hover:bg-white/5 transition-colors">View</a>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="py-6 text-center text-sm text-gray-400">No subscriptions found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@endsection

View File

@@ -0,0 +1,207 @@
@extends('dashboard.app')
@section('page_title', 'Admin Users')
@section('page_subtitle', 'Manage accounts, roles, and access tiers.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="mb-6 rounded-2xl border border-amber-300/40 bg-amber-400/10 px-4 py-3 text-sm text-amber-200">
{{ $errors->first() }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Total users', 'value' => number_format(\App\Models\User::count()), 'note' => 'All accounts'],
['label' => 'Personal tier', 'value' => number_format(\App\Models\User::where('tier', 'personal')->count()), 'note' => 'Active subscriptions'],
['label' => 'Admins', 'value' => number_format(\App\Models\User::where('role', 'admin')->count()), 'note' => 'Staff'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="mb-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Create</div>
<div class="mt-2 text-lg font-semibold text-white">Add new user</div>
</div>
<button id="toggle-create-user" type="button" class="rounded-full border border-white/10 px-4 py-2 text-xs font-semibold text-gray-200 hover:bg-white/5">
Show form
</button>
</div>
<form id="create-user-form" method="POST" action="{{ route('dashboard.admin.users.create') }}" class="mt-4 grid gap-3 md:grid-cols-2 hidden">
@csrf
<div>
<label class="text-xs uppercase tracking-[0.15em] text-gray-500">Name</label>
<input type="text" name="name" placeholder="Optional" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-ocean/40 theme-surface">
</div>
<div>
<label class="text-xs uppercase tracking-[0.15em] text-gray-500">Email</label>
<input type="email" name="email" required class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-ocean/40 theme-surface">
</div>
<div>
<label class="text-xs uppercase tracking-[0.15em] text-gray-500">Password</label>
<input type="password" name="password" minlength="8" required class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-ocean/40 theme-surface">
</div>
<div>
<label class="text-xs uppercase tracking-[0.15em] text-gray-500">Role</label>
<select name="role" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div>
<label class="text-xs uppercase tracking-[0.15em] text-gray-500">Tier</label>
<select name="tier" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="free">Free</option>
<option value="personal">Personal</option>
</select>
</div>
<div class="flex items-end">
<button class="w-full rounded-xl bg-brand-ocean text-white font-semibold px-4 py-2 text-sm">Create user</button>
</div>
</form>
</div>
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Directory</div>
<div class="mt-2 text-lg font-semibold text-white">User list</div>
</div>
<form method="GET" class="flex w-full flex-wrap gap-3 md:w-auto md:justify-end">
<input type="hidden" name="sort" value="{{ $sort ?? 'id' }}">
<input type="hidden" name="dir" value="{{ $dir ?? 'desc' }}">
<input type="search" name="q" value="{{ $filters['q'] ?? '' }}" placeholder="Search by email or name"
class="w-full md:w-72 rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-ocean/40 theme-surface">
<select name="tier" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All tiers</option>
<option value="free" @selected(($filters['tier'] ?? '') === 'free')>Free</option>
<option value="personal" @selected(($filters['tier'] ?? '') === 'personal')>Personal</option>
</select>
<select name="role" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All roles</option>
<option value="admin" @selected(($filters['role'] ?? '') === 'admin')>Admin</option>
<option value="user" @selected(($filters['role'] ?? '') === 'user')>User</option>
</select>
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Filter</button>
</form>
</div>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.15em] text-gray-400">
<tr>
@php
$sortParam = $sort ?? 'id';
$dirParam = $dir ?? 'desc';
$toggle = fn ($field) => ($sortParam === $field && $dirParam === 'asc') ? 'desc' : 'asc';
$sortUrl = fn ($field) => request()->fullUrlWithQuery(['sort' => $field, 'dir' => $toggle($field)]);
@endphp
<th class="py-3 pr-4">
<a href="{{ $sortUrl('name') }}" class="inline-flex items-center gap-1 hover:text-white">
User
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('role') }}" class="inline-flex items-center gap-1 hover:text-white">
Role
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('tier') }}" class="inline-flex items-center gap-1 hover:text-white">
Tier
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">Status</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('created_at') }}" class="inline-flex items-center gap-1 hover:text-white">
Joined
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
@forelse ($users ?? [] as $user)
<tr>
<td class="py-4 pr-4">
<div class="font-semibold text-white">{{ $user->name ?? '—' }}</div>
<div class="text-xs text-gray-400">{{ $user->email }}</div>
</td>
<td class="py-4 pr-4">{{ $user->role ?? 'user' }}</td>
<td class="py-4 pr-4">
<form method="POST" action="{{ route('dashboard.admin.users.tier') }}" class="flex items-center gap-2">
@csrf
<input type="hidden" name="user_id" value="{{ $user->id }}">
<select name="tier" class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 theme-surface">
<option value="free" @selected($user->tier === 'free')>Free</option>
<option value="personal" @selected($user->tier === 'personal')>Personal</option>
</select>
<button class="text-xs font-semibold text-brand-ocean hover:text-brand-oceanSoft">Save</button>
</form>
</td>
<td class="py-4 pr-4">
<span class="rounded-full bg-emerald-100 px-3 py-1 text-xs font-semibold text-emerald-800 dark:bg-emerald-500/20 dark:text-emerald-200">Active</span>
</td>
<td class="py-4 pr-4 text-xs">{{ $user->created_at?->toDateString() }}</td>
<td class="py-4 pr-4 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('dashboard.admin.users.show', $user->id) }}" class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 hover:bg-white/5 transition-colors">View</a>
<form method="POST" action="{{ route('dashboard.admin.users.delete', $user->id) }}" data-confirm="Delete this user? This removes their data and cannot be undone." data-confirm-title="Delete user" data-confirm-ok="Delete">
@csrf
@method('DELETE')
<button class="rounded-full border border-rose-200 px-3 py-1 text-xs font-semibold text-rose-700 hover:bg-rose-50 transition-colors dark:border-rose-500/40 dark:text-rose-200 dark:hover:bg-rose-500/10">Delete</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="py-6 text-center text-sm text-gray-400">No users found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $users->links('vendor.pagination.dashboard') }}
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const toggleBtn = document.getElementById('toggle-create-user');
const form = document.getElementById('create-user-form');
if (!toggleBtn || !form) return;
const setState = (open) => {
form.classList.toggle('hidden', !open);
toggleBtn.textContent = open ? 'Hide form' : 'Show form';
};
const hasErrors = @json($errors->any());
setState(hasErrors);
toggleBtn.addEventListener('click', () => {
setState(form.classList.contains('hidden'));
});
})();
</script>
@endpush

View File

@@ -0,0 +1,57 @@
@extends('dashboard.app')
@section('page_title', 'Webhook Detail')
@section('page_subtitle', 'Event payload, headers, and processing status.')
@section('dashboard_content')
<a href="{{ route('dashboard.admin.webhooks') }}" class="inline-flex items-center gap-2 text-sm text-gray-400 hover:text-white">
<i data-lucide="arrow-left" class="w-4 h-4"></i><span>Back to webhooks</span>
</a>
<div class="mt-6 grid gap-6 lg:grid-cols-3">
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Event</div>
<div class="mt-3 text-2xl font-semibold text-white">#{{ $event->id }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $event->event_type ?? 'event' }}</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Provider</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $event->provider }}</div>
<div class="mt-2 text-sm text-gray-400">Event ID: {{ $event->event_id ?? '—' }}</div>
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Status</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $event->status }}</div>
<div class="mt-2 text-sm text-gray-400">Received: {{ $event->received_at?->toDateTimeString() ?? $event->created_at?->toDateTimeString() }}</div>
</div>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<div class="rounded-2xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Payload</div>
<div class="mt-2 text-lg font-semibold text-white">Event body</div>
</div>
<form method="POST" action="{{ route('dashboard.admin.webhooks.replay', $event->id) }}">
@csrf
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Replay</button>
</form>
</div>
<pre class="mt-4 rounded-xl border border-white/10 bg-black/40 p-4 text-xs text-gray-200 overflow-x-auto">{{ json_encode($event->payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
@if ($event->error)
<div class="mt-4 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700 dark:border-rose-500/30 dark:bg-rose-500/15 dark:text-rose-200">
{{ $event->error }}
</div>
@endif
</div>
<div class="rounded-2xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Headers</div>
<div class="mt-2 text-lg font-semibold text-white">Request metadata</div>
<pre class="mt-4 rounded-xl border border-white/10 bg-black/40 p-4 text-xs text-gray-200 overflow-x-auto">{{ json_encode($event->headers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) }}</pre>
<div class="mt-4 text-sm text-gray-300">
Processed: <span class="text-gray-200">{{ $event->processed_at?->toDateTimeString() ?? '—' }}</span>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,139 @@
@extends('dashboard.app')
@section('page_title', 'Admin Webhooks')
@section('page_subtitle', 'Monitor deliveries, failures, and replays.')
@section('dashboard_content')
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
<div class="grid gap-6 lg:grid-cols-3">
@foreach ([
['label' => 'Events (total)', 'value' => number_format(\App\Models\WebhookEvent::count()), 'note' => 'All providers'],
['label' => 'Failures', 'value' => number_format(\App\Models\WebhookEvent::where('status', 'error')->count()), 'note' => 'Needs replay'],
['label' => 'Providers', 'value' => number_format(\App\Models\WebhookEvent::distinct('provider')->count('provider')), 'note' => 'Configured'],
] as $card)
<div class="rounded-2xl glass-card p-5">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">{{ $card['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $card['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $card['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-8 rounded-2xl glass-card p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Delivery log</div>
<div class="mt-2 text-lg font-semibold text-white">Recent events</div>
</div>
<div class="flex flex-wrap gap-2">
<form method="GET" class="flex flex-wrap gap-2">
<input type="hidden" name="sort" value="{{ $sort ?? 'id' }}">
<input type="hidden" name="dir" value="{{ $dir ?? 'desc' }}">
<select name="provider" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All providers</option>
@foreach ($providers ?? [] as $provider)
<option value="{{ $provider }}" @selected(($filters['provider'] ?? '') === $provider)>{{ $provider }}</option>
@endforeach
</select>
<select name="status" class="rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="">All statuses</option>
<option value="processed" @selected(($filters['status'] ?? '') === 'processed')>Processed</option>
<option value="received" @selected(($filters['status'] ?? '') === 'received')>Received</option>
<option value="pending" @selected(($filters['status'] ?? '') === 'pending')>Pending</option>
<option value="error" @selected(($filters['status'] ?? '') === 'error')>Error</option>
</select>
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Filter</button>
</form>
<form method="POST" action="{{ route('dashboard.admin.webhooks.replay_failed') }}">
@csrf
<button class="rounded-xl border border-white/10 px-4 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">Replay failed</button>
</form>
</div>
</div>
<div class="mt-6 overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.15em] text-gray-400">
<tr>
@php
$sortParam = $sort ?? 'id';
$dirParam = $dir ?? 'desc';
$toggle = fn ($field) => ($sortParam === $field && $dirParam === 'asc') ? 'desc' : 'asc';
$sortUrl = fn ($field) => request()->fullUrlWithQuery(['sort' => $field, 'dir' => $toggle($field)]);
@endphp
<th class="py-3 pr-4">
<a href="{{ $sortUrl('id') }}" class="inline-flex items-center gap-1 hover:text-white">
Event
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('provider') }}" class="inline-flex items-center gap-1 hover:text-white">
Provider
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('status') }}" class="inline-flex items-center gap-1 hover:text-white">
Status
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4">
<a href="{{ $sortUrl('received_at') }}" class="inline-flex items-center gap-1 hover:text-white">
Time
<i data-lucide="arrow-up-down" class="w-3 h-3"></i>
</a>
</th>
<th class="py-3 pr-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
@forelse ($events ?? [] as $row)
<tr>
<td class="py-4 pr-4">
<div class="font-semibold text-white">#{{ $row->id }}</div>
<div class="text-xs text-gray-400">{{ $row->event_type ?? 'event' }}</div>
</td>
<td class="py-4 pr-4">{{ $row->provider }}</td>
<td class="py-4 pr-4">
@php
$pill = $row->status === 'error'
? ['bg' => 'bg-rose-100 dark:bg-rose-500/20', 'text' => 'text-rose-800 dark:text-rose-200']
: ($row->status === 'pending' || $row->status === 'received'
? ['bg' => 'bg-amber-100 dark:bg-amber-500/20', 'text' => 'text-amber-800 dark:text-amber-200']
: ['bg' => 'bg-emerald-100 dark:bg-emerald-500/20', 'text' => 'text-emerald-800 dark:text-emerald-200']);
@endphp
<span class="rounded-full {{ $pill['bg'] }} px-3 py-1 text-xs font-semibold {{ $pill['text'] }}">
{{ $row->status }}
</span>
</td>
<td class="py-4 pr-4 text-xs">{{ $row->received_at?->diffForHumans() ?? $row->created_at?->diffForHumans() }}</td>
<td class="py-4 pr-4 text-right">
<div class="flex items-center justify-end gap-2">
<a href="{{ route('dashboard.admin.webhooks.show', $row->id) }}" class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 hover:bg-white/5 transition-colors">View</a>
<form method="POST" action="{{ route('dashboard.admin.webhooks.replay', $row->id) }}">
@csrf
<button class="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold text-gray-200 hover:bg-white/5 transition-colors">Replay</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="py-6 text-center text-sm text-gray-400">No webhook events found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<div class="mt-6">
{{ $events->links('vendor.pagination.dashboard') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,286 @@
@extends('site.layout')
@php
$user = auth()->user();
$isAdmin = Gate::allows('admin');
$navUser = [
['label' => 'Overview', 'route' => 'dashboard.overview', 'icon' => 'layout-dashboard'],
['label' => 'My Keywords', 'route' => 'dashboard.keywords', 'icon' => 'hash'],
['label' => 'API Keys', 'route' => 'dashboard.api-keys', 'icon' => 'key-round'],
['label' => 'Billing', 'route' => 'dashboard.billing', 'icon' => 'badge-dollar-sign'],
['label' => 'Preferences', 'route' => 'dashboard.preferences', 'icon' => 'settings-2'],
['label' => 'Profile', 'route' => 'profile.edit', 'icon' => 'user-round'],
];
$navAdmin = [
['label' => 'Overview', 'route' => 'dashboard.overview', 'icon' => 'layout-dashboard'],
['label' => 'Users', 'route' => 'dashboard.admin.users', 'icon' => 'users'],
['label' => 'Subscriptions', 'route' => 'dashboard.admin.subscriptions', 'icon' => 'credit-card'],
['label' => 'Pricing', 'route' => 'dashboard.admin.pricing', 'icon' => 'badge-dollar-sign'],
['label' => 'Webhooks', 'route' => 'dashboard.admin.webhooks', 'icon' => 'webhook'],
['label' => 'Audit Logs', 'route' => 'dashboard.admin.audit_logs', 'icon' => 'list-checks'],
['label' => 'Settings', 'route' => 'dashboard.admin.settings', 'icon' => 'settings'],
['label' => 'Profile', 'route' => 'profile.edit', 'icon' => 'user-round'],
];
$nav = $isAdmin ? $navAdmin : $navUser;
$exportQuery = request()->query();
@endphp
@section('content')
<div class="flex h-screen w-full">
<aside class="hidden lg:flex w-20 lg:w-64 h-full glass-panel flex-col justify-between p-4 z-50 shrink-0">
<div>
<div class="flex items-center gap-3 px-2 mb-8 mt-2">
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-white to-gray-300 flex items-center justify-center shadow-lg shadow-white/20 shrink-0">
<img src="/assets/logo/logo-mark.svg" alt="Dewemoji logo" class="w-7 h-7 object-contain" />
</div>
<div class="hidden lg:block">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Dewemoji</div>
<div class="font-display font-bold text-lg tracking-tight">Dashboard</div>
</div>
</div>
<div class="px-2 mb-6">
<div class="rounded-2xl bg-[#0b0b0f]/90 theme-surface border border-white/5 px-4 py-3">
<div class="text-xs text-gray-400">
Signed in as {{ $isAdmin ? 'Admin' : 'User' }}
</div>
<div class="mt-1 font-semibold text-gray-200">{{ $user?->name ?? 'Guest' }}</div>
<div class="text-xs text-gray-400">{{ $user?->email ?? 'no-session' }}</div>
</div>
</div>
<nav class="space-y-1">
<div class="px-3 text-[11px] uppercase tracking-[0.3em] text-gray-500 mb-2">Menu</div>
@foreach ($nav as $item)
@php
$active = request()->routeIs($item['route']);
$classes = $active
? 'bg-white/10 text-brand-sun border border-white/5'
: 'text-gray-400 hover:text-white hover:bg-white/5 border border-transparent';
@endphp
<a href="{{ route($item['route']) }}" class="flex items-center gap-4 px-3 py-3 rounded-xl transition-all group {{ $classes }}">
<i data-lucide="{{ $item['icon'] }}" class="w-5 h-5 group-hover:scale-110 transition-transform"></i>
<span class="text-sm font-medium hidden lg:block">{{ $item['label'] }}</span>
</a>
@endforeach
</nav>
</div>
<div class="space-y-1">
<a href="{{ route('home') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="layout-grid" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Discover</span>
</a>
<a href="{{ route('support') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="life-buoy" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Support</span>
</a>
@auth
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="w-full flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="log-out" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Logout</span>
</button>
</form>
@endauth
</div>
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-6 shrink-0 sticky top-0 z-40">
<div class="w-full flex flex-col gap-4">
<div class="flex flex-col md:flex-row gap-4 md:items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.3em] text-gray-400">Workspace</div>
<h1 class="mt-2 text-3xl font-bold">{{ trim($__env->yieldContent('page_title')) ?: 'Dashboard Overview' }}</h1>
<p class="mt-1 text-sm text-gray-400">{{ trim($__env->yieldContent('page_subtitle')) ?: 'A shared layout with role-based navigation.' }}</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<div class="relative">
<button id="quick-action-btn" class="rounded-full bg-white/10 text-white border border-white/10 px-5 py-2 text-sm font-semibold hover:bg-white/20 transition-colors">
Quick action
</button>
<div id="quick-action-menu" class="hidden absolute right-0 mt-2 w-60 rounded-2xl border border-slate-200 bg-white p-2 shadow-xl dark:border-white/10 dark:bg-[#0b0b0f]/95 dark:backdrop-blur">
<div class="text-[11px] uppercase tracking-[0.3em] text-slate-500 px-3 py-2 dark:text-gray-500">Actions</div>
@if ($isAdmin)
<a href="{{ route('dashboard.admin.users') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="users" class="w-4 h-4"></i><span>Manage users</span>
</a>
<a href="{{ route('dashboard.admin.subscriptions') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="credit-card" class="w-4 h-4"></i><span>Grant subscription</span>
</a>
<a href="{{ route('dashboard.admin.webhooks') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="webhook" class="w-4 h-4"></i><span>Review webhooks</span>
</a>
<a href="{{ route('dashboard.admin.audit_logs') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="list-checks" class="w-4 h-4"></i><span>Audit logs</span>
</a>
<a href="{{ route('dashboard.admin.settings') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="settings" class="w-4 h-4"></i><span>Update settings</span>
</a>
<a href="{{ route('profile.edit') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="user-round" class="w-4 h-4"></i><span>Edit profile</span>
</a>
@else
<a href="{{ route('dashboard.keywords') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="hash" class="w-4 h-4"></i><span>My keywords</span>
</a>
<a href="{{ route('dashboard.keywords') }}#add" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="plus-circle" class="w-4 h-4"></i><span>Add keyword</span>
</a>
<a href="{{ route('dashboard.api-keys') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="key-round" class="w-4 h-4"></i><span>Manage API keys</span>
</a>
<a href="{{ route('dashboard.billing') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Billing overview</span>
</a>
<a href="{{ route('dashboard.preferences') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="settings-2" class="w-4 h-4"></i><span>Preferences</span>
</a>
<a href="{{ route('profile.edit') }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="user-round" class="w-4 h-4"></i><span>Edit profile</span>
</a>
@endif
</div>
</div>
@if ($isAdmin)
<div class="relative">
<button id="export-btn" class="rounded-full border border-white/10 px-5 py-2 text-sm font-semibold text-gray-200 hover:bg-white/5 transition-colors">
Export
</button>
<div id="export-menu" class="hidden absolute right-0 mt-2 w-56 rounded-2xl border border-slate-200 bg-white p-2 shadow-xl dark:border-white/10 dark:bg-[#0b0b0f]/95 dark:backdrop-blur">
<div class="text-[11px] uppercase tracking-[0.3em] text-slate-500 px-3 py-2 dark:text-gray-500">Export CSV</div>
<a href="{{ route('dashboard.admin.export', array_merge(['type' => 'users'], $exportQuery)) }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="users" class="w-4 h-4"></i><span>Users</span>
</a>
<a href="{{ route('dashboard.admin.export', array_merge(['type' => 'subscriptions'], $exportQuery)) }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="credit-card" class="w-4 h-4"></i><span>Subscriptions</span>
</a>
<a href="{{ route('dashboard.admin.export', array_merge(['type' => 'webhooks'], $exportQuery)) }}" class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm text-slate-700 hover:bg-slate-100 dark:text-gray-200 dark:hover:bg-white/10">
<i data-lucide="webhook" class="w-4 h-4"></i><span>Webhooks</span>
</a>
</div>
</div>
@endif
<button id="theme-toggle" class="w-10 h-10 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
</div>
</div>
</header>
<div class="flex-1 overflow-y-auto p-4 sm:p-6 pb-24 lg:pb-6">
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div class="mb-6 rounded-2xl border border-amber-300/40 bg-amber-400/10 px-4 py-4 text-sm text-amber-200">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<div class="font-semibold text-amber-100">Verify your email to unlock billing and API keys.</div>
<div class="mt-1 text-xs text-amber-200/80">No reminders will be sent automatically.</div>
</div>
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<button class="rounded-full border border-amber-200/40 px-4 py-2 text-xs font-semibold text-amber-100 hover:bg-amber-200/10 transition-colors">
Resend verification
</button>
</form>
</div>
</div>
@endif
@yield('dashboard_content')
</div>
</main>
</div>
@endsection
@section('mobile_nav')
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2 theme-nav">
<div class="grid grid-cols-4 gap-1 text-[11px]">
<a href="{{ route('dashboard.overview') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('dashboard.overview') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i><span>Overview</span>
</a>
<a href="{{ $isAdmin ? route('dashboard.admin.users') : route('dashboard.keywords') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('dashboard.admin.users') || request()->routeIs('dashboard.keywords') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="{{ $isAdmin ? 'users' : 'hash' }}" class="w-4 h-4"></i><span>{{ $isAdmin ? 'Users' : 'Keywords' }}</span>
</a>
<a href="{{ $isAdmin ? route('dashboard.admin.pricing') : route('dashboard.billing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('dashboard.admin.pricing') || request()->routeIs('dashboard.billing') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Plans</span>
</a>
<button id="more-menu-btn" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="more-horizontal" class="w-4 h-4"></i><span>More</span>
</button>
</div>
</nav>
@endsection
@section('more_menu')
<div id="more-menu-backdrop" class="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden"></div>
<div id="more-menu" class="lg:hidden fixed bottom-16 left-4 right-4 glass-panel rounded-2xl p-4 z-50 hidden">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">More</span>
<button id="more-menu-close" class="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="flex flex-col gap-2 text-sm">
@foreach ($nav as $item)
<a href="{{ route($item['route']) }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="{{ $item['icon'] }}" class="w-4 h-4"></i><span>{{ $item['label'] }}</span>
</a>
@endforeach
<a href="{{ route('home') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
</a>
@auth
<form method="POST" action="{{ route('logout') }}" class="flex">
@csrf
<button type="submit" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10 w-full text-left">
<i data-lucide="log-out" class="w-4 h-4"></i><span>Logout</span>
</button>
</form>
@endauth
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const quickBtn = document.getElementById('quick-action-btn');
const quickMenu = document.getElementById('quick-action-menu');
const exportBtn = document.getElementById('export-btn');
const exportMenu = document.getElementById('export-menu');
const closeMenus = () => {
if (quickMenu) quickMenu.classList.add('hidden');
if (exportMenu) exportMenu.classList.add('hidden');
};
if (quickBtn && quickMenu) {
quickBtn.addEventListener('click', (e) => {
e.stopPropagation();
const open = !quickMenu.classList.contains('hidden');
closeMenus();
if (!open) quickMenu.classList.remove('hidden');
});
}
if (exportBtn && exportMenu) {
exportBtn.addEventListener('click', (e) => {
e.stopPropagation();
const open = !exportMenu.classList.contains('hidden');
closeMenus();
if (!open) exportMenu.classList.remove('hidden');
});
}
document.addEventListener('click', closeMenus);
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMenus(); });
})();
</script>
@endpush

View File

@@ -0,0 +1,182 @@
@extends('dashboard.app')
@section('title', 'Dashboard')
@section('page_title', 'Dashboard Overview')
@section('page_subtitle', 'A shared layout with role-based navigation.')
@section('dashboard_content')
@php
$isAdmin = Gate::allows('admin');
$metrics = $overviewMetrics ?? [
'users_total' => 0,
'users_personal' => 0,
'subscriptions_active' => 0,
'subscriptions_total' => 0,
'webhook_total' => 0,
'webhook_errors' => 0,
];
$personalPct = $metrics['users_total'] > 0
? round(($metrics['users_personal'] / $metrics['users_total']) * 100)
: 0;
@endphp
<div class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Total users</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($metrics['users_total']) }}</div>
<div class="mt-2 text-sm text-gray-400">{{ number_format($metrics['users_personal']) }} personal users</div>
<div class="mt-4 h-2 w-full overflow-hidden rounded-full bg-white/10">
<div class="h-full rounded-full bg-brand-ocean" style="width: {{ $personalPct }}%"></div>
</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Active subscriptions</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($metrics['subscriptions_active']) }}</div>
<div class="mt-2 text-sm text-gray-400">{{ number_format($metrics['subscriptions_total']) }} total subscriptions</div>
<div class="mt-4 flex items-center gap-2 text-sm text-brand-sun">
<span class="inline-flex h-2 w-2 rounded-full bg-brand-sun"></span>
{{ $metrics['subscriptions_active'] > 0 ? 'Live access enabled' : 'No active subs' }}
</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Webhook events</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($metrics['webhook_total']) }}</div>
<div class="mt-2 text-sm text-gray-400">{{ number_format($metrics['webhook_errors']) }} failures</div>
<div class="mt-4 flex items-center gap-2 text-sm text-gray-400">
<span class="inline-flex h-2 w-2 rounded-full {{ $metrics['webhook_errors'] > 0 ? 'bg-amber-400' : 'bg-emerald-400' }}"></span>
{{ $metrics['webhook_errors'] > 0 ? 'Needs review' : 'All clear' }}
</div>
</div>
</div>
<div class="mt-8">
<div class="rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Insights</div>
<div class="mt-2 text-xl font-semibold text-white">Usage highlights</div>
</div>
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-300 theme-surface">Last 7 days</span>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl theme-surface border border-white/10 p-4 shadow-sm">
<div class="text-sm font-semibold text-gray-200">Top search</div>
<div class="mt-2 text-2xl font-semibold text-white">"smile"</div>
<div class="mt-1 text-xs text-gray-400">14% of queries</div>
</div>
<div class="rounded-2xl theme-surface border border-white/10 p-4 shadow-sm">
<div class="text-sm font-semibold text-gray-200">Most used category</div>
<div class="mt-2 text-2xl font-semibold text-white">Smileys</div>
<div class="mt-1 text-xs text-gray-400">8,214 views</div>
</div>
</div>
<div class="mt-6 rounded-2xl border border-white/10 theme-surface p-4 shadow-sm">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">New users</div>
<div class="mt-2 text-sm text-gray-300">Last 7 days</div>
<div class="mt-4">
<div id="overview-users-chart" class="h-36"></div>
</div>
</div>
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl border border-white/10 theme-surface p-4 shadow-sm">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Subscriptions</div>
<div class="mt-2 text-sm text-gray-300">Created last 7 days</div>
<div class="mt-3">
<div id="overview-subs-chart" class="h-28"></div>
</div>
</div>
<div class="rounded-2xl border border-white/10 theme-surface p-4 shadow-sm">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Webhooks</div>
<div class="mt-2 text-sm text-gray-300">Events last 7 days</div>
<div class="mt-3">
<div id="overview-webhooks-chart" class="h-28"></div>
</div>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const waitForApex = (cb) => {
if (window.ApexCharts) return cb();
let tries = 0;
const t = setInterval(() => {
if (window.ApexCharts) {
clearInterval(t);
cb();
return;
}
tries += 1;
if (tries > 50) {
clearInterval(t);
console.warn('ApexCharts not loaded.');
}
}, 100);
};
const root = document.documentElement;
const isDark = () => root.classList.contains('dark');
const palette = () => ({
text: isDark() ? '#e2e8f0' : '#334155',
grid: isDark() ? 'rgba(148,163,184,0.15)' : 'rgba(15,23,42,0.08)',
blue: isDark() ? '#60a5fa' : '#2563eb',
amber: isDark() ? '#f59e0b' : '#d97706',
mint: isDark() ? '#34d399' : '#10b981',
});
const labels = @json($chartLabels ?? []);
const users = @json($chartValues ?? []);
const subs = @json($chartSubs ?? []);
const webhooks = @json($chartWebhooks ?? []);
waitForApex(() => {
const colors = palette();
const base = {
chart: {
type: 'area',
sparkline: { enabled: true },
toolbar: { show: false },
animations: { enabled: true }
},
stroke: { curve: 'smooth', width: 2 },
fill: { opacity: 0.25 },
grid: { show: false },
tooltip: { theme: isDark() ? 'dark' : 'light' },
xaxis: { categories: labels },
yaxis: { show: false }
};
const usersEl = document.getElementById('overview-users-chart');
if (usersEl) {
new ApexCharts(usersEl, {
...base,
colors: [colors.blue],
series: [{ name: 'New users', data: users }]
}).render();
}
const subsEl = document.getElementById('overview-subs-chart');
if (subsEl) {
new ApexCharts(subsEl, {
...base,
colors: [colors.amber],
series: [{ name: 'Subscriptions', data: subs }]
}).render();
}
const webhooksEl = document.getElementById('overview-webhooks-chart');
if (webhooksEl) {
new ApexCharts(webhooksEl, {
...base,
colors: [colors.mint],
series: [{ name: 'Webhooks', data: webhooks }]
}).render();
}
});
})();
</script>
@endpush

View File

@@ -0,0 +1,164 @@
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Dashboard') · Dewemoji</title>
<link rel="icon" href="/favicon.ico">
<link rel="preload" href="/assets/fonts/PlusJakartaSans-Regular.ttf" as="font" type="font/ttf" crossorigin>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
display: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
},
colors: {
ink: '#0b0b10',
paper: '#f7f6f2',
dune: '#e7e1d6',
ocean: '#1d4ed8',
ember: '#f97316',
},
boxShadow: {
'panel': '0 18px 60px rgba(15, 23, 42, 0.12)',
}
}
}
};
</script>
<style>
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
</style>
</head>
<body class="h-full bg-paper text-slate-900">
@php
$user = auth()->user();
$isAdmin = Gate::allows('admin');
$navUser = [
['label' => 'Overview', 'href' => route('dashboard.overview')],
['label' => 'Usage', 'href' => route('dashboard.usage')],
['label' => 'API Keys', 'href' => route('dashboard.api-keys')],
['label' => 'Billing', 'href' => route('dashboard.billing')],
['label' => 'Preferences', 'href' => route('dashboard.preferences')],
];
$navAdmin = [
['label' => 'Overview', 'href' => route('dashboard.overview')],
['label' => 'Users', 'href' => route('dashboard.admin.users')],
['label' => 'Subscriptions', 'href' => route('dashboard.admin.subscriptions')],
['label' => 'Pricing', 'href' => route('dashboard.admin.pricing')],
['label' => 'Webhooks', 'href' => route('dashboard.admin.webhooks')],
['label' => 'Settings', 'href' => route('dashboard.admin.settings')],
];
$nav = $isAdmin ? $navAdmin : $navUser;
@endphp
<div class="min-h-screen bg-gradient-to-br from-[#f8f5ef] via-[#f6f7fb] to-[#e8eefc]">
<div class="mx-auto flex max-w-7xl flex-col lg:flex-row lg:items-start lg:gap-6">
<aside class="w-full lg:w-72 lg:shrink-0 lg:self-start">
<div class="p-6 lg:sticky lg:top-6">
<div class="rounded-3xl bg-white shadow-panel border border-[#e8e3d7] p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-slate-400">Dewemoji</div>
<div class="text-2xl font-semibold text-slate-900">Dashboard</div>
</div>
<span class="rounded-full bg-ink text-paper text-xs px-3 py-1">
{{ $isAdmin ? 'Admin' : 'User' }}
</span>
</div>
<div class="mt-6 rounded-2xl bg-[#0f172a] text-white p-4">
<div class="text-sm opacity-70">Signed in as</div>
<div class="mt-1 font-semibold">
{{ $user?->name ?? 'Guest' }}
</div>
<div class="text-xs opacity-60">
{{ $user?->email ?? 'no-session' }}
</div>
</div>
<nav class="mt-6">
<div class="text-xs font-semibold uppercase tracking-[0.25em] text-slate-400">Menu</div>
<ul class="mt-4 space-y-2">
@foreach ($nav as $item)
<li>
<a href="{{ $item['href'] }}"
class="flex items-center justify-between rounded-xl border border-transparent px-3 py-2 text-sm font-medium text-slate-700 transition hover:border-[#d9d3c7] hover:bg-[#f5f1ea]">
<span>{{ $item['label'] }}</span>
<span class="text-xs text-slate-400"></span>
</a>
</li>
@endforeach
</ul>
</nav>
<div class="mt-6 rounded-2xl border border-dashed border-[#d8d2c6] bg-[#faf7f1] p-4 text-sm text-slate-600">
<div class="font-semibold text-slate-800">Tip</div>
<div class="mt-1">Menu items are shared layout; only the sidebar changes by role.</div>
</div>
</div>
</div>
</aside>
<main class="flex-1 min-w-0 p-6 lg:p-10">
<div class="rounded-[32px] border border-[#e2deca] bg-white/70 shadow-panel backdrop-blur">
<div class="border-b border-[#ece7db] p-6 lg:p-8">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-slate-400">Workspace</div>
<h1 class="mt-2 text-3xl font-semibold text-slate-900">
@yield('page_title', 'Dashboard Overview')
</h1>
<p class="mt-2 text-sm text-slate-600">@yield('page_subtitle', 'Everything you need in one calm, focused place.') </p>
</div>
<div class="flex gap-2">
<button class="rounded-full bg-ink px-5 py-2 text-sm font-semibold text-paper">
Quick action
</button>
<button class="rounded-full border border-[#d9d3c7] px-5 py-2 text-sm font-semibold text-slate-700">
Export
</button>
</div>
</div>
</div>
<div class="p-6 lg:p-8">
@yield('content')
</div>
</div>
</main>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,42 @@
@extends('dashboard.app')
@section('title', $title)
@section('page_title', $title)
@section('page_subtitle', $subtitle)
@section('dashboard_content')
<div class="grid gap-6 lg:grid-cols-3">
@foreach ($kpis as $kpi)
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">{{ $kpi['label'] }}</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $kpi['value'] }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $kpi['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-8 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Summary</div>
<div class="mt-2 text-xl font-semibold text-white">{{ $summaryTitle }}</div>
</div>
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-300 theme-surface">{{ $summaryTag }}</span>
</div>
<p class="mt-4 text-sm text-gray-300">{{ $summaryBody }}</p>
<div class="mt-6 grid gap-4 md:grid-cols-2">
@foreach ($highlights as $highlight)
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-sm font-semibold text-gray-200">{{ $highlight['title'] }}</div>
<div class="mt-2 text-2xl font-semibold text-white">{{ $highlight['value'] }}</div>
<div class="mt-1 text-xs text-gray-400">{{ $highlight['note'] }}</div>
</div>
@endforeach
</div>
<div class="mt-6 rounded-2xl border border-dashed border-white/10 p-4 text-sm text-gray-300">
{{ $footerNote }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,117 @@
@extends('dashboard.app')
@section('title', 'API Keys')
@section('page_title', 'API Keys')
@section('page_subtitle', 'Create and manage access tokens for integrations.')
@section('dashboard_content')
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Your keys</div>
<div class="mt-2 text-xl font-semibold text-white">Manage API access</div>
</div>
@if ($canCreate)
<form method="POST" action="{{ route('dashboard.api-keys.create') }}" class="flex items-center gap-2">
@csrf
<input type="text" name="name" placeholder="Key name (optional)" class="rounded-full bg-white/5 border border-white/10 px-4 py-2 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean">
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm">Create key</button>
</form>
@endif
</div>
@if (!$canCreate)
<div class="mt-4 rounded-2xl border border-amber-400/30 bg-amber-400/10 p-4 text-sm text-amber-200">
API keys are available on the Personal plan. Upgrade to unlock API access.
</div>
@endif
@if ($newKey)
<div class="mt-6 rounded-2xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-200">
New key created. Copy it now; you wont be able to see it again.
<div class="mt-3 flex items-center gap-2">
<code class="rounded-lg bg-black/40 px-3 py-2 text-xs text-emerald-200">{{ $newKey }}</code>
<button type="button" data-copy-key="{{ $newKey }}" class="rounded-full border border-emerald-400/40 px-3 py-1 text-xs text-emerald-200 hover:bg-emerald-400/10">Copy</button>
</div>
</div>
@endif
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Prefix</th>
<th class="px-4 py-3 text-left">Name</th>
<th class="px-4 py-3 text-left">Created</th>
<th class="px-4 py-3 text-left">Last used</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
@forelse ($keys as $key)
<tr class="border-t border-white/5">
<td class="px-4 py-3 font-mono text-xs text-gray-400">{{ $key->key_prefix }}</td>
<td class="px-4 py-3 text-white">{{ $key->name ?? '—' }}</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $key->created_at?->toDateString() }}</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $key->last_used_at?->diffForHumans() ?? 'Never' }}</td>
<td class="px-4 py-3 text-right">
@if ($key->revoked_at)
<span class="text-xs text-gray-500">Revoked</span>
@else
<form method="POST" action="{{ route('dashboard.api-keys.revoke', $key->id) }}" class="inline" data-revoke-form>
@csrf
<button type="submit" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10">Revoke</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">No API keys yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="rounded-3xl glass-card p-6 space-y-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Tips</div>
<div class="mt-2 text-xl font-semibold text-white">Keep keys safe</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Use separate keys for apps and automate revocation if you suspect compromise.
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
API keys are required to access private keyword search results.
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
document.querySelectorAll('[data-copy-key]').forEach((btn) => {
btn.addEventListener('click', () => {
navigator.clipboard.writeText(btn.dataset.copyKey || '');
btn.textContent = 'Copied';
setTimeout(() => { btn.textContent = 'Copy'; }, 1200);
});
});
document.querySelectorAll('[data-revoke-form]').forEach((form) => {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const ok = await window.dewemojiConfirm('Revoke this API key? This cannot be undone.', {
title: 'Revoke API key',
okText: 'Revoke',
});
if (!ok) return;
form.submit();
});
});
})();
</script>
@endpush

View File

@@ -0,0 +1,127 @@
@extends('dashboard.app')
@section('title', 'Billing')
@section('page_title', 'Billing')
@section('page_subtitle', 'Subscription status and plan details.')
@section('dashboard_content')
@php
$user = $user ?? auth()->user();
$subscription = $subscription ?? null;
$hasSub = $subscription !== null;
$orders = $orders ?? collect();
$payments = $payments ?? collect();
@endphp
<div class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Plan</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ $hasSub ? ucfirst($subscription->plan ?? 'Personal') : 'Free' }}</div>
<div class="mt-2 text-sm text-gray-400">{{ $hasSub ? ucfirst($subscription->status ?? 'active') : 'No subscription' }}</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Renewal</div>
<div class="mt-3 text-2xl font-semibold text-white">
{{ $subscription?->next_renewal_at?->toDateString() ?? '—' }}
</div>
<div class="mt-2 text-sm text-gray-400">Next charge date</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Status</div>
<div class="mt-3 text-2xl font-semibold text-white">
{{ $subscription?->expires_at?->toDateString() ?? 'Active' }}
</div>
<div class="mt-2 text-sm text-gray-400">Access ends</div>
</div>
</div>
<div class="mt-8 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Billing summary</div>
<div class="mt-2 text-xl font-semibold text-white">Subscription details</div>
</div>
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-300 theme-surface">Current cycle</span>
</div>
<div class="mt-4 text-sm text-gray-300">
@if ($hasSub)
Provider: {{ $subscription->provider ?? 'direct' }} · Started {{ $subscription->started_at?->toDateString() }}
@else
You are on the free plan. Upgrade to unlock private keywords and sync.
@endif
</div>
<div class="mt-3 text-xs text-gray-400">
Downgrading to Free revokes any active API keys immediately.
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2">
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-sm font-semibold text-gray-200">Payment method</div>
<div class="mt-2 text-2xl font-semibold text-white">Card</div>
<div class="mt-1 text-xs text-gray-400">Coming soon</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-sm font-semibold text-gray-200">Invoices</div>
<div class="mt-2 text-2xl font-semibold text-white">0</div>
<div class="mt-1 text-xs text-gray-400">Billing history</div>
</div>
</div>
@if ($payments->count() > 0)
<div class="mt-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Recent payments</div>
<div class="mt-3 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Provider</th>
<th class="px-4 py-3 text-left">Plan</th>
<th class="px-4 py-3 text-left">Amount</th>
<th class="px-4 py-3 text-left">Status</th>
<th class="px-4 py-3 text-left">Created</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10">
@foreach ($payments as $payment)
@php
$status = $payment->status ?? 'pending';
$pill = $status === 'paid'
? ['bg' => 'bg-emerald-100 dark:bg-emerald-500/20', 'text' => 'text-emerald-800 dark:text-emerald-200']
: ($status === 'failed'
? ['bg' => 'bg-rose-100 dark:bg-rose-500/20', 'text' => 'text-rose-800 dark:text-rose-200']
: ['bg' => 'bg-amber-100 dark:bg-amber-500/20', 'text' => 'text-amber-800 dark:text-amber-200']);
@endphp
<tr>
<td class="px-4 py-3">{{ $payment->provider ?? '—' }}</td>
<td class="px-4 py-3">{{ $payment->plan_code ?? '—' }}</td>
<td class="px-4 py-3">{{ $payment->currency ?? 'USD' }} {{ number_format((float) ($payment->amount ?? 0), 2) }}</td>
<td class="px-4 py-3">
<span class="rounded-full {{ $pill['bg'] }} px-3 py-1 text-xs font-semibold {{ $pill['text'] }}">{{ $status }}</span>
</td>
<td class="px-4 py-3 text-xs text-gray-400">{{ $payment->created_at?->toDateString() ?? '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if ($payments->contains(fn ($payment) => in_array($payment->status, ['pending', 'failed'], true)))
<div class="mt-4 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-amber-400/30 dark:bg-amber-400/10 dark:text-amber-200">
Pending or failed payments need a new checkout. Start a fresh transaction from the pricing page.
</div>
<a href="{{ route('pricing') }}" class="mt-3 inline-flex items-center justify-center rounded-full border border-amber-300 px-4 py-2 text-xs font-semibold text-amber-800 hover:bg-amber-100 dark:border-amber-300/40 dark:text-amber-200 dark:hover:bg-amber-400/10">
Start new checkout
</a>
@endif
</div>
@endif
@if (!$hasSub || (string) $user?->tier !== 'personal')
<div class="mt-6 rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-brand-sun/30 dark:bg-brand-sun/10 dark:text-brand-sun">
Upgrade to Personal for private keywords and synced personalization.
</div>
<a href="{{ route('pricing') }}" class="mt-4 inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">
Upgrade to Personal
</a>
@endif
</div>
@endsection

View File

@@ -0,0 +1,251 @@
@extends('dashboard.app')
@section('title', 'My Keywords')
@section('page_title', 'My Keywords')
@section('page_subtitle', 'Manage your private emoji keywords and language tags.')
@section('dashboard_content')
@php
$user = $user ?? auth()->user();
$isPersonal = $user && (string) $user->tier === 'personal';
$freeLimit = $freeLimit ?? null;
$limitReached = $freeLimit !== null && $items->count() >= $freeLimit;
$emojiLookup = $emojiLookup ?? [];
@endphp
@if (session('status'))
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
{{ session('status') }}
</div>
@endif
@if ($errors->any())
<div class="mb-6 rounded-2xl border border-amber-300/40 bg-amber-400/10 px-4 py-3 text-sm text-amber-200">
{{ $errors->first() }}
</div>
@endif
<div class="rounded-3xl glass-card p-6">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Keyword library</div>
<div class="mt-2 text-xl font-semibold text-white">{{ $isPersonal ? 'Ready to personalize' : 'Free plan keywords' }}</div>
<p class="mt-2 text-sm text-gray-400">Add keywords to emojis to improve your personal search results.</p>
@if (!$isPersonal && $freeLimit)
<p class="mt-1 text-xs text-gray-500">Free plan limit: {{ $items->count() }} / {{ $freeLimit }} keywords.</p>
@endif
</div>
<div class="flex flex-wrap items-center gap-2">
<button id="add-keyword-btn" class="rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm {{ $limitReached ? 'opacity-50 cursor-not-allowed' : '' }}" {{ $limitReached ? 'disabled' : '' }}>
+ Add Keyword
</button>
<button id="import-btn" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5 {{ $limitReached ? 'opacity-50 cursor-not-allowed' : '' }}" {{ $limitReached ? 'disabled' : '' }}>
Import JSON
</button>
<a href="{{ route('dashboard.keywords.export') }}" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">
Export JSON
</a>
<div class="relative">
<input id="keyword-search" type="text" placeholder="Search keywords..." class="rounded-full bg-white/5 border border-white/10 px-4 py-2 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean">
</div>
</div>
</div>
@if (!$isPersonal && $freeLimit)
<div class="mt-6 rounded-2xl border border-brand-sun/30 bg-brand-sun/10 p-4 text-sm text-brand-sun">
Free plan includes up to {{ $freeLimit }} keywords total. Upgrade for unlimited keywords.
</div>
@endif
<div id="import-panel" class="mt-6 hidden rounded-2xl border border-white/10 bg-white/5 p-5">
<form method="POST" action="{{ route('dashboard.keywords.import') }}" enctype="multipart/form-data" class="grid gap-3 md:grid-cols-2">
@csrf
<div class="space-y-2">
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">JSON file</label>
<input type="file" name="file" accept="application/json" class="block w-full text-sm text-gray-200 file:mr-4 file:rounded-full file:border-0 file:bg-brand-ocean file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-brand-oceanSoft" {{ $limitReached ? 'disabled' : '' }}>
</div>
<div class="space-y-2">
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Or paste JSON</label>
<textarea name="payload" rows="4" class="w-full rounded-2xl bg-black/30 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder='[{"emoji_slug":"sparkles","keyword":"magic","lang":"en"}]'></textarea>
</div>
<div class="md:col-span-2 flex items-center justify-end gap-2">
<button type="button" id="import-cancel" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">Cancel</button>
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-5 py-2 text-sm" {{ $limitReached ? 'disabled' : '' }}>Import</button>
</div>
</form>
</div>
<div class="mt-6 overflow-hidden rounded-2xl border border-white/10">
<table class="min-w-full text-sm text-gray-300">
<thead class="bg-white/5 text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="px-4 py-3 text-left">Emoji</th>
<th class="px-4 py-3 text-left">Keyword</th>
<th class="px-4 py-3 text-left">Language</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody id="keyword-table">
@forelse ($items as $item)
<tr class="border-t border-white/5 keyword-row" data-keyword="{{ strtolower($item->keyword) }}" data-slug="{{ strtolower($item->emoji_slug) }}">
@php
$lookup = $emojiLookup[$item->emoji_slug] ?? null;
@endphp
<td class="px-4 py-3">
<div class="flex items-center gap-3">
<span class="text-xl">{{ $lookup['emoji'] ?? '⬚' }}</span>
<div>
<div class="text-sm text-white">{{ $lookup['name'] ?? $item->emoji_slug }}</div>
<div class="text-xs text-gray-500">{{ $item->emoji_slug }}</div>
</div>
</div>
</td>
<td class="px-4 py-3 font-semibold text-white">{{ $item->keyword }}</td>
<td class="px-4 py-3 text-xs uppercase tracking-[0.15em] text-gray-400">{{ $item->lang ?? 'und' }}</td>
<td class="px-4 py-3 text-right">
<button
type="button"
class="edit-btn rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10"
data-id="{{ $item->id }}"
data-emoji="{{ $item->emoji_slug }}"
data-keyword="{{ $item->keyword }}"
data-lang="{{ $item->lang }}"
>
Edit
</button>
<form method="POST" action="{{ route('dashboard.keywords.delete', $item->id) }}" class="inline">
@csrf
@method('DELETE')
<button type="submit" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/10">
Delete
</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-4 py-8 text-center text-sm text-gray-500">No keywords yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div id="keyword-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div class="relative z-10 w-full max-w-lg rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-white" id="keyword-modal-title">Add keyword</h3>
<button id="keyword-modal-close" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-gray-200">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form id="keyword-form" method="POST" action="{{ route('dashboard.keywords.store') }}" class="mt-4 grid gap-4">
@csrf
<input type="hidden" name="_method" id="keyword-form-method" value="POST">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Emoji Slug</label>
<input type="text" name="emoji_slug" id="keyword-emoji" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="sparkles" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" name="keyword" id="keyword-text" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="magic" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" name="lang" id="keyword-lang" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="en">
</div>
<div class="flex items-center justify-end gap-2">
<button type="button" id="keyword-cancel" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">Cancel</button>
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-5 py-2 text-sm">Save keyword</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const isPersonal = @json($isPersonal);
const limitReached = @json($limitReached);
const modal = document.getElementById('keyword-modal');
const modalTitle = document.getElementById('keyword-modal-title');
const openBtn = document.getElementById('add-keyword-btn');
const closeBtn = document.getElementById('keyword-modal-close');
const cancelBtn = document.getElementById('keyword-cancel');
const form = document.getElementById('keyword-form');
const methodEl = document.getElementById('keyword-form-method');
const emojiEl = document.getElementById('keyword-emoji');
const textEl = document.getElementById('keyword-text');
const langEl = document.getElementById('keyword-lang');
const searchEl = document.getElementById('keyword-search');
const importBtn = document.getElementById('import-btn');
const importPanel = document.getElementById('import-panel');
const importCancel = document.getElementById('import-cancel');
const openModal = (mode, data = {}) => {
if (mode === 'add' && limitReached) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
modalTitle.textContent = mode === 'edit' ? 'Edit keyword' : 'Add keyword';
form.action = mode === 'edit' ? `/dashboard/keywords/${data.id}` : '{{ route('dashboard.keywords.store') }}';
methodEl.value = mode === 'edit' ? 'PUT' : 'POST';
emojiEl.value = data.emoji || '';
textEl.value = data.keyword || '';
langEl.value = data.lang || '';
emojiEl.focus();
};
const closeModal = () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
};
openBtn?.addEventListener('click', () => openModal('add'));
closeBtn?.addEventListener('click', closeModal);
cancelBtn?.addEventListener('click', closeModal);
modal?.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
document.querySelectorAll('.edit-btn').forEach((btn) => {
btn.addEventListener('click', () => {
openModal('edit', {
id: btn.dataset.id,
emoji: btn.dataset.emoji,
keyword: btn.dataset.keyword,
lang: btn.dataset.lang,
});
});
});
if (searchEl) {
searchEl.addEventListener('input', () => {
const value = searchEl.value.trim().toLowerCase();
document.querySelectorAll('.keyword-row').forEach((row) => {
const match = row.dataset.keyword?.includes(value) || row.dataset.slug?.includes(value);
row.classList.toggle('hidden', value && !match);
});
});
}
importBtn?.addEventListener('click', () => {
if (!isPersonal) return;
importPanel.classList.remove('hidden');
});
importCancel?.addEventListener('click', () => {
importPanel.classList.add('hidden');
});
if (window.location.hash === '#add') {
openModal('add');
}
window.addEventListener('hashchange', () => {
if (window.location.hash === '#add') {
openModal('add');
}
});
})();
</script>
@endpush

View File

@@ -0,0 +1,93 @@
@extends('dashboard.app')
@section('title', 'Dashboard')
@section('page_title', 'Your Overview')
@section('page_subtitle', 'Quick stats and recent keyword activity.')
@section('dashboard_content')
@php
$user = auth()->user();
$isPersonal = $user && (string) $user->tier === 'personal';
$subscription = $subscription ?? null;
@endphp
<div class="grid gap-6 lg:grid-cols-4">
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Total keywords</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($totalKeywords ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">Across your emoji library</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Last 7 days</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($recentWeekCount ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">New keywords added</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">API keys</div>
<div class="mt-3 text-3xl font-semibold text-white">{{ number_format($apiKeyCount ?? 0) }}</div>
<div class="mt-2 text-sm text-gray-400">Active keys</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Plan</div>
<div class="mt-3 text-2xl font-semibold text-white">{{ $isPersonal ? 'Personal' : 'Free' }}</div>
<div class="mt-2 text-sm text-gray-400">
{{ $subscription?->status ? ucfirst($subscription->status) : 'No active subscription' }}
</div>
</div>
<div class="rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Synced devices</div>
<div class="mt-3 text-3xl font-semibold text-white">0</div>
<div class="mt-2 text-sm text-gray-400">Coming soon</div>
</div>
</div>
<div class="mt-8 grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Recent keywords</div>
<div class="mt-2 text-xl font-semibold text-white">Latest additions</div>
</div>
<a href="{{ route('dashboard.keywords') }}" class="rounded-full border border-white/10 px-4 py-2 text-xs text-gray-300 hover:bg-white/5">
Manage keywords
</a>
</div>
<div class="mt-6 grid gap-3 md:grid-cols-2">
@forelse ($recentKeywords ?? [] as $keyword)
<div class="rounded-2xl bg-white/5 border border-white/10 p-4">
<div class="text-xs uppercase tracking-[0.2em] text-gray-500">{{ $keyword->lang ?? 'und' }}</div>
<div class="mt-2 text-lg font-semibold text-white">{{ $keyword->keyword }}</div>
<div class="mt-1 text-xs text-gray-400">Emoji slug: {{ $keyword->emoji_slug }}</div>
</div>
@empty
<div class="rounded-2xl border border-dashed border-white/10 p-6 text-sm text-gray-400">
No keywords yet. Add your first keyword from an emoji detail page or the dashboard.
</div>
@endforelse
</div>
</div>
<div class="rounded-3xl glass-card p-6 flex flex-col gap-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Next steps</div>
<div class="mt-2 text-xl font-semibold text-white">Personalize faster</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Add keywords right on emoji detail pages to speed up your searches.
</div>
@if (!$isPersonal)
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800 dark:border-brand-sun/30 dark:bg-brand-sun/10 dark:text-brand-sun">
Upgrade to Personal to unlock private keywords and sync across devices.
</div>
<a href="{{ route('pricing') }}" class="inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">
Upgrade to Personal
</a>
@else
<a href="{{ route('dashboard.keywords') }}" class="inline-flex items-center justify-center rounded-full bg-brand-ocean text-white font-semibold px-4 py-2 text-sm">
Add keywords
</a>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,122 @@
@extends('dashboard.app')
@section('title', 'Preferences')
@section('page_title', 'Preferences')
@section('page_subtitle', 'Personalize your Dewemoji experience.')
@section('dashboard_content')
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 rounded-3xl glass-card p-6">
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Personal settings</div>
<div class="mt-2 text-xl font-semibold text-white">Preferences</div>
<p class="mt-2 text-sm text-gray-400">Saved locally for now. We will sync these to your account later.</p>
<form id="preferences-form" class="mt-6 grid gap-4 md:grid-cols-2">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Theme</label>
<select id="pref-theme" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<div class="mt-1 text-xs text-gray-400">Matches the global theme toggle.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Locale</label>
<select id="pref-locale" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="en-US">English (US)</option>
<option value="id-ID">Bahasa Indonesia</option>
</select>
<div class="mt-1 text-xs text-gray-400">Controls language defaults.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Tone lock</label>
<select id="pref-tone" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="off">Off</option>
<option value="on">Always on</option>
</select>
<div class="mt-1 text-xs text-gray-400">Prefer a skin tone when available.</div>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Insert mode</label>
<select id="pref-insert" class="mt-2 w-full rounded-xl border border-white/10 px-4 py-2 text-sm text-gray-200 theme-surface">
<option value="copy">Copy</option>
<option value="insert">Insert</option>
</select>
<div class="mt-1 text-xs text-gray-400">Default action for emoji pickers.</div>
</div>
</form>
<div id="pref-status" class="mt-4 text-sm text-gray-400"></div>
</div>
<div class="rounded-3xl glass-card p-6 space-y-4">
<div>
<div class="text-xs uppercase tracking-[0.25em] text-gray-400">Preferences</div>
<div class="mt-2 text-xl font-semibold text-white">Whats next</div>
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Sync preferences across devices and extensions once account settings are wired.
</div>
<div class="rounded-2xl bg-white/5 border border-white/10 p-4 text-sm text-gray-300">
Add tone presets and quick language switching.
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const storageKey = 'dewemoji_prefs';
const prefTheme = document.getElementById('pref-theme');
const prefLocale = document.getElementById('pref-locale');
const prefTone = document.getElementById('pref-tone');
const prefInsert = document.getElementById('pref-insert');
const status = document.getElementById('pref-status');
const readPrefs = () => {
try {
return JSON.parse(localStorage.getItem(storageKey) || '{}');
} catch {
return {};
}
};
const writePrefs = (prefs) => {
localStorage.setItem(storageKey, JSON.stringify(prefs));
if (status) {
status.textContent = 'Saved just now.';
setTimeout(() => { status.textContent = ''; }, 1600);
}
};
const applyThemePref = (value) => {
if (value === 'light') localStorage.setItem('dewemoji_theme', 'light');
if (value === 'dark') localStorage.setItem('dewemoji_theme', 'dark');
if (value === 'system') localStorage.removeItem('dewemoji_theme');
window.location.reload();
};
const prefs = readPrefs();
if (prefTheme) prefTheme.value = prefs.theme || 'system';
if (prefLocale) prefLocale.value = prefs.locale || 'en-US';
if (prefTone) prefTone.value = prefs.tone || 'off';
if (prefInsert) prefInsert.value = prefs.insert || 'copy';
[prefTheme, prefLocale, prefTone, prefInsert].forEach((el) => {
if (!el) return;
el.addEventListener('change', () => {
const next = {
theme: prefTheme?.value || 'system',
locale: prefLocale?.value || 'en-US',
tone: prefTone?.value || 'off',
insert: prefInsert?.value || 'copy',
};
writePrefs(next);
if (el === prefTheme) applyThemePref(next.theme);
});
});
})();
</script>
@endpush

View File

@@ -0,0 +1,25 @@
@extends('dashboard.app')
@section('title', 'Profile')
@section('page_title', 'Profile')
@section('page_subtitle', 'Update your account details and security settings.')
@section('dashboard_content')
@if (session('status') === 'profile-updated')
<div class="mb-6 rounded-2xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-700 dark:border-emerald-500/30 dark:bg-emerald-500/15 dark:text-emerald-200">
Profile updated.
</div>
@endif
<div class="grid gap-6">
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.update-profile-information-form')
</div>
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.update-password-form')
</div>
<div class="rounded-3xl glass-card p-6">
@include('profile.partials.delete-user-form')
</div>
</div>
@endsection

View File

@@ -0,0 +1,101 @@
@php
$logoUrl = rtrim(config('app.url', ''), '/') . '/assets/logo/logo-mark-128.png';
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dewemoji Password Reset</title>
</head>
<body style="margin:0; padding:0; background:#f5f7fb;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb; padding:32px 16px;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background:#ffffff; border-radius:24px; overflow:hidden; box-shadow:0 20px 50px rgba(15,23,42,0.10); border:1px solid #e6ebf5;">
<tr>
<td style="padding:26px 28px 10px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="width:40px; height:40px; border-radius:12px; background:#f1f5ff; text-align:center; border:1px solid #e2e8f0;">
<img src="{{ $logoUrl }}" alt="Dewemoji" width="40" height="40" style="display:block; border-radius:12px;" />
</td>
<td style="padding-left:12px; font-family:Arial, sans-serif; color:#0f172a; font-size:18px; font-weight:700;">
Dewemoji
</td>
</tr>
</table>
</td>
<td align="right" style="font-family:Arial, sans-serif; color:#6b7280; font-size:12px;">
Password reset
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:12px 28px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-radius:18px; padding:26px; border:1px solid #e2e8f0;">
<tr>
<td style="padding-bottom:14px;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="width:34px; height:34px; border-radius:10px; background:#e2e8f0; text-align:center;">
<span style="display:inline-block; font-size:18px; line-height:34px;">🔒</span>
</td>
<td style="padding-left:10px; font-family:Arial, sans-serif; color:#0f172a; font-size:22px; font-weight:700;">
Reset your password
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#475569; font-size:14px; line-height:1.7; padding-bottom:20px;">
We received a request to reset your Dewemoji password. Click the button below to choose a new one.
</td>
</tr>
<tr>
<td align="left" style="padding-bottom:18px;">
<a href="{{ $resetUrl }}" style="display:inline-block; padding:12px 20px; background:#111827; color:#ffffff; text-decoration:none; border-radius:999px; font-family:Arial, sans-serif; font-size:14px; font-weight:700;">
Reset Password
</a>
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#64748b; font-size:12px; line-height:1.6;">
If the button does not work, paste this link into your browser:
<br />
<span style="color:#2563eb;">{{ $resetUrl }}</span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:20px 28px 28px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#ffffff; border-radius:16px; padding:16px; border:1px dashed #e2e8f0;">
<tr>
<td style="font-family:Arial, sans-serif; color:#64748b; font-size:12px; line-height:1.6;">
This link expires in 60 minutes. If you did not request a password reset, you can safely ignore this email.
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#94a3b8; font-size:11px; padding-top:8px;">
Dewemoji Emoji discovery and keywords for creators
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dewemoji test email</title>
</head>
<body style="margin:0; padding:24px; font-family: Arial, sans-serif; background:#f4f6fb; color:#0f172a;">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="560" cellpadding="0" cellspacing="0" role="presentation" style="background:#ffffff; border-radius:16px; padding:28px; box-shadow:0 10px 30px rgba(15, 23, 42, 0.08);">
<tr>
<td style="font-size:20px; font-weight:700; padding-bottom:8px;">
Dewemoji
</td>
</tr>
<tr>
<td style="font-size:16px; line-height:1.6; padding-bottom:18px;">
This is a test email sent via the Mailketing API from your Dewemoji app.
</td>
</tr>
<tr>
<td style="padding-bottom:22px;">
<a href="{{ $actionUrl }}" style="display:inline-block; padding:12px 22px; background:#0f172a; color:#ffffff; text-decoration:none; border-radius:999px; font-size:14px; font-weight:700;">
Open Dewemoji
</a>
</td>
</tr>
<tr>
<td style="font-size:12px; color:#64748b;">
If the button does not work, open this link: {{ $actionUrl }}
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,101 @@
@php
$logoUrl = rtrim(config('app.url', ''), '/') . '/assets/logo/logo-mark-128.png';
@endphp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dewemoji Email Verification</title>
</head>
<body style="margin:0; padding:0; background:#f5f7fb;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f5f7fb; padding:32px 16px;">
<tr>
<td align="center">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="width:600px; max-width:100%; background:#ffffff; border-radius:24px; overflow:hidden; box-shadow:0 20px 50px rgba(15,23,42,0.10); border:1px solid #e6ebf5;">
<tr>
<td style="padding:26px 28px 10px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="width:40px; height:40px; border-radius:12px; background:#f1f5ff; text-align:center; border:1px solid #e2e8f0;">
<img src="{{ $logoUrl }}" alt="Dewemoji" width="40" height="40" style="display:block; border-radius:12px;" />
</td>
<td style="padding-left:12px; font-family:Arial, sans-serif; color:#0f172a; font-size:18px; font-weight:700;">
Dewemoji
</td>
</tr>
</table>
</td>
<td align="right" style="font-family:Arial, sans-serif; color:#6b7280; font-size:12px;">
Email verification
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:12px 28px 0;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-radius:18px; padding:26px; border:1px solid #e2e8f0;">
<tr>
<td style="padding-bottom:14px;">
<table role="presentation" cellpadding="0" cellspacing="0">
<tr>
<td style="width:34px; height:34px; border-radius:10px; background:#e2e8f0; text-align:center;">
<span style="display:inline-block; font-size:18px; line-height:34px;"></span>
</td>
<td style="padding-left:10px; font-family:Arial, sans-serif; color:#0f172a; font-size:22px; font-weight:700;">
Verify your email
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#475569; font-size:14px; line-height:1.7; padding-bottom:20px;">
Thanks for joining Dewemoji. Confirm your email to activate your account and start saving emoji keywords.
</td>
</tr>
<tr>
<td align="left" style="padding-bottom:18px;">
<a href="{{ $verificationUrl }}" style="display:inline-block; padding:12px 20px; background:#111827; color:#ffffff; text-decoration:none; border-radius:999px; font-family:Arial, sans-serif; font-size:14px; font-weight:700;">
Verify Email
</a>
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#64748b; font-size:12px; line-height:1.6;">
If the button does not work, paste this link into your browser:
<br />
<span style="color:#2563eb;">{{ $verificationUrl }}</span>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td style="padding:20px 28px 28px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#ffffff; border-radius:16px; padding:16px; border:1px dashed #e2e8f0;">
<tr>
<td style="font-family:Arial, sans-serif; color:#64748b; font-size:12px; line-height:1.6;">
This link expires in 60 minutes. If you did not request this email, you can safely ignore it.
</td>
</tr>
<tr>
<td style="font-family:Arial, sans-serif; color:#94a3b8; font-size:11px; padding-top:8px;">
Dewemoji Emoji discovery and keywords for creators
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Dewemoji') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,144 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', config('app.name', 'Dewemoji'))</title>
<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/logo/logo-mark-128.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/logo/logo-mark-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/logo/logo-mark-512.png">
<script>
(() => {
const stored = localStorage.getItem('dewemoji_theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const mode = stored || (prefersDark ? 'dark' : 'light');
const root = document.documentElement;
root.classList.toggle('dark', mode === 'dark');
root.classList.toggle('theme-light', mode !== 'dark');
})();
</script>
<link rel="preload" href="/assets/fonts/PlusJakartaSans-Regular.ttf" as="font" type="font/ttf" crossorigin>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
display: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
sun: '#fcb735',
sunSoft: '#ffda9c',
ocean: '#2053ff',
oceanSoft: '#356cf0',
},
},
},
},
};
</script>
<!-- Scripts -->
@vite(['resources/js/app.js'])
<style>
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
</style>
</head>
<body class="min-h-screen bg-slate-50 text-slate-900 dark:bg-slate-950 dark:text-slate-100 antialiased" style="font-family: 'Plus Jakarta Sans', system-ui, sans-serif;">
<div class="fixed inset-0 -z-10 pointer-events-none">
<div class="absolute top-[-15%] right-[-10%] w-[360px] h-[360px] bg-blue-500/10 blur-[120px]"></div>
<div class="absolute bottom-[-15%] left-[-10%] w-[420px] h-[420px] bg-amber-500/10 blur-[140px]"></div>
</div>
<div class="min-h-screen flex flex-col items-center justify-center px-6 py-12">
<div class="w-full max-w-md">
<div class="flex items-center justify-between mb-8">
<a href="/" class="inline-flex items-center gap-3">
<span class="w-12 h-12 rounded-2xl bg-gradient-to-br from-white to-gray-200 dark:from-white/20 dark:to-white/5 flex items-center justify-center shadow-lg">
<img src="/assets/logo/logo-mark.svg" alt="Dewemoji logo" class="w-7 h-7 object-contain" />
</span>
<span class="font-semibold text-lg">Dewemoji</span>
</a>
<button id="theme-toggle" class="w-10 h-10 rounded-full border border-slate-200 dark:border-white/10 bg-white/70 dark:bg-white/5 flex items-center justify-center text-slate-500 dark:text-slate-200 hover:text-slate-900 dark:hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
<div class="w-full rounded-2xl border border-slate-200/70 dark:border-white/10 bg-white/90 dark:bg-slate-900/70 shadow-xl backdrop-blur px-6 py-6">
{{ $slot }}
</div>
</div>
</div>
<script>lucide.createIcons();</script>
<script>
(() => {
const root = document.documentElement;
const toggle = document.getElementById('theme-toggle');
const iconDark = document.querySelector('[data-theme-icon="dark"]');
const iconLight = document.querySelector('[data-theme-icon="light"]');
const setTheme = (mode) => {
root.classList.toggle('dark', mode === 'dark');
root.classList.toggle('theme-light', mode !== 'dark');
if (iconDark && iconLight) {
iconDark.classList.toggle('hidden', mode !== 'dark');
iconLight.classList.toggle('hidden', mode === 'dark');
}
localStorage.setItem('dewemoji_theme', mode);
};
const stored = localStorage.getItem('dewemoji_theme');
if (stored) {
setTheme(stored);
} else {
setTheme(root.classList.contains('dark') ? 'dark' : 'light');
}
toggle?.addEventListener('click', () => {
const next = root.classList.contains('dark') ? 'light' : 'dark';
setTheme(next);
});
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard.overview') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard.overview')" :active="request()->routeIs('dashboard.*')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard.overview')" :active="request()->routeIs('dashboard.*')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@@ -0,0 +1,29 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-profile-information-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-password-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.delete-user-form')
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,55 @@
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Delete Account') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
</p>
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>
</section>

View File

@@ -0,0 +1,48 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -0,0 +1,64 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf
</form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
@csrf
@method('patch')
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -5,7 +5,7 @@
@push('head')
<style>
.doc-table tbody tr { border-bottom: 1px solid rgba(148,163,184,0.2); }
.doc-table tbody tr { border-bottom: 1px solid var(--muted-border); }
.doc-table tbody tr:hover { background: rgba(32,83,255,0.08); }
.codewrap { position: relative; }
.copy-btn {
@@ -56,15 +56,28 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0">
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Documentation</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">API Documentation</h1>
<header class="glass-header px-6 py-5 shrink-0 flex items-center justify-between">
<div>
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Documentation</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">API Documentation</h1>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">
@@ -85,7 +98,7 @@
<section id="overview" class="glass-card rounded-2xl p-6 scroll-mt-28">
<div class="flex items-center gap-2 mb-2">
<h2 class="font-display text-2xl font-bold">Emoji API Docs</h2>
<span id="pro-badge" class="hidden text-[10px] font-semibold px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-400/40">Pro mode</span>
<span id="api-badge" class="hidden text-[10px] font-semibold px-2 py-1 rounded bg-emerald-500/20 text-emerald-300 border border-emerald-400/40">API key active</span>
</div>
<p class="text-sm text-gray-300">Read-only API with search, categories, and emoji detail. Responses are cacheable and include ETag.</p>
<div class="codewrap mt-4">
@@ -96,20 +109,20 @@
<section id="auth" class="glass-card rounded-2xl p-6 scroll-mt-28">
<h3 class="text-lg font-semibold">Authentication</h3>
<p class="text-sm text-gray-300 mt-2">Free tier is public with daily caps. Pro unlocks higher limits with license key.</p>
<p class="text-sm text-gray-300 mt-2">Public endpoints are open. Personal plan holders can create API keys for private keyword access.</p>
<div class="mt-3 space-y-3">
<div class="codewrap">
<button class="copy-btn" data-copy-target="auth-bearer">Copy</button>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-bearer">Authorization: Bearer YOUR_LICENSE_KEY</code></pre>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-bearer">Authorization: Bearer YOUR_API_KEY</code></pre>
</div>
<div class="codewrap">
<button class="copy-btn" data-copy-target="auth-query">Copy</button>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-query">?key=YOUR_LICENSE_KEY</code></pre>
<pre class="text-xs overflow-x-auto bg-black/30 rounded-lg p-3"><code id="auth-query">X-Api-Key: YOUR_API_KEY</code></pre>
</div>
</div>
<ul class="mt-3 list-disc pl-5 text-sm text-gray-300 space-y-1">
<li><code>Authorization</code> (recommended)</li>
<li><code>X-License-Key</code> (also supported)</li>
<li><code>Authorization</code> or <code>X-Api-Key</code> for Personal API keys</li>
<li><code>X-License-Key</code> or <code>?key=</code> is only used for <code>/license/*</code> endpoints</li>
<li><code>X-Account-Id</code> (optional usage association)</li>
</ul>
</section>
@@ -122,22 +135,25 @@
<tr class="text-left text-gray-400">
<th class="py-2 pr-6">Tier</th>
<th class="py-2 pr-6 text-right">Page size cap</th>
<th class="py-2 pr-6 text-right">Daily cap</th>
<th class="py-2 pr-6">Auth</th>
<th class="py-2 pr-6">Rate limits</th>
<th class="py-2">Notes</th>
</tr>
</thead>
<tbody class="align-top text-gray-200">
<tr>
<td class="py-2 pr-6"><strong>Free</strong></td>
<td class="py-2 pr-6"><strong>Free (public)</strong></td>
<td class="py-2 pr-6 text-right">20</td>
<td class="py-2 pr-6 text-right">~30 (page-1, distinct)</td>
<td class="py-2">Cached responses do not count.</td>
<td class="py-2 pr-6">None</td>
<td class="py-2 pr-6">Hourly public limit (if enabled)</td>
<td class="py-2">Public dataset (EN + ID) only.</td>
</tr>
<tr>
<td class="py-2 pr-6"><strong>Pro</strong></td>
<td class="py-2 pr-6"><strong>Personal</strong></td>
<td class="py-2 pr-6 text-right">50</td>
<td class="py-2 pr-6 text-right">5,000 / day / license</td>
<td class="py-2">Up to 3 Chrome profiles.</td>
<td class="py-2 pr-6">API key</td>
<td class="py-2 pr-6">Unlimited public + private</td>
<td class="py-2">Unlocks private keyword search + sync.</td>
</tr>
</tbody>
</table>
@@ -204,11 +220,11 @@
<section id="try-it" class="glass-card rounded-2xl p-6 scroll-mt-28">
<h3 class="text-lg font-semibold">Try it</h3>
<p class="text-sm text-gray-300 mt-2">Demo request is limited to <strong>page=1</strong> and <strong>limit=10</strong>.</p>
<p id="ti-pro-note" class="hidden text-xs text-emerald-300 mt-2">Using Pro key via Authorization header.</p>
<p id="ti-api-note" class="hidden text-xs text-emerald-300 mt-2">Using API key via Authorization header.</p>
<div class="grid gap-3 md:grid-cols-3 mt-4">
<input id="ti-q" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="keyword (love)">
<input id="ti-category" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="category (Smileys & Emotion)">
<input id="ti-subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200" placeholder="subcategory (face-smiling)">
<input id="ti-q" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="keyword (love)">
<input id="ti-category" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="category (Smileys & Emotion)">
<input id="ti-subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-3 py-2 text-sm text-gray-200 theme-surface" placeholder="subcategory (face-smiling)">
</div>
<div class="mt-4 flex gap-3">
<button id="ti-run" type="button" class="px-4 py-2 rounded-lg bg-brand-ocean hover:bg-brand-oceanSoft text-white text-sm">Run /emojis</button>
@@ -236,12 +252,12 @@
(() => {
const BASE_API = @json(url('/v1'));
const urlParams = new URLSearchParams(location.search);
const proKey = urlParams.get('key') || '';
const proBadge = document.getElementById('pro-badge');
const proNote = document.getElementById('ti-pro-note');
if (proKey) {
proBadge?.classList.remove('hidden');
proNote?.classList.remove('hidden');
const apiKey = urlParams.get('key') || '';
const apiBadge = document.getElementById('api-badge');
const apiNote = document.getElementById('ti-api-note');
if (apiKey) {
apiBadge?.classList.remove('hidden');
apiNote?.classList.remove('hidden');
}
const baseUrlCode = document.getElementById('base-url-code');
@@ -253,14 +269,14 @@
const lim = 10;
const exCurlEmojis = document.getElementById('ex-curl-emojis');
if (exCurlEmojis) {
exCurlEmojis.textContent = proKey
? `curl -H "Authorization: Bearer ${proKey}" "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`
exCurlEmojis.textContent = apiKey
? `curl -H "Authorization: Bearer ${apiKey}" "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`
: `curl "${BASE_API}/emojis?q=${q}&category=${cat}&limit=${lim}&page=1"`;
}
const exCurlCats = document.getElementById('ex-curl-cats');
if (exCurlCats) {
exCurlCats.textContent = proKey
? `curl -H "Authorization: Bearer ${proKey}" "${BASE_API}/categories"`
exCurlCats.textContent = apiKey
? `curl -H "Authorization: Bearer ${apiKey}" "${BASE_API}/categories"`
: `curl "${BASE_API}/categories"`;
}
const exCurlEmoji = document.getElementById('ex-curl-emoji');
@@ -290,15 +306,15 @@
const resultDrawer = document.createElement('aside');
const backdrop = document.createElement('div');
resultDrawer.id = 'ti-drawer';
resultDrawer.className = 'fixed top-0 right-0 h-full w-full max-w-xl bg-[#0b0b0f] border-l border-white/10 translate-x-full transition-transform z-50';
backdrop.className = 'fixed inset-0 bg-black/40 opacity-0 pointer-events-none transition-opacity z-40';
resultDrawer.className = 'fixed top-0 right-0 h-full w-full max-w-xl border-l translate-x-full transition-transform z-50 offcanvas-panel';
backdrop.className = 'fixed inset-0 opacity-0 pointer-events-none transition-opacity z-40 offcanvas-backdrop';
resultDrawer.innerHTML = `
<div class="flex items-center justify-between p-4 border-b border-white/10">
<h3 class="text-lg font-semibold">Try it Result</h3>
<button id="ti-close" class="p-2 rounded-full text-gray-400 hover:bg-white/5"></button>
</div>
<div class="p-4 h-[calc(100%-64px)] overflow-auto">
<pre class="p-3 rounded bg-black/30 overflow-auto text-xs"><code id="ti-result">{ }</code></pre>
<pre class="p-3 rounded overflow-auto text-xs offcanvas-code"><code id="ti-result">{ }</code></pre>
</div>`;
document.body.appendChild(backdrop);
document.body.appendChild(resultDrawer);
@@ -340,7 +356,7 @@
params.set('page', '1');
const headers = {};
if (proKey) headers['Authorization'] = 'Bearer ' + proKey;
if (apiKey) headers['Authorization'] = 'Bearer ' + apiKey;
try {
const res = await fetch(`${BASE_API}/emojis?` + params.toString(), { headers });
if (!res.ok) throw new Error('Request failed: ' + res.status);
@@ -355,7 +371,7 @@
document.getElementById('ti-cats')?.addEventListener('click', async () => {
const headers = {};
if (proKey) headers['Authorization'] = 'Bearer ' + proKey;
if (apiKey) headers['Authorization'] = 'Bearer ' + apiKey;
try {
const res = await fetch(`${BASE_API}/categories`, { headers });
if (!res.ok) throw new Error('Request failed: ' + res.status);

View File

@@ -12,6 +12,10 @@
$description = $emoji['description'] ?? '';
$unified = $emoji['unified'] ?? '';
$shortcode = $emoji['shortcodes'][0] ?? '';
$user = auth()->user();
$userTier = $userTier ?? $user?->tier;
$isPersonal = $userTier === 'personal';
$userKeywords = $userKeywords ?? collect();
$htmlHex = '';
$cssCode = '';
if (!empty($emoji['codepoints'][0])) {
@@ -66,13 +70,22 @@
</div>
</aside>
<main class="flex-1 h-full overflow-y-auto relative p-4 sm:p-6 lg:p-10 pb-24 lg:pb-10 flex flex-col">
<div class="flex items-center gap-2 text-sm text-gray-500 mb-8 font-mono">
<a href="{{ route('home') }}" class="hover:text-white transition-colors">Home</a>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="hover:text-white">{{ $category }}</span>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="text-brand-sun">{{ $name }}</span>
<main class="flex-1 h-full overflow-y-auto relative p-4 pt-0 sm:p-6 lg:p-10 pb-24 lg:pb-10 flex flex-col">
<div class="sticky top-0 z-40 -mx-4 px-4 py-3 mb-6 bg-[var(--app-bg)]/90 backdrop-blur border-b border-white/10 sm:static sm:mx-0 sm:px-0 sm:py-0 sm:mb-8 sm:border-0">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2 text-xs sm:text-sm text-gray-500 font-mono">
<a href="{{ route('home') }}" class="hover:text-white transition-colors">Home</a>
<i data-lucide="chevron-right" class="w-3 h-3"></i>
<span class="hidden sm:inline-flex hover:text-white">{{ $category }}</span>
<i data-lucide="chevron-right" class="w-3 h-3 hidden sm:inline-flex"></i>
<span class="text-brand-sun">{{ $name }}</span>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 max-w-6xl mx-auto w-full">
@@ -186,6 +199,48 @@
</div>
</div>
@endif
<div class="mt-6 glass-card rounded-2xl p-5">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<i data-lucide="sparkles" class="w-4 h-4 text-brand-ocean"></i>
<h3 class="text-sm font-bold text-gray-200 uppercase tracking-wide">Your Keywords</h3>
</div>
@if ($isPersonal)
<button id="user-keyword-add" class="rounded-full bg-brand-ocean text-white text-xs font-semibold px-3 py-1.5">Add keyword</button>
@endif
</div>
@if ($isPersonal)
<div id="user-keyword-list" class="mt-4 flex flex-wrap gap-2">
@forelse ($userKeywords as $keyword)
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-sm text-gray-200">
<span>{{ $keyword->keyword }}</span>
<span class="text-[10px] uppercase tracking-[0.2em] text-gray-500">{{ $keyword->lang ?? 'und' }}</span>
</span>
@empty
<span class="text-sm text-gray-400">No private keywords yet. Add one to personalize search.</span>
@endforelse
</div>
@elseif ($user)
<div class="mt-4 rounded-xl border border-brand-sun/30 bg-brand-sun/10 p-3 text-sm text-brand-sun">
Upgrade to Personal to add private keywords for this emoji.
</div>
<div class="mt-4 flex flex-wrap gap-2 opacity-70">
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-1</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-2</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-300">your-tag-3</span>
<span class="px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-xs text-gray-400">Unlock with Personal</span>
</div>
<a href="{{ route('pricing') }}" class="mt-3 inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">Upgrade to Personal</a>
@else
<div class="mt-4 rounded-xl border border-white/10 bg-white/5 p-3 text-sm text-gray-300">
Sign up to personalize keywords and sync across devices.
</div>
<a href="{{ route('register') }}" class="mt-3 inline-flex items-center justify-center rounded-full bg-brand-sun text-black font-semibold px-4 py-2 text-sm">Sign up free</a>
@endif
</div>
</div>
</div>
@@ -193,28 +248,6 @@
</main>
</div>
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
<div class="grid grid-cols-6 gap-1 text-[11px]">
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
</a>
<a href="{{ route('api-docs') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="book-open" class="w-4 h-4"></i><span>Docs</span>
</a>
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
</a>
<a href="{{ route('support') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
</a>
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
</a>
<a href="{{ route('terms') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="file-text" class="w-4 h-4"></i><span>Terms</span>
</a>
</div>
</nav>
<div id="toast" class="fixed bottom-10 left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50 pointer-events-none">
<div class="bg-brand-ocean text-white px-6 py-2 rounded-full font-bold shadow-[0_0_20px_rgba(32,83,255,0.45)] flex items-center gap-2">
@@ -222,6 +255,32 @@
<span id="toast-msg">Copied!</span>
</div>
</div>
<div id="user-keyword-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div class="relative z-10 w-full max-w-lg rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-white">Add keyword</h3>
<button id="user-keyword-close" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-gray-200">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form id="user-keyword-form" class="mt-4 grid gap-4">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" name="keyword" id="user-keyword-input" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="magic" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" name="lang" id="user-keyword-lang" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" placeholder="en">
</div>
<div class="flex items-center justify-end gap-2">
<button type="button" id="user-keyword-cancel" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">Cancel</button>
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-5 py-2 text-sm">Save keyword</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
@@ -267,5 +326,74 @@ document.addEventListener('keydown', (e) => {
// Treat opening the single-emoji page as a "recently viewed emoji" event.
addRecent(@json($symbol));
(() => {
const isPersonal = @json($isPersonal);
if (!isPersonal) return;
const modal = document.getElementById('user-keyword-modal');
const openBtn = document.getElementById('user-keyword-add');
const closeBtn = document.getElementById('user-keyword-close');
const cancelBtn = document.getElementById('user-keyword-cancel');
const form = document.getElementById('user-keyword-form');
const keywordInput = document.getElementById('user-keyword-input');
const langInput = document.getElementById('user-keyword-lang');
const list = document.getElementById('user-keyword-list');
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const openModal = () => {
modal.classList.remove('hidden');
modal.classList.add('flex');
keywordInput.value = '';
langInput.value = '';
keywordInput.focus();
};
const closeModal = () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
};
openBtn?.addEventListener('click', openModal);
closeBtn?.addEventListener('click', closeModal);
cancelBtn?.addEventListener('click', closeModal);
modal?.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
form?.addEventListener('submit', async (e) => {
e.preventDefault();
const payload = {
emoji_slug: @json($slug),
keyword: keywordInput.value.trim(),
lang: langInput.value.trim() || 'und',
};
if (!payload.keyword) return;
const res = await fetch('{{ route('dashboard.keywords.store') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
showToast('Could not save keyword');
return;
}
const badge = document.createElement('span');
badge.className = 'inline-flex items-center gap-2 px-3 py-1.5 rounded-lg bg-white/5 border border-white/10 text-sm text-gray-200';
badge.innerHTML = `<span>${payload.keyword}</span><span class="text-[10px] uppercase tracking-[0.2em] text-gray-500">${payload.lang}</span>`;
if (list) {
const empty = list.querySelector('span.text-sm');
if (empty) empty.remove();
list.prepend(badge);
}
closeModal();
showToast('Keyword added');
});
})();
</script>
@endpush

View File

@@ -5,16 +5,17 @@
@push('head')
<style>
.glass-header {
background: rgba(5, 5, 5, 0.85);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
#grid {
--card-min: 104px;
--emoji-size: 2rem;
grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
}
@media (max-width: 640px) {
#grid {
--card-min: 0px;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
</style>
@endpush
@@ -48,6 +49,18 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="log-in" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Login</span>
</a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="shield-check" class="w-5 h-5"></i>
<span class="text-sm font-medium hidden lg:block">Privacy</span>
@@ -65,11 +78,11 @@
<div class="flex flex-col md:flex-row gap-4 md:items-center justify-between">
<div class="relative group grow max-w-3xl">
<div class="absolute -inset-0.5 bg-gradient-to-r from-brand-ocean to-brand-sun rounded-xl blur opacity-20 group-hover:opacity-40 transition duration-500"></div>
<div class="relative flex items-center bg-[#151518] border border-white/10 rounded-xl shadow-2xl h-11">
<div class="relative flex items-center bg-[#151518] border border-white/10 rounded-xl shadow-2xl h-11 theme-surface">
<div class="pl-4 pr-3 text-gray-400">
<i data-lucide="search" class="w-5 h-5"></i>
</div>
<input id="q" type="text" placeholder="Search emojis by keyword, mood, meaning..." class="w-full bg-transparent text-white placeholder-gray-500 focus:outline-none font-medium h-full">
<input id="q" type="text" placeholder="Search emojis by keyword, mood, meaning..." class="w-full bg-transparent text-white placeholder-gray-500 focus:outline-none font-medium h-full text-sm sm:text-base">
<div class="hidden md:flex items-center pr-3">
<kbd class="hidden sm:inline-block px-2 py-0.5 text-[10px] font-mono text-gray-500 bg-white/5 border border-white/10 rounded-md">⌘K</kbd>
</div>
@@ -77,14 +90,18 @@
</div>
<div class="flex items-center gap-3 shrink-0">
<select id="category" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none">
<select id="category" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none theme-surface">
<option value="">All Categories</option>
</select>
<select id="subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none" disabled>
<select id="subcategory" class="bg-[#151518] border border-white/10 rounded-xl px-4 text-sm text-gray-300 focus:outline-none focus:border-brand-ocean hover:bg-white/5 transition-colors h-11 cursor-pointer appearance-none theme-surface" disabled>
<option value="">All Subcategories</option>
</select>
<button id="theme-toggle" class="w-10 h-10 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
<div class="w-px h-8 bg-white/10 mx-1 hidden lg:block"></div>
<div class="hidden lg:flex w-10 h-10 rounded-full bg-gradient-to-r from-gray-700 to-gray-600 border border-white/10"></div>
</div>
</div>
@@ -159,41 +176,48 @@
</main>
</div>
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2">
<div class="grid grid-cols-6 gap-1 text-[11px]">
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-brand-sun bg-white/5">
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
</a>
<a href="{{ route('api-docs') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="book-open" class="w-4 h-4"></i><span>Docs</span>
</a>
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
</a>
<a href="{{ route('support') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
</a>
<a href="{{ route('privacy') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
</a>
<a href="{{ route('terms') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="file-text" class="w-4 h-4"></i><span>Terms</span>
</a>
</div>
</nav>
<div id="toast" class="fixed bottom-8 left-1/2 -translate-x-1/2 translate-y-24 opacity-0 transition-all duration-300 z-50">
<div class="bg-brand-ocean text-white px-4 py-2 rounded-full font-bold shadow-[0_0_20px_rgba(32,83,255,0.45)] flex items-center gap-2 text-sm">
<i data-lucide="check" class="w-4 h-4"></i>
<span id="toast-msg">Copied!</span>
</div>
</div>
<div id="keyword-edit-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div class="relative z-10 w-full max-w-lg rounded-3xl glass-card p-6">
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold text-white">Edit keyword</h3>
<button id="keyword-edit-close" class="w-8 h-8 rounded-full bg-white/10 hover:bg-white/20 flex items-center justify-center text-gray-200">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<form id="keyword-edit-form" class="mt-4 grid gap-4">
<input type="hidden" id="keyword-edit-id">
<input type="hidden" id="keyword-edit-slug">
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Keyword</label>
<input type="text" id="keyword-edit-text" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean" required>
</div>
<div>
<label class="text-xs uppercase tracking-[0.2em] text-gray-400">Language</label>
<input type="text" id="keyword-edit-lang" class="mt-2 w-full rounded-xl bg-black/40 border border-white/10 px-4 py-3 text-sm text-gray-200 focus:outline-none focus:border-brand-ocean">
</div>
<div class="flex items-center justify-end gap-2">
<button type="button" id="keyword-edit-cancel" class="rounded-full border border-white/10 px-4 py-2 text-sm text-gray-200 hover:bg-white/5">Cancel</button>
<button type="submit" class="rounded-full bg-brand-ocean text-white font-semibold px-5 py-2 text-sm">Save</button>
</div>
</form>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const state = { page: 1, limit: 32, total: 0, items: [], categories: {} };
const userTier = @json($userTier ?? null);
const isPersonal = userTier === 'personal';
const initialQuery = @json($initialQuery ?? '');
const initialCategory = @json($initialCategory ?? '');
const initialSubcategory = @json($initialSubcategory ?? '');
@@ -216,6 +240,15 @@
const gridSmallerEl = document.getElementById('grid-smaller');
const gridBiggerEl = document.getElementById('grid-bigger');
const densityStorageKey = 'dewemoji_grid_density';
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const keywordEditModal = document.getElementById('keyword-edit-modal');
const keywordEditClose = document.getElementById('keyword-edit-close');
const keywordEditCancel = document.getElementById('keyword-edit-cancel');
const keywordEditForm = document.getElementById('keyword-edit-form');
const keywordEditId = document.getElementById('keyword-edit-id');
const keywordEditSlug = document.getElementById('keyword-edit-slug');
const keywordEditText = document.getElementById('keyword-edit-text');
const keywordEditLang = document.getElementById('keyword-edit-lang');
if (initialQuery) qEl.value = initialQuery;
@@ -423,7 +456,8 @@
if (catEl.value) params.set('category', catEl.value);
if (subEl.value) params.set('subcategory', subEl.value);
const res = await fetch('/v1/emojis?' + params.toString());
const endpoint = isPersonal ? '/dashboard/keywords/search' : '/v1/emojis';
const res = await fetch(endpoint + '?' + params.toString());
const data = await res.json();
if (!res.ok) {
const msg = data.message || data.error || `API error (${res.status})`;
@@ -449,18 +483,24 @@
}
items.forEach((item) => {
const isPrivate = item.source === 'private';
const card = document.createElement('div');
card.className = 'relative aspect-square rounded-lg bg-white/5 hover:bg-white/10 transition-transform hover:scale-[1.02] border border-transparent hover:border-white/20 overflow-hidden group';
card.className = 'emoji-card relative aspect-square rounded-lg bg-white/5 hover:bg-white/10 transition-transform hover:scale-[1.02] border border-transparent hover:border-white/20 overflow-hidden group';
card.innerHTML = `
<a href="/emoji/${encodeURIComponent(item.slug)}" class="absolute inset-0 flex items-center justify-center pb-10">
<span class="leading-none" style="font-size:var(--emoji-size)">${esc(item.emoji)}</span>
</a>
<div class="absolute bottom-0 left-0 right-0 border-t border-white/10 bg-black/20 px-2 py-1.5 flex items-end gap-1">
<div class="emoji-card-bar absolute bottom-0 left-0 right-0 border-t border-white/10 bg-black/20 px-2 py-1.5 flex items-start gap-1">
<span class="emoji-name-clamp text-[10px] text-gray-300 text-left flex-1">${esc(item.name)}</span>
${isPrivate ? `<span class="px-1.5 py-0.5 rounded bg-brand-ocean/20 text-[9px] text-brand-oceanSoft" title="${esc(item.matched_keyword || '')}">Your: ${esc(item.matched_keyword || '')}</span>` : ''}
${isPrivate ? `<button type="button" class="edit-btn shrink-0 rounded bg-white/10 px-1.5 text-[9px] text-gray-200 hover:bg-brand-ocean/30">Edit</button>` : ''}
${isPrivate ? `<button type="button" class="delete-btn shrink-0 rounded bg-white/10 px-1.5 text-[9px] text-gray-200 hover:bg-red-500/30">Del</button>` : ''}
<button type="button" class="copy-btn shrink-0 w-6 h-6 rounded bg-white/10 hover:bg-brand-ocean/30 text-[11px] text-gray-200 hover:text-white transition-colors" title="Copy emoji"></button>
</div>
`;
const copyBtn = card.querySelector('.copy-btn');
const editBtn = card.querySelector('.edit-btn');
const deleteBtn = card.querySelector('.delete-btn');
if (copyBtn) {
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
@@ -471,6 +511,36 @@
});
});
}
if (editBtn && isPrivate) {
editBtn.addEventListener('click', (e) => {
e.preventDefault();
openKeywordEdit(item);
});
}
if (deleteBtn && isPrivate) {
deleteBtn.addEventListener('click', async (e) => {
e.preventDefault();
if (!item.matched_keyword_id) return;
const ok = await window.dewemojiConfirm('Delete this keyword?', {
title: 'Delete keyword',
okText: 'Delete',
});
if (!ok) return;
const res = await fetch(`/dashboard/keywords/${item.matched_keyword_id}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
showToast('Could not delete keyword');
return;
}
fetchEmojis(true);
});
}
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
navigator.clipboard.writeText(item.emoji).then(() => {
@@ -514,6 +584,55 @@
});
});
function openKeywordEdit(item) {
if (!isPersonal || !item.matched_keyword_id) return;
keywordEditId.value = item.matched_keyword_id;
keywordEditSlug.value = item.slug;
keywordEditText.value = item.matched_keyword || '';
keywordEditLang.value = item.matched_lang || '';
keywordEditModal.classList.remove('hidden');
keywordEditModal.classList.add('flex');
keywordEditText.focus();
}
function closeKeywordEdit() {
keywordEditModal.classList.add('hidden');
keywordEditModal.classList.remove('flex');
}
keywordEditClose?.addEventListener('click', closeKeywordEdit);
keywordEditCancel?.addEventListener('click', closeKeywordEdit);
keywordEditModal?.addEventListener('click', (e) => {
if (e.target === keywordEditModal) closeKeywordEdit();
});
keywordEditForm?.addEventListener('submit', async (e) => {
e.preventDefault();
const id = keywordEditId.value;
const payload = {
emoji_slug: keywordEditSlug.value,
keyword: keywordEditText.value.trim(),
lang: keywordEditLang.value.trim() || 'und',
};
if (!id || !payload.keyword) return;
const res = await fetch(`/dashboard/keywords/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify(payload),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.ok) {
showToast('Could not update keyword');
return;
}
closeKeywordEdit();
fetchEmojis(true);
});
if (gridSizeEl && gridSmallerEl && gridBiggerEl) {
const initialDensity = localStorage.getItem(densityStorageKey) ?? '1';
applyGridDensity(Number(initialDensity));

View File

@@ -15,6 +15,7 @@
@endphp
<title>@yield('title', 'Dewemoji')</title>
<meta name="description" content="{{ $metaDescription }}">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="canonical" href="{{ $canonicalUrl }}">
<meta property="og:title" content="{{ $metaTitle }}">
<meta property="og:description" content="{{ $metaDescription }}">
@@ -25,6 +26,10 @@
<meta name="twitter:title" content="{{ $metaTitle }}">
<meta name="twitter:description" content="{{ $metaDescription }}">
<meta name="theme-color" content="#2053ff">
<link rel="icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/assets/logo/logo-mark-128.png">
<link rel="icon" type="image/png" sizes="192x192" href="/assets/logo/logo-mark-512.png">
<link rel="apple-touch-icon" sizes="180x180" href="/assets/logo/logo-mark-512.png">
<script type="application/ld+json">
{
"@@context": "https://schema.org",
@@ -48,19 +53,33 @@
]
}
</script>
<script>
(() => {
const stored = localStorage.getItem('dewemoji_theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const mode = stored || (prefersDark ? 'dark' : 'light');
const root = document.documentElement;
if (mode === 'dark') {
root.classList.add('dark');
root.classList.remove('theme-light');
} else {
root.classList.remove('dark');
root.classList.add('theme-light');
}
})();
</script>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
<link rel="preload" href="/assets/fonts/PlusJakartaSans-Regular.ttf" as="font" type="font/ttf" crossorigin>
<script src="https://unpkg.com/lucide@latest"></script>
@vite(['resources/js/app.js'])
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
sans: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
display: ['"Plus Jakarta Sans"', 'system-ui', 'sans-serif'],
},
colors: {
brand: {
@@ -85,28 +104,164 @@
};
</script>
<style>
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Light.ttf") format("truetype");
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Regular.ttf") format("truetype");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Medium.ttf") format("truetype");
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBold.ttf") format("truetype");
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Bold.ttf") format("truetype");
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-LightItalic.ttf") format("truetype");
font-weight: 300;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-Italic.ttf") format("truetype");
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-MediumItalic.ttf") format("truetype");
font-weight: 500;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-SemiBoldItalic.ttf") format("truetype");
font-weight: 600;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: "Plus Jakarta Sans";
src: url("/assets/fonts/PlusJakartaSans-BoldItalic.ttf") format("truetype");
font-weight: 700;
font-style: italic;
font-display: swap;
}
:root {
--font-family: "Plus Jakarta Sans", system-ui, sans-serif;
--fs-xl: clamp(2.5rem, 5vw, 3rem);
--fs-l: clamp(1.875rem, 4vw, 2rem);
--fs-m: clamp(1.5rem, 3vw, 1.5rem);
--fs-s: 1.125rem;
--fs-xs: 0.875rem;
--fw-light: 300;
--fw-normal: 400;
--fw-medium: 500;
--fw-semibold: 600;
--fw-bold: 700;
--app-bg: #050505;
--app-fg: #f8fafc;
--muted-text: #9ca3af;
--muted-strong: #e5e7eb;
--muted-border: rgba(148,163,184,0.2);
--panel-bg: rgba(20, 20, 23, 0.6);
--panel-border: rgba(255, 255, 255, 0.08);
--card-bg: linear-gradient(145deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
--card-border: rgba(255, 255, 255, 0.05);
--card-hover-bg: linear-gradient(145deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0.02) 100%);
--card-hover-border: rgba(53, 108, 240, 0.35);
--header-bg: rgba(5, 5, 5, 0.85);
--header-border: rgba(255, 255, 255, 0.05);
--surface-bg: #151518;
--surface-border: rgba(255, 255, 255, 0.1);
--nav-bg: rgba(11, 11, 15, 0.95);
--code-bg: rgba(0,0,0,0.3);
--scrollbar-thumb: rgba(255,255,255,.1);
--scrollbar-thumb-hover: rgba(255,255,255,.2);
}
html.theme-light {
--app-bg: #f6f7fb;
--app-fg: #0f172a;
--muted-text: #64748b;
--muted-strong: #1f2937;
--muted-border: rgba(15,23,42,0.12);
--panel-bg: rgba(255, 255, 255, 0.78);
--panel-border: rgba(15, 23, 42, 0.08);
--card-bg: linear-gradient(145deg, rgba(15,23,42,0.04) 0%, rgba(15,23,42,0.02) 100%);
--card-border: rgba(15, 23, 42, 0.08);
--card-hover-bg: linear-gradient(145deg, rgba(15,23,42,0.06) 0%, rgba(15,23,42,0.02) 100%);
--card-hover-border: rgba(32, 83, 255, 0.2);
--header-bg: rgba(255, 255, 255, 0.85);
--header-border: rgba(15, 23, 42, 0.08);
--surface-bg: #ffffff;
--surface-border: rgba(15, 23, 42, 0.12);
--nav-bg: rgba(255, 255, 255, 0.9);
--code-bg: rgba(15,23,42,0.06);
--scrollbar-thumb: rgba(15,23,42,.18);
--scrollbar-thumb-hover: rgba(15,23,42,.28);
}
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.2); }
::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
.glass-panel {
background: rgba(20, 20, 23, 0.6);
background: var(--panel-bg);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.08);
border: 1px solid var(--panel-border);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
.glass-card {
background: linear-gradient(145deg, rgba(255,255,255,0.03) 0%, rgba(255,255,255,0.01) 100%);
border: 1px solid rgba(255, 255, 255, 0.05);
background: var(--card-bg);
border: 1px solid var(--card-border);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.glass-card:hover {
background: linear-gradient(145deg, rgba(255,255,255,0.07) 0%, rgba(255,255,255,0.02) 100%);
border-color: rgba(53, 108, 240, 0.35);
background: var(--card-hover-bg);
border-color: var(--card-hover-border);
transform: translateY(-2px);
box-shadow: 0 10px 40px -10px rgba(32, 83, 255, 0.2);
}
.glass-header {
background: var(--header-bg);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--header-border);
}
.theme-surface {
background: var(--surface-bg) !important;
border-color: var(--surface-border) !important;
}
.theme-nav {
background: var(--nav-bg) !important;
border-color: var(--panel-border) !important;
}
.text-gradient {
background: linear-gradient(to right, #fcb735, #2053ff);
-webkit-background-clip: text;
@@ -120,11 +275,79 @@
line-height: 1.1;
max-height: 2.2em;
}
h1 { font-size: var(--fs-xl); font-weight: var(--fw-bold); line-height: 1.1; }
h2 { font-size: var(--fs-l); font-weight: var(--fw-semibold); line-height: 1.25; }
h3 { font-size: var(--fs-m); font-weight: var(--fw-semibold); line-height: 1.3; }
html.theme-light .text-white:not(.force-white) { color: var(--app-fg) !important; }
html.theme-light .text-gray-200 { color: #334155 !important; }
html.theme-light .text-gray-300 { color: #475569 !important; }
html.theme-light .text-gray-400 { color: var(--muted-text) !important; }
html.theme-light .text-gray-500 { color: #94a3b8 !important; }
html.theme-light .text-gray-100 { color: #1f2937 !important; }
html.theme-light .border-white\/5 { border-color: rgba(15,23,42,0.08) !important; }
html.theme-light .border-white\/10 { border-color: rgba(15,23,42,0.12) !important; }
html.theme-light .border-white\/20 { border-color: rgba(15,23,42,0.18) !important; }
html.theme-light .bg-white\/5 { background: rgba(15,23,42,0.04) !important; }
html.theme-light .bg-white\/10 { background: rgba(15,23,42,0.06) !important; }
html.theme-light .bg-white\/20 { background: rgba(15,23,42,0.1) !important; }
html.theme-light .bg-black\/30 { background: var(--code-bg) !important; }
html.theme-light .bg-black\/40 { background: var(--code-bg) !important; }
html.theme-light .copy-btn {
background: rgba(255,255,255,0.9);
border-color: rgba(15,23,42,0.2);
color: var(--app-fg);
}
html.theme-light .copy-btn:hover { background: rgba(32,83,255,0.18); }
html.theme-light .emoji-card {
background: #ffffff;
border-color: rgba(15,23,42,0.12);
box-shadow: 0 6px 18px rgba(15,23,42,0.08);
}
html.theme-light .emoji-card:hover {
border-color: rgba(32,83,255,0.35);
box-shadow: 0 10px 22px rgba(32,83,255,0.18);
}
html.theme-light .emoji-card-bar {
background: rgba(15,23,42,0.04);
border-top-color: rgba(15,23,42,0.1);
}
.offcanvas-panel {
background: var(--panel-bg);
border-color: var(--panel-border);
color: var(--app-fg);
}
.offcanvas-backdrop {
background: rgba(0,0,0,0.4);
}
html.theme-light .offcanvas-backdrop {
background: rgba(15,23,42,0.3);
}
.offcanvas-code {
background: var(--code-bg);
}
.cookie-banner {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
color: var(--app-fg);
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
}
.cookie-btn {
background: rgba(32,83,255,0.14);
border: 1px solid rgba(32,83,255,0.4);
color: var(--app-fg);
}
.cookie-btn:hover { background: rgba(32,83,255,0.22); }
.cookie-btn.secondary {
background: transparent;
border: 1px solid var(--muted-border);
color: var(--muted-text);
}
html.theme-light .cookie-btn.secondary { color: var(--app-fg); }
</style>
@stack('head')
@stack('jsonld')
</head>
<body class="bg-[#050505] text-white min-h-screen selection:bg-brand-ocean selection:text-white">
<body class="bg-[var(--app-bg)] text-[var(--app-fg)] min-h-screen selection:bg-brand-ocean selection:text-white" style="font-family: var(--font-family); font-size: var(--fs-s); font-weight: var(--fw-normal); line-height: 1.6;">
<div class="fixed top-0 left-0 w-full h-full overflow-hidden -z-10 pointer-events-none">
<div class="absolute top-[-10%] right-[-5%] w-[500px] h-[500px] bg-brand-ocean/20 rounded-full blur-[120px] animate-pulse-slow"></div>
<div class="absolute bottom-[-10%] left-[-10%] w-[600px] h-[600px] bg-blue-900/10 rounded-full blur-[120px]"></div>
@@ -132,7 +355,255 @@
@yield('content')
@hasSection('mobile_nav')
@yield('mobile_nav')
@else
<nav class="lg:hidden fixed bottom-0 inset-x-0 z-50 border-t border-white/10 bg-[#0b0b0f]/95 backdrop-blur px-2 py-2 theme-nav">
<div class="grid grid-cols-4 gap-1 text-[11px]">
<a href="{{ route('home') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('home') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="layout-grid" class="w-4 h-4"></i><span>Discover</span>
</a>
<a href="{{ route('api-docs') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('api-docs') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="book-open" class="w-4 h-4"></i><span>Docs</span>
</a>
<a href="{{ route('pricing') }}" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 {{ request()->routeIs('pricing') ? 'text-brand-sun bg-white/5' : 'text-gray-300 hover:bg-white/5' }}">
<i data-lucide="badge-dollar-sign" class="w-4 h-4"></i><span>Pricing</span>
</a>
<button id="more-menu-btn" class="flex flex-col items-center justify-center gap-1 rounded-lg py-2 text-gray-300 hover:bg-white/5">
<i data-lucide="more-horizontal" class="w-4 h-4"></i><span>More</span>
</button>
</div>
</nav>
@endif
@hasSection('more_menu')
@yield('more_menu')
@else
<div id="more-menu-backdrop" class="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden"></div>
<div id="more-menu" class="lg:hidden fixed bottom-16 left-4 right-4 glass-panel rounded-2xl p-4 z-50 hidden">
<div class="flex items-center justify-between mb-3">
<span class="text-sm font-semibold">More</span>
<button id="more-menu-close" class="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
<div class="flex flex-col gap-2 text-sm">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="layout-dashboard" class="w-4 h-4"></i><span>Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="log-in" class="w-4 h-4"></i><span>Login</span>
</a>
@endguest
<a href="{{ route('support') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="life-buoy" class="w-4 h-4"></i><span>Support</span>
</a>
<a href="{{ route('privacy') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="shield-check" class="w-4 h-4"></i><span>Privacy</span>
</a>
<a href="{{ route('terms') }}" class="flex items-center gap-3 rounded-xl px-4 py-3 bg-white/5 hover:bg-white/10">
<i data-lucide="file-text" class="w-4 h-4"></i><span>Terms</span>
</a>
</div>
</div>
@endif
<div id="cookie-banner" class="cookie-banner fixed bottom-6 left-6 right-6 md:right-8 md:left-auto md:max-w-xl rounded-2xl p-4 md:p-5 hidden z-50">
<div class="flex flex-col gap-3">
<div>
<p class="text-sm font-semibold">Cookies & analytics</p>
<p class="text-xs text-gray-400">We use cookies to measure usage and improve Dewemoji. No tracking on staging. You can change this anytime.</p>
</div>
<div class="flex flex-wrap gap-2">
<button id="cookie-accept" class="cookie-btn rounded-xl px-3 py-2 text-xs font-semibold">Accept analytics</button>
<button id="cookie-decline" class="cookie-btn secondary rounded-xl px-3 py-2 text-xs font-semibold">Decline</button>
<a href="/privacy" class="text-xs text-gray-400 underline underline-offset-2">Privacy</a>
</div>
</div>
</div>
<div id="confirm-dialog" class="fixed inset-0 z-[100] hidden items-center justify-center">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div class="relative w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 text-slate-900 shadow-2xl dark:border-white/10 dark:bg-slate-950 dark:text-white">
<div class="text-xs uppercase tracking-[0.25em] text-slate-400" id="confirm-title">Confirm action</div>
<div class="mt-3 text-lg font-semibold" id="confirm-message">Are you sure?</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button id="confirm-cancel" class="rounded-full border border-slate-200 px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 dark:border-white/10 dark:text-slate-300 dark:hover:bg-white/5">
Cancel
</button>
<button id="confirm-ok" class="rounded-full bg-rose-500 px-4 py-2 text-sm font-semibold text-white hover:bg-rose-600">
Confirm
</button>
</div>
</div>
</div>
<script>lucide.createIcons();</script>
<script>
(() => {
const root = document.documentElement;
const toggle = document.getElementById('theme-toggle');
const iconDark = document.querySelector('[data-theme-icon="dark"]');
const iconLight = document.querySelector('[data-theme-icon="light"]');
const setTheme = (mode) => {
if (mode === 'dark') {
root.classList.add('dark');
root.classList.remove('theme-light');
} else {
root.classList.remove('dark');
root.classList.add('theme-light');
}
localStorage.setItem('dewemoji_theme', mode);
if (iconDark && iconLight) {
iconDark.classList.toggle('hidden', mode !== 'dark');
iconLight.classList.toggle('hidden', mode === 'dark');
}
};
const stored = localStorage.getItem('dewemoji_theme');
if (stored) setTheme(stored);
else setTheme(root.classList.contains('dark') ? 'dark' : 'light');
if (toggle) {
toggle.addEventListener('click', () => {
const next = root.classList.contains('dark') ? 'light' : 'dark';
setTheme(next);
});
}
})();
</script>
<script>
(() => {
const moreMenuBtn = document.getElementById('more-menu-btn');
const moreMenu = document.getElementById('more-menu');
const moreMenuBackdrop = document.getElementById('more-menu-backdrop');
const moreMenuClose = document.getElementById('more-menu-close');
const openMoreMenu = () => {
if (moreMenu) moreMenu.classList.remove('hidden');
if (moreMenuBackdrop) moreMenuBackdrop.classList.remove('hidden');
};
const closeMoreMenu = () => {
if (moreMenu) moreMenu.classList.add('hidden');
if (moreMenuBackdrop) moreMenuBackdrop.classList.add('hidden');
};
if (moreMenuBtn) moreMenuBtn.addEventListener('click', openMoreMenu);
if (moreMenuBackdrop) moreMenuBackdrop.addEventListener('click', closeMoreMenu);
if (moreMenuClose) moreMenuClose.addEventListener('click', closeMoreMenu);
})();
</script>
<script>
(() => {
const GA_ID = 'G-R7FYYRBVJK';
const allowedHosts = new Set(['dewemoji.com', 'www.dewemoji.com']);
const consentKey = 'dewemoji_cookie_consent';
const banner = document.getElementById('cookie-banner');
const acceptBtn = document.getElementById('cookie-accept');
const declineBtn = document.getElementById('cookie-decline');
const loadGA = () => {
if (!allowedHosts.has(window.location.hostname)) return;
if (window.__dewemojiGaLoaded) return;
window.__dewemojiGaLoaded = true;
const script = document.createElement('script');
script.async = true;
script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_ID}`;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
function gtag(){ dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', GA_ID, { anonymize_ip: true });
};
const consent = localStorage.getItem(consentKey);
if (!consent && banner) banner.classList.remove('hidden');
if (consent === 'accepted') loadGA();
if (acceptBtn) {
acceptBtn.addEventListener('click', () => {
localStorage.setItem(consentKey, 'accepted');
if (banner) banner.classList.add('hidden');
loadGA();
});
}
if (declineBtn) {
declineBtn.addEventListener('click', () => {
localStorage.setItem(consentKey, 'declined');
if (banner) banner.classList.add('hidden');
});
}
})();
</script>
<script>
(() => {
const dialog = document.getElementById('confirm-dialog');
const titleEl = document.getElementById('confirm-title');
const msgEl = document.getElementById('confirm-message');
const okBtn = document.getElementById('confirm-ok');
const cancelBtn = document.getElementById('confirm-cancel');
let resolver = null;
const close = (result) => {
if (!dialog) return;
dialog.classList.add('hidden');
dialog.classList.remove('flex');
if (resolver) resolver(result);
resolver = null;
};
window.dewemojiConfirm = (message, options = {}) => {
if (!dialog) return Promise.resolve(false);
if (titleEl) titleEl.textContent = options.title || 'Confirm action';
if (msgEl) msgEl.textContent = message || 'Are you sure?';
if (okBtn) okBtn.textContent = options.okText || 'Confirm';
if (cancelBtn) cancelBtn.textContent = options.cancelText || 'Cancel';
if (okBtn) {
okBtn.classList.toggle('bg-rose-500', options.danger !== false);
okBtn.classList.toggle('hover:bg-rose-600', options.danger !== false);
okBtn.classList.toggle('bg-brand-ocean', options.danger === false);
okBtn.classList.toggle('hover:bg-brand-ocean/90', options.danger === false);
}
dialog.classList.remove('hidden');
dialog.classList.add('flex');
return new Promise((resolve) => {
resolver = resolve;
});
};
okBtn?.addEventListener('click', () => close(true));
cancelBtn?.addEventListener('click', () => close(false));
dialog?.addEventListener('click', (event) => {
if (event.target === dialog) close(false);
});
dialog?.querySelector(':scope > div.absolute')?.addEventListener('click', () => close(false));
document.addEventListener('keydown', (event) => {
if (dialog?.classList.contains('hidden')) return;
if (event.key === 'Escape') close(false);
});
document.addEventListener('submit', async (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const message = form.getAttribute('data-confirm');
if (!message || form.dataset.confirmed === 'true') return;
event.preventDefault();
const ok = await window.dewemojiConfirm(message, {
title: form.getAttribute('data-confirm-title') || undefined,
okText: form.getAttribute('data-confirm-ok') || undefined,
cancelText: form.getAttribute('data-confirm-cancel') || undefined,
danger: form.getAttribute('data-confirm-danger') !== 'false',
});
if (ok) {
form.dataset.confirmed = 'true';
form.submit();
}
});
})();
</script>
@stack('scripts')
</body>
</html>

View File

@@ -1,7 +1,7 @@
@extends('site.layout')
@section('title', 'Pricing - Dewemoji')
@section('meta_description', 'Choose Dewemoji pricing for Free, Pro subscription, and Lifetime access for website, extension, and API usage.')
@section('meta_description', 'Choose Dewemoji pricing for Free, Personal subscription, and Lifetime access for website, extension, and API usage.')
@push('jsonld')
<script type="application/ld+json">
@@ -10,20 +10,20 @@
"@@graph": [
{
"@@type": "Product",
"name": "Dewemoji Pro License",
"description": "One Pro license unlocks Dewemoji extension and API access.",
"name": "Dewemoji Personal",
"description": "Personal plan unlocks Dewemoji extension and API access.",
"brand": {"@@type": "Brand", "name": "Dewemoji"},
"offers": [
{"@@type":"Offer","price":"3.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"},
{"@@type":"Offer","price":"27.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"}
{"@@type":"Offer","price":"30000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"},
{"@@type":"Offer","price":"300000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-subscription"}
]
},
{
"@@type": "Product",
"name": "Dewemoji Lifetime License",
"name": "Dewemoji Lifetime",
"description": "Lifetime access to Dewemoji extension and API.",
"brand": {"@@type": "Brand", "name": "Dewemoji"},
"offers": {"@@type":"Offer","price":"69.00","priceCurrency":"USD","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-lifetime"}
"offers": {"@@type":"Offer","price":"900000","priceCurrency":"IDR","availability":"https://schema.org/InStock","url":"https://dwindown.gumroad.com/l/dewemoji-pro-lifetime"}
}
]
}
@@ -32,6 +32,22 @@
@section('content')
<div class="flex h-screen w-full">
<style>
#qris-code canvas,
#qris-code img {
background: #ffffff;
}
.qris-modal-card.glass-card:hover {
background: #ffffff !important;
border-color: rgba(15, 23, 42, 0.12) !important;
transform: none !important;
box-shadow: none !important;
}
html.dark .qris-modal-card.glass-card:hover {
background: rgba(2, 6, 23, 0.9) !important;
border-color: rgba(255, 255, 255, 0.08) !important;
}
</style>
<aside class="hidden lg:flex w-20 lg:w-64 h-full glass-panel flex-col justify-between p-4 z-50 shrink-0">
<div>
<div class="flex items-center gap-3 px-2 mb-8 mt-2">
@@ -56,6 +72,16 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span>
</a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span>
</a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all">
<i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span>
</a>
@@ -66,9 +92,28 @@
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0 flex justify-end items-center gap-4">
<span class="text-xs text-gray-500 hidden md:block">Billing shown in USD</span>
<header class="glass-header px-6 py-5 shrink-0 flex justify-between items-center gap-4">
<div class="flex items-center gap-3 text-xs text-gray-500">
<span>Prices shown in <strong class="text-gray-300">{{ $currencyPref ?? 'USD' }}</strong></span>
<form method="POST" action="{{ route('pricing.currency') }}" class="flex items-center gap-2">
@csrf
<button type="submit" name="currency" value="IDR"
class="px-3 py-1.5 rounded-full text-[11px] font-semibold border {{ ($currencyPref ?? 'USD') === 'IDR' ? 'bg-white/10 text-white border-white/10' : 'text-gray-400 border-white/5 hover:text-white hover:border-white/10' }}">
IDR
</button>
<button type="submit" name="currency" value="USD"
class="px-3 py-1.5 rounded-full text-[11px] font-semibold border {{ ($currencyPref ?? 'USD') === 'USD' ? 'bg-white/10 text-white border-white/10' : 'text-gray-400 border-white/5 hover:text-white hover:border-white/10' }}">
USD
</button>
</form>
<span class="hidden md:inline">PayPal fixed rate Rp {{ number_format($usdRate ?? 15000) }}/USD</span>
</div>
<button class="text-sm font-semibold text-brand-oceanSoft hover:text-white transition-colors">Restore Purchases</button>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">
@@ -78,56 +123,558 @@
<p class="text-gray-400">Use Dewemoji for search, extension, and API in one license system.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8">
@php
$pref = $currencyPref ?? 'USD';
$monthlyIdr = $pricing['personal_monthly']['idr'] ?? 30000;
$annualIdr = $pricing['personal_annual']['idr'] ?? 300000;
$lifetimeIdr = $pricing['personal_lifetime']['idr'] ?? 900000;
$monthlyUsd = $pricing['personal_monthly']['usd'] ?? 2;
$annualUsd = $pricing['personal_annual']['usd'] ?? 20;
$lifetimeUsd = $pricing['personal_lifetime']['usd'] ?? 60;
$qrisUrl = $payments['qris_url'] ?? '';
$paypalUrl = $payments['paypal_url'] ?? '';
$paypalJoiner = $paypalUrl && str_contains($paypalUrl, '?') ? '&' : '?';
$paypalLifetimeUrl = $paypalUrl ? $paypalUrl.$paypalJoiner.'plan=personal_lifetime' : '';
$canQris = $pakasirEnabled ?? false;
$paypalEnabled = $paypalEnabled ?? false;
$paypalPlans = $paypalPlans ?? ['personal_monthly' => false, 'personal_annual' => false];
@endphp
<div class="mb-6 flex flex-wrap items-center justify-center gap-3 text-sm text-gray-400">
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center">
<button type="button" class="period-toggle rounded-full px-4 py-2 font-semibold text-gray-200" data-period="monthly">Monthly</button>
<button type="button" class="period-toggle rounded-full px-4 py-2 font-semibold text-gray-400 hover:text-gray-200" data-period="annual">Annual</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 mb-8" data-default-currency="{{ $pref === 'IDR' ? 'IDR' : 'USD' }}">
<section class="glass-card rounded-3xl p-6 flex flex-col">
<h2 class="font-display text-xl font-bold">Starter</h2>
<p class="text-sm text-gray-500 mt-1">For casual usage</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$0</span><span class="text-gray-500">/mo</span></div>
<ul class="space-y-2 text-sm text-gray-300 flex-1">
<li>Extension access</li>
<li>Basic API testing</li>
<li>30 daily free queries</li>
</ul>
<div class="flex-1"></div>
<button class="mt-6 w-full py-2.5 rounded-xl bg-white/5 border border-white/10 text-gray-300">Current Plan</button>
</section>
<section class="rounded-3xl p-6 flex flex-col border border-brand-ocean/40 bg-gradient-to-b from-brand-ocean/15 to-transparent shadow-[0_0_28px_rgba(32,83,255,0.18)] md:-translate-y-2">
<div class="inline-flex w-fit mb-3 px-2 py-1 text-[10px] font-bold rounded-full bg-brand-sun text-black">MOST POPULAR</div>
<h2 class="font-display text-xl font-bold text-brand-oceanSoft">Pro</h2>
<p class="text-sm text-gray-400 mt-1">For power users & teams</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$3</span><span class="text-gray-400">/mo</span> <span class="text-sm text-gray-500">or $27/yr</span></div>
<ul class="space-y-2 text-sm text-gray-200 flex-1">
<li>Semantic search engine</li>
<li>API Pro access</li>
<li>3 active Chrome profiles</li>
<li>Priority support</li>
</ul>
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-subscription" target="_blank" rel="noopener noreferrer" class="mt-6 w-full py-2.5 rounded-xl bg-brand-ocean hover:bg-brand-oceanSoft text-white font-semibold text-center">Upgrade to Pro</a>
<section class="relative rounded-3xl p-6 flex flex-col border border-brand-ocean/40 bg-gradient-to-b from-brand-ocean/15 to-transparent shadow-[0_0_28px_rgba(32,83,255,0.18)] md:-translate-y-2">
<div class="absolute -top-3 right-4 rounded-full bg-brand-sun text-black text-[10px] font-bold px-3 py-1 shadow-lg">Most Popular</div>
<div class="flex items-center justify-between gap-3">
<h2 class="font-display text-xl font-bold text-brand-oceanSoft">Personal</h2>
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center text-xs">
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-200" data-currency="USD">USD</button>
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-400 hover:text-gray-200" data-currency="IDR">IDR</button>
</div>
</div>
<p class="text-sm text-gray-400 mt-1">For private keywords + sync</p>
<div class="mt-4 mb-2" id="personal-price"
data-monthly-usd="{{ rtrim(rtrim(number_format($monthlyUsd, 2), '0'), '.') }}"
data-annual-usd="{{ rtrim(rtrim(number_format($annualUsd, 2), '0'), '.') }}"
data-monthly-idr="{{ number_format($monthlyIdr) }}"
data-annual-idr="{{ number_format($annualIdr) }}">
<span class="text-4xl font-bold">$0</span><span class="text-gray-400">/mo</span>
</div>
<div class="mb-6 text-xs text-gray-500" id="personal-secondary"
data-monthly-usd-note="≈ Rp {{ number_format($monthlyIdr) }} (QRIS)"
data-annual-usd-note="≈ Rp {{ number_format($annualIdr) }} (QRIS)"
data-monthly-idr-note="${{ rtrim(rtrim(number_format($monthlyUsd, 2), '0'), '.') }} (PayPal fixed rate)"
data-annual-idr-note="${{ rtrim(rtrim(number_format($annualUsd, 2), '0'), '.') }} (PayPal fixed rate)">
</div>
<div class="flex-1"></div>
<div class="mt-6 space-y-2">
<div class="hidden text-xs text-gray-500" id="personal-pay-note"></div>
<button type="button"
id="personal-pay-btn"
data-paypal-enabled="{{ $paypalEnabled && $paypalPlans['personal_monthly'] ? 'true' : 'false' }}"
data-paypal-annual-enabled="{{ $paypalEnabled && $paypalPlans['personal_annual'] ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
class="!text-white w-full py-2.5 rounded-xl bg-brand-ocean hover:bg-brand-oceanSoft text-white font-semibold text-center block">
Pay now
</button>
</div>
<div class="hidden">
<button type="button" data-paypal-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-paypal-plan="personal_annual" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_monthly" data-original="Start Personal"></button>
<button type="button" data-qris-plan="personal_annual" data-original="Start Personal"></button>
</div>
</section>
<section class="glass-card rounded-3xl p-6 flex flex-col">
<h2 class="font-display text-xl font-bold">Lifetime</h2>
<div class="flex items-center justify-between gap-3">
<h2 class="font-display text-xl font-bold">Lifetime</h2>
<div class="rounded-full border border-white/10 bg-white/5 p-1 flex items-center text-xs">
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-200" data-currency="USD">USD</button>
<button type="button" class="currency-toggle rounded-full px-3 py-1.5 font-semibold text-gray-400 hover:text-gray-200" data-currency="IDR">IDR</button>
</div>
</div>
<p class="text-sm text-gray-500 mt-1">Pay once, use forever</p>
<div class="mt-5 mb-6"><span class="text-4xl font-bold">$69</span></div>
<ul class="space-y-2 text-sm text-gray-300 flex-1">
<li>Lifetime extension + API</li>
<li>Future updates included</li>
<li>IDR/Mayar available on request</li>
</ul>
<a href="https://dwindown.gumroad.com/l/dewemoji-pro-lifetime" target="_blank" rel="noopener noreferrer" class="mt-6 w-full py-2.5 rounded-xl bg-brand-sun hover:bg-brand-sunSoft text-black font-semibold text-center">Buy Lifetime</a>
<div class="mt-5 mb-2" id="lifetime-price"
data-lifetime-usd="{{ rtrim(rtrim(number_format($lifetimeUsd, 2), '0'), '.') }}"
data-lifetime-idr="{{ number_format($lifetimeIdr) }}">
<span class="text-4xl font-bold">$0</span>
</div>
<div class="mb-4 text-xs text-gray-500" id="lifetime-secondary"
data-lifetime-usd-note="≈ Rp {{ number_format($lifetimeIdr) }} (QRIS)"
data-lifetime-idr-note="${{ rtrim(rtrim(number_format($lifetimeUsd, 2), '0'), '.') }} (PayPal fixed rate)">
</div>
<div class="flex-1"></div>
<div class="mt-6 space-y-2">
<div class="hidden text-xs text-gray-500" id="lifetime-pay-note"></div>
<button type="button"
id="lifetime-pay-btn"
data-paypal-enabled="{{ $paypalLifetimeUrl ? 'true' : 'false' }}"
data-qris-enabled="{{ $canQris ? 'true' : 'false' }}"
class="force-white w-full py-2.5 rounded-xl border border-brand-ocean/60 text-brand-ocean font-semibold text-center block hover:bg-brand-ocean/10">
Pay now
</button>
</div>
<div class="hidden">
<a href="{{ $paypalLifetimeUrl ?: '#' }}"
target="_blank" rel="noopener noreferrer"
data-paypal-lifetime="true"
class="{{ $paypalLifetimeUrl ? '' : 'pointer-events-none' }}">
PayPal Lifetime
</a>
<button type="button"
data-qris-plan="personal_lifetime" data-original="Get Lifetime Access">
QRIS Lifetime
</button>
</div>
</section>
</div>
<section class="glass-card rounded-2xl p-6 text-sm text-gray-300">
<h3 class="font-semibold text-white mb-2">Licensing basics</h3>
<ul class="list-disc pl-5 space-y-1">
<li>One key unlocks extension and API.</li>
<li>Use <code>Authorization: Bearer YOUR_LICENSE_KEY</code> for API requests.</li>
<li>Maximum 3 active Chrome profiles per license.</li>
</ul>
<h3 class="font-semibold text-white mb-4">Plan comparison</h3>
<div class="overflow-x-auto">
<table class="min-w-full text-left text-sm">
<thead class="text-xs uppercase tracking-[0.2em] text-gray-400">
<tr>
<th class="py-3 pr-4">Feature</th>
<th class="py-3 pr-4">Starter</th>
<th class="py-3 pr-4">Personal</th>
<th class="py-3 pr-4">Lifetime</th>
</tr>
</thead>
<tbody class="divide-y divide-white/10 text-gray-300">
<tr>
<td class="py-3 pr-4 border-t border-white/10">Emoji search + discovery</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Private keywords</td>
<td class="py-3 pr-4 border-t border-white/10">Up to 20</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Keyword sync</td>
<td class="py-3 pr-4 border-t border-white/10">Account-only</td>
<td class="py-3 pr-4 border-t border-white/10">All channels</td>
<td class="py-3 pr-4 border-t border-white/10">All channels</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">API access</td>
<td class="py-3 pr-4 border-t border-white/10">Not included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
<td class="py-3 pr-4 border-t border-white/10">Included</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Public search usage</td>
<td class="py-3 pr-4 border-t border-white/10">Hourly limits apply</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
<td class="py-3 pr-4 border-t border-white/10">Unlimited</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Support</td>
<td class="py-3 pr-4 border-t border-white/10">Standard</td>
<td class="py-3 pr-4 border-t border-white/10">Priority</td>
<td class="py-3 pr-4 border-t border-white/10">Priority</td>
</tr>
<tr>
<td class="py-3 pr-4 border-t border-white/10">Updates</td>
<td class="py-3 pr-4 border-t border-white/10">Regular</td>
<td class="py-3 pr-4 border-t border-white/10">All updates</td>
<td class="py-3 pr-4 border-t border-white/10">All updates</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</div>
</main>
</div>
<div id="qris-modal" class="hidden fixed inset-0 z-50 items-center justify-center">
<div class="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div class="relative z-10 w-full max-w-lg rounded-3xl glass-card qris-modal-card p-6 bg-white/95 text-slate-900 dark:bg-slate-950/90 dark:text-white">
<div class="flex items-center justify-between">
<div>
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">QRIS Payment</div>
<div class="mt-2 text-xl font-semibold text-white">Scan to pay</div>
</div>
</div>
<div class="mt-6 grid gap-4 md:grid-cols-2 items-center">
<div class="rounded-2xl bg-white/10 border border-white/10 p-4 flex items-center justify-center">
<div id="qris-code" class="rounded-xl bg-white p-3 shadow-lg"></div>
</div>
<div class="space-y-3 text-sm text-gray-300">
<div class="rounded-xl bg-white/5 border border-white/10 p-3">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Amount</div>
<div id="qris-amount" class="mt-1 text-lg font-semibold text-white">Rp 0</div>
</div>
<div class="rounded-xl bg-white/5 border border-white/10 p-3">
<div class="text-xs uppercase tracking-[0.2em] text-gray-400">Expires</div>
<div id="qris-expiry" class="mt-1 text-sm text-gray-300">Complete within 30 minutes</div>
</div>
<div id="qris-text" class="hidden"></div>
</div>
</div>
<div class="mt-6 flex items-center justify-end gap-2">
<button id="qris-cancel" class="rounded-full bg-rose-500 text-white font-semibold px-4 py-2 text-sm hover:bg-rose-600">Cancel payment</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js"></script>
<script>
(() => {
const buttons = document.querySelectorAll('[data-paypal-plan]');
if (!buttons.length) return;
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const isAuthed = @json(auth()->check());
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
const plan = btn.dataset.paypalPlan;
if (!plan) return;
if (!isAuthed) {
window.location.href = "{{ route('login') }}";
return;
}
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Redirecting...';
try {
const res = await fetch("{{ route('billing.paypal.create') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ plan_code: plan }),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.approve_url) {
const reason = data?.error ? ` (${data.error})` : '';
alert('Could not start PayPal checkout. Please try again.' + reason);
btn.disabled = false;
btn.textContent = original;
return;
}
window.location.href = data.approve_url;
} catch (e) {
alert('Checkout failed. Please try again.');
btn.disabled = false;
btn.textContent = original;
}
});
});
})();
</script>
<script>
(() => {
const periodButtons = document.querySelectorAll('.period-toggle');
const currencyButtons = document.querySelectorAll('.currency-toggle');
const priceWrap = document.getElementById('personal-price');
const secondary = document.getElementById('personal-secondary');
const payBtn = document.getElementById('personal-pay-btn');
const payNote = document.getElementById('personal-pay-note');
const lifetimePrice = document.getElementById('lifetime-price');
const lifetimeSecondary = document.getElementById('lifetime-secondary');
const lifetimePay = document.getElementById('lifetime-pay-btn');
const lifetimeNote = document.getElementById('lifetime-pay-note');
if (!priceWrap || !secondary || !payBtn || !lifetimePrice || !lifetimeSecondary || !lifetimePay) return;
let period = 'monthly';
let currency = document.querySelector('[data-default-currency]')?.dataset.defaultCurrency || 'USD';
const setActive = (nodes, value) => {
nodes.forEach((btn) => {
const active = btn.dataset[btn.classList.contains('period-toggle') ? 'period' : 'currency'] === value;
btn.classList.toggle('text-gray-200', active);
btn.classList.toggle('text-gray-400', !active);
});
};
const paypalIcon = `<svg role="img" viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4"><path fill="currentColor" d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"/></svg>`;
const qrisIcon = `<svg viewBox="0 0 24 24" aria-hidden="true" class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h6v6H3z"/><path d="M15 3h6v6h-6z"/><path d="M3 15h6v6H3z"/><path d="M15 15h2v2h-2z"/><path d="M17 19h2v2h-2z"/><path d="M19 15h2v2h-2z"/><path d="M15 19h2v2h-2z"/></svg>`;
const setButtonLabel = (btn, label, icon) => {
if (!btn) return;
btn.classList.add('inline-flex', 'items-center', 'justify-center', 'gap-2');
btn.innerHTML = `${icon}<span>${label}</span>`;
};
const updatePrice = () => {
const amount = currency === 'USD'
? priceWrap.dataset[period === 'monthly' ? 'monthlyUsd' : 'annualUsd']
: priceWrap.dataset[period === 'monthly' ? 'monthlyIdr' : 'annualIdr'];
const suffix = period === 'monthly' ? '/mo' : '/yr';
if (currency === 'USD') {
priceWrap.innerHTML = `<span class="text-4xl font-bold">$${amount}</span><span class="text-gray-400">${suffix}</span>`;
secondary.textContent = secondary.dataset[`${period}UsdNote`] || '—';
} else {
priceWrap.innerHTML = `<span class="text-4xl font-bold">Rp ${amount}</span><span class="text-gray-400">${suffix}</span>`;
secondary.textContent = secondary.dataset[`${period}IdrNote`] || '—';
}
const canPaypal = (period === 'monthly' ? payBtn.dataset.paypalEnabled === 'true' : payBtn.dataset.paypalAnnualEnabled === 'true');
const canQris = payBtn.dataset.qrisEnabled === 'true';
let disabled = false;
let label = 'Start Personal';
let note = '';
if (currency === 'USD') {
disabled = !canPaypal;
label = 'Start Personal';
note = canPaypal ? '' : 'PayPal is not configured for this plan.';
payBtn.classList.remove('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
payBtn.classList.add('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
payBtn.classList.remove('text-white');
} else {
disabled = !canQris;
label = 'Start Personal';
note = canQris ? '' : 'QRIS is not available right now.';
payBtn.classList.remove('bg-brand-ocean', 'hover:bg-brand-oceanSoft', 'text-white');
payBtn.classList.add('bg-brand-sun', 'hover:bg-brand-sunSoft', 'text-black');
}
setButtonLabel(payBtn, label, currency === 'USD' ? paypalIcon : qrisIcon);
payBtn.disabled = disabled;
payBtn.classList.toggle('opacity-60', disabled);
payBtn.classList.toggle('pointer-events-none', disabled);
if (payNote) {
payNote.textContent = note;
payNote.classList.toggle('hidden', note === '');
}
const lifetimeAmount = currency === 'USD'
? lifetimePrice.dataset.lifetimeUsd
: lifetimePrice.dataset.lifetimeIdr;
if (currency === 'USD') {
lifetimePrice.innerHTML = `<span class="text-4xl font-bold">$${lifetimeAmount}</span>`;
lifetimeSecondary.textContent = lifetimeSecondary.dataset.lifetimeUsdNote || '—';
} else {
lifetimePrice.innerHTML = `<span class="text-4xl font-bold">Rp ${lifetimeAmount}</span>`;
lifetimeSecondary.textContent = lifetimeSecondary.dataset.lifetimeIdrNote || '—';
}
const canLifetimePaypal = lifetimePay.dataset.paypalEnabled === 'true';
const canLifetimeQris = lifetimePay.dataset.qrisEnabled === 'true';
let lifetimeDisabled = false;
let lifetimeLabel = 'Get Lifetime Access';
let lifetimeHint = '';
if (currency === 'USD') {
lifetimeDisabled = !canLifetimePaypal;
lifetimeLabel = 'Get Lifetime Access';
lifetimeHint = canLifetimePaypal ? '' : 'PayPal is not configured.';
lifetimePay.classList.remove('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
lifetimePay.classList.add('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
} else {
lifetimeDisabled = !canLifetimeQris;
lifetimeLabel = 'Get Lifetime Access';
lifetimeHint = canLifetimeQris ? '' : 'QRIS is not available right now.';
lifetimePay.classList.remove('border-brand-ocean/60', 'text-brand-ocean', 'hover:bg-brand-ocean/10');
lifetimePay.classList.add('border-brand-sun/60', 'text-brand-sun', 'hover:bg-brand-sun/10');
}
setButtonLabel(lifetimePay, lifetimeLabel, currency === 'USD' ? paypalIcon : qrisIcon);
lifetimePay.disabled = lifetimeDisabled;
lifetimePay.classList.toggle('opacity-60', lifetimeDisabled);
lifetimePay.classList.toggle('pointer-events-none', lifetimeDisabled);
if (lifetimeNote) {
lifetimeNote.textContent = lifetimeHint;
lifetimeNote.classList.toggle('hidden', lifetimeHint === '');
}
};
periodButtons.forEach((btn) => {
btn.addEventListener('click', () => {
period = btn.dataset.period;
setActive(periodButtons, period);
updatePrice();
});
});
currencyButtons.forEach((btn) => {
btn.addEventListener('click', () => {
currency = btn.dataset.currency;
setActive(currencyButtons, currency);
updatePrice();
});
});
payBtn.addEventListener('click', async () => {
if (payBtn.disabled) return;
const plan = period === 'monthly' ? 'personal_monthly' : 'personal_annual';
if (currency === 'USD') {
const button = document.querySelector(`[data-paypal-plan="${plan}"]`);
button?.click();
return;
}
const button = document.querySelector(`[data-qris-plan="${plan}"]`);
button?.click();
});
lifetimePay.addEventListener('click', async () => {
if (lifetimePay.disabled) return;
if (currency === 'USD') {
const paypal = document.querySelector('[data-paypal-lifetime="true"]');
paypal?.click();
return;
}
const button = document.querySelector('[data-qris-plan="personal_lifetime"]');
button?.click();
});
setActive(periodButtons, period);
setActive(currencyButtons, currency);
updatePrice();
})();
</script>
<script>
(() => {
const buttons = document.querySelectorAll('[data-qris-plan]');
if (!buttons.length) return;
const csrf = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const isAuthed = @json(auth()->check());
const modal = document.getElementById('qris-modal');
const qrTarget = document.getElementById('qris-code');
const qrText = document.getElementById('qris-text');
const qrAmount = document.getElementById('qris-amount');
const qrExpiry = document.getElementById('qris-expiry');
const cancelBtn = document.getElementById('qris-cancel');
let currentOrderId = null;
let modalOpen = false;
const openModal = () => {
if (!modal) return;
modal.classList.remove('hidden');
modal.classList.add('flex');
modalOpen = true;
};
const closeModal = () => {
if (!modal) return;
modal.classList.add('hidden');
modal.classList.remove('flex');
modalOpen = false;
};
cancelBtn?.addEventListener('click', async () => {
const ok = await window.dewemojiConfirm('Cancel this QRIS payment? You will need to start a new checkout.', {
title: 'Cancel payment',
okText: 'Cancel payment',
});
if (!ok) return;
try {
await fetch("{{ route('billing.pakasir.cancel') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ order_id: currentOrderId }),
});
} catch (e) {
// best-effort cancel
} finally {
currentOrderId = null;
closeModal();
}
});
document.addEventListener('keydown', (event) => {
if (!modalOpen) return;
if (event.key === 'Escape') {
event.preventDefault();
}
});
const formatExpiry = (value) => {
if (!value) return null;
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return null;
return new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(parsed);
};
buttons.forEach((btn) => {
btn.addEventListener('click', async () => {
const plan = btn.dataset.qrisPlan;
if (!plan) return;
if (!isAuthed) {
window.location.href = "{{ route('login') }}";
return;
}
const original = btn.dataset.original || btn.textContent;
btn.disabled = true;
btn.textContent = 'Generating QR...';
try {
const res = await fetch("{{ route('billing.pakasir.create') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(csrf ? { 'X-CSRF-TOKEN': csrf } : {}),
},
body: JSON.stringify({ plan_code: plan }),
});
const data = await res.json().catch(() => null);
if (!res.ok || !data?.payment_number) {
alert('Could not generate QRIS. Please try again.');
btn.disabled = false;
btn.textContent = original;
return;
}
currentOrderId = data.order_id || null;
if (qrTarget) qrTarget.innerHTML = '';
if (qrTarget && window.QRCode) {
new QRCode(qrTarget, {
text: data.payment_number,
width: 220,
height: 220,
colorDark: '#0b0b0f',
colorLight: '#ffffff',
});
}
if (qrText) qrText.textContent = data.payment_number;
if (qrAmount) qrAmount.textContent = `Rp ${Number(data.total_payment || data.amount || 0).toLocaleString('id-ID')}`;
if (qrExpiry) {
const formatted = formatExpiry(data.expired_at);
qrExpiry.textContent = formatted ? `Expires ${formatted}` : 'Complete within 30 minutes';
}
openModal();
btn.disabled = false;
btn.textContent = original;
} catch (e) {
alert('QRIS request failed. Please try again.');
btn.disabled = false;
btn.textContent = original;
}
});
});
})();
</script>
@endpush

View File

@@ -5,10 +5,10 @@
@push('head')
<style>
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: #fff; font-weight: 700; }
.legal-h3 { font-family: 'Space Grotesk', sans-serif; margin-top: 1.4rem; margin-bottom: .5rem; font-size: 1.05rem; color: #e5e7eb; font-weight: 600; }
.legal-p { color: #9ca3af; line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: #9ca3af; margin-bottom: .9rem; }
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: var(--app-fg); font-weight: 700; }
.legal-h3 { font-family: 'Space Grotesk', sans-serif; margin-top: 1.4rem; margin-bottom: .5rem; font-size: 1.05rem; color: var(--muted-strong); font-weight: 600; }
.legal-p { color: var(--muted-text); line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: var(--muted-text); margin-bottom: .9rem; }
</style>
@endpush
@@ -30,6 +30,12 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl bg-white/10 text-brand-sun border border-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
@@ -41,9 +47,16 @@
<div class="text-[11px] uppercase tracking-wider text-gray-500">Legal / Privacy Policy</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Privacy Policy</h1>
</div>
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
</header>

View File

@@ -35,15 +35,28 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
</aside>
<main class="flex-1 flex flex-col h-full min-w-0 relative z-10">
<header class="glass-header px-6 py-5 shrink-0">
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Support</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Support Center</h1>
<header class="glass-header px-6 py-5 shrink-0 flex items-center justify-between">
<div>
<div class="text-[11px] uppercase tracking-wider text-gray-500 mb-1">Public / Support</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Support Center</h1>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</header>
<div class="flex-1 overflow-y-auto p-6 md:p-10">

View File

@@ -5,9 +5,9 @@
@push('head')
<style>
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: #fff; font-weight: 700; }
.legal-p { color: #9ca3af; line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: #9ca3af; margin-bottom: .9rem; }
.legal-h2 { font-family: 'Space Grotesk', sans-serif; margin-top: 2rem; margin-bottom: .75rem; font-size: 1.3rem; color: var(--app-fg); font-weight: 700; }
.legal-p { color: var(--muted-text); line-height: 1.7; margin-bottom: .9rem; }
.legal-ul { list-style: disc; padding-left: 1.2rem; color: var(--muted-text); margin-bottom: .9rem; }
</style>
@endpush
@@ -29,6 +29,12 @@
</nav>
</div>
<div class="space-y-1">
@auth
<a href="{{ route('dashboard.overview') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="layout-dashboard" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Dashboard</span></a>
@endauth
@guest
<a href="{{ route('login') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="log-in" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Login</span></a>
@endguest
<a href="{{ route('privacy') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl text-gray-400 hover:text-white hover:bg-white/5 transition-all"><i data-lucide="shield-check" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Privacy</span></a>
<a href="{{ route('terms') }}" class="flex items-center gap-4 px-3 py-3 rounded-xl bg-white/10 text-brand-sun border border-white/5 transition-all"><i data-lucide="file-text" class="w-5 h-5"></i><span class="text-sm hidden lg:block">Terms</span></a>
</div>
@@ -40,9 +46,16 @@
<div class="text-[11px] uppercase tracking-wider text-gray-500">Legal / Terms</div>
<h1 class="font-display text-2xl md:text-3xl font-bold">Terms of Service</h1>
</div>
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
<div class="flex items-center gap-4">
<div class="text-right">
<div class="text-[10px] uppercase tracking-wider text-gray-500">Last Updated</div>
<div class="text-sm text-gray-200">February 5, 2026</div>
</div>
<button id="theme-toggle" class="w-9 h-9 rounded-full theme-surface border border-white/10 shadow-lg flex items-center justify-center text-gray-300 hover:text-white transition-colors">
<span class="sr-only">Toggle theme</span>
<i data-lucide="moon" class="w-4 h-4" data-theme-icon="dark"></i>
<i data-lucide="sun" class="w-4 h-4 hidden" data-theme-icon="light"></i>
</button>
</div>
</header>

View File

@@ -0,0 +1,43 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="Pagination" class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between text-sm">
<div class="text-xs text-gray-400">
Showing
<span class="font-semibold text-gray-200">{{ $paginator->firstItem() }}</span>
to
<span class="font-semibold text-gray-200">{{ $paginator->lastItem() }}</span>
of
<span class="font-semibold text-gray-200">{{ $paginator->total() }}</span>
results
</div>
<div class="flex items-center gap-2">
@if ($paginator->onFirstPage())
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-500">Prev</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/5">Prev</a>
@endif
@foreach ($elements as $element)
@if (is_string($element))
<span class="px-2 text-gray-500">{{ $element }}</span>
@endif
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<span class="rounded-full bg-white/10 px-3 py-1 text-xs font-semibold text-white">{{ $page }}</span>
@else
<a href="{{ $url }}" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/5">{{ $page }}</a>
@endif
@endforeach
@endif
@endforeach
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-200 hover:bg-white/5">Next</a>
@else
<span class="rounded-full border border-white/10 px-3 py-1 text-xs text-gray-500">Next</span>
@endif
</div>
</nav>
@endif

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'Dewemoji') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">