Deploy Ladill QR Plus / deploy (push) Failing after 1s
Utility QR types only (URL, WiFi, link list, business, app download) with SSO, Billing API integration, public /q resolver, and qr-plus:import for platform migration. vCard and commerce types stay on the platform. Co-authored-by: Cursor <cursoragent@cursor.com>
268 lines
16 KiB
PHP
268 lines
16 KiB
PHP
<x-user-layout>
|
||
<x-slot name="title">Create QR Code</x-slot>
|
||
|
||
@php
|
||
$modalId = 'service-topup-qr';
|
||
$openTopup = session('open_topup_modal') === 'qr';
|
||
$defaultStyle = \App\Support\Qr\QrStyleDefaults::merge(old('style'));
|
||
@endphp
|
||
|
||
<div class="mx-auto max-w-6xl space-y-6"
|
||
x-data="qrCustomizer({
|
||
previewUrl: @js(route('qr-codes.style-preview')),
|
||
csrf: @js(csrf_token()),
|
||
shortCode: 'preview',
|
||
style: @js($defaultStyle),
|
||
balance: @js((float) $wallet->spendableBalance()),
|
||
price: @js((float) $pricePerQr),
|
||
topupModalId: @js($modalId),
|
||
type: @js(old('type', 'url')),
|
||
})">
|
||
|
||
@if(session('error'))
|
||
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{{ session('error') }}</div>
|
||
@endif
|
||
|
||
{{-- Mobile page header --}}
|
||
<div class="flex items-center gap-3 py-1 lg:hidden">
|
||
<a href="{{ route('qr-codes.index') }}"
|
||
class="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-200 bg-white shadow-sm text-slate-500 hover:text-slate-900 transition">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||
</svg>
|
||
</a>
|
||
<span class="text-sm font-semibold text-slate-900">Create QR code</span>
|
||
</div>
|
||
|
||
{{-- Desktop page header --}}
|
||
<div class="hidden lg:block">
|
||
<a href="{{ route('qr-codes.index') }}" class="inline-flex items-center gap-1 text-sm text-slate-500 hover:text-slate-700">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5"/></svg>
|
||
QR Codes
|
||
</a>
|
||
<h1 class="mt-2 text-2xl font-bold text-slate-900">Create QR code</h1>
|
||
<p class="mt-1 text-sm text-slate-500">Customize your code, then create.</p>
|
||
</div>
|
||
|
||
<form x-ref="createForm" action="{{ route('qr-codes.store') }}" method="POST" enctype="multipart/form-data"
|
||
class="grid grid-cols-1 gap-8 lg:grid-cols-[380px_1fr]"
|
||
@submit="submitOrTopup($event)">
|
||
@csrf
|
||
|
||
{{-- Left: Sticky preview (desktop only) --}}
|
||
<div class="hidden lg:block lg:sticky lg:top-6 lg:self-start">
|
||
@include('qr-codes.partials.qr-preview-card', ['showDownloads' => false])
|
||
</div>
|
||
|
||
{{-- Right: Controls --}}
|
||
<div class="space-y-5 pb-4 lg:pb-0">
|
||
|
||
{{-- Content first --}}
|
||
<div class="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm">
|
||
<div class="border-b border-slate-100 bg-slate-50/70 px-5 py-4">
|
||
<h2 class="text-sm font-semibold text-slate-900">Content</h2>
|
||
<p class="mt-0.5 text-xs text-slate-400">What should this QR code link to?</p>
|
||
</div>
|
||
|
||
<div class="space-y-4 p-5">
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-600">Label</label>
|
||
<input type="text" name="label" value="{{ old('label') }}" required maxlength="120"
|
||
class="mt-1.5 w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-sm placeholder:text-slate-400 focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30"
|
||
placeholder="e.g. Shop flyer, WiFi guest, Menu">
|
||
</div>
|
||
|
||
{{-- Custom link --}}
|
||
<div x-data="{
|
||
slug: @js(old('custom_short_code', '')),
|
||
status: '',
|
||
timer: null,
|
||
checkUrl: @js(route('qr-codes.check-slug')),
|
||
baseUrl: @js(\App\Models\QrCode::publicBaseUrl() . '/q'),
|
||
check() {
|
||
clearTimeout(this.timer);
|
||
const s = this.slug.trim();
|
||
if (s === '') { this.status = ''; return; }
|
||
if (!/^[a-z0-9][a-z0-9-]{1,18}[a-z0-9]$/.test(s)) { this.status = 'invalid'; return; }
|
||
this.status = 'checking';
|
||
this.timer = setTimeout(() => {
|
||
fetch(this.checkUrl + '?code=' + encodeURIComponent(s))
|
||
.then(r => r.json())
|
||
.then(d => { this.status = d.available ? 'available' : 'taken'; })
|
||
.catch(() => { this.status = ''; });
|
||
}, 400);
|
||
},
|
||
}">
|
||
<label class="block text-xs font-semibold text-slate-600">Custom link <span class="font-normal text-slate-400">(optional)</span></label>
|
||
<div class="mt-1.5 flex items-stretch rounded-xl border focus-within:border-indigo-400 focus-within:ring-1 focus-within:ring-indigo-400/30"
|
||
:class="status === 'available' ? 'border-green-400' : status === 'taken' || status === 'invalid' ? 'border-red-400' : 'border-slate-200'">
|
||
<span class="flex items-center rounded-l-xl border-r border-slate-200 bg-slate-50 px-3 text-xs text-slate-400 whitespace-nowrap select-none"
|
||
:class="status === 'available' ? 'border-green-400' : status === 'taken' || status === 'invalid' ? 'border-red-400' : 'border-slate-200'">
|
||
{{ \App\Models\QrCode::publicBaseUrl() }}/q/
|
||
</span>
|
||
<input type="text" name="custom_short_code"
|
||
x-model="slug"
|
||
@input="check()"
|
||
maxlength="20"
|
||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
|
||
class="min-w-0 flex-1 rounded-r-xl bg-white px-3 py-2.5 text-sm placeholder:text-slate-400 focus:outline-none"
|
||
placeholder="my-brand">
|
||
</div>
|
||
<div class="mt-1.5 flex items-center gap-1.5 text-[11px]">
|
||
<template x-if="status === 'checking'">
|
||
<span class="text-slate-400">Checking…</span>
|
||
</template>
|
||
<template x-if="status === 'available'">
|
||
<span class="flex items-center gap-1 text-green-600">
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5"/></svg>
|
||
Available — your link will be <span x-text="baseUrl + '/' + slug.trim()" class="font-medium text-green-700"></span>
|
||
</span>
|
||
</template>
|
||
<template x-if="status === 'taken'">
|
||
<span class="flex items-center gap-1 text-red-500">
|
||
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/></svg>
|
||
Already taken — choose another
|
||
</span>
|
||
</template>
|
||
<template x-if="status === 'invalid'">
|
||
<span class="text-red-500">3–20 chars, letters, numbers and hyphens only (no leading/trailing hyphens)</span>
|
||
</template>
|
||
<template x-if="status === ''">
|
||
<span class="text-slate-400">Leave blank to auto-generate</span>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-semibold text-slate-600">Type</label>
|
||
<div class="mt-2 grid grid-cols-2 gap-2">
|
||
@foreach($types as $key => $meta)
|
||
<label class="cursor-pointer rounded-xl border-2 p-3 transition-all"
|
||
:class="type === '{{ $key }}' ? 'border-indigo-500 bg-indigo-50/60' : 'border-slate-200 hover:border-slate-300'">
|
||
<input type="radio" name="type" value="{{ $key }}" x-model="type" class="sr-only">
|
||
@if(!empty($meta['icon']))
|
||
<img src="{{ \App\Support\Qr\QrTypeCatalog::iconUrl($meta['icon']) }}" alt="" class="mb-1.5 h-5 w-5 text-slate-900">
|
||
@endif
|
||
<p class="text-sm font-semibold text-slate-900">{{ $meta['label'] }}</p>
|
||
<p class="mt-0.5 text-[11px] leading-4 text-slate-400">{{ $meta['description'] }}</p>
|
||
</label>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
@include('qr-codes.partials.type-fields-create')
|
||
</div>
|
||
|
||
<p class="rounded-xl bg-slate-50 px-3.5 py-2.5 text-xs leading-5 text-slate-400">
|
||
Your QR always points to a Ladill short link. Change the destination any time without reprinting.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Customization --}}
|
||
@include('qr-codes.partials.customization-fields', [
|
||
'style' => $defaultStyle,
|
||
'moduleStyles' => $moduleStyles,
|
||
'cornerOuterStyles' => $cornerOuterStyles,
|
||
'cornerInnerStyles' => $cornerInnerStyles,
|
||
'frameStyles' => $frameStyles,
|
||
])
|
||
|
||
{{-- Desktop submit button --}}
|
||
<button type="button"
|
||
@click="submitOrTopup($event)"
|
||
class="hidden lg:block w-full rounded-xl bg-indigo-600 py-3 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||
Create QR code
|
||
</button>
|
||
|
||
</div>
|
||
</form>
|
||
|
||
{{-- Mobile sticky action bar --}}
|
||
<div x-show="!showPreviewModal"
|
||
class="mobile-action-bar lg:hidden">
|
||
<div class="mobile-action-bar__toolbar">
|
||
<button type="button"
|
||
@click="showPreviewModal = true"
|
||
class="flex flex-1 items-center justify-center gap-2 rounded-xl border border-slate-200 bg-slate-50 py-2.5 text-sm font-semibold text-slate-700 transition hover:bg-slate-100">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z"/>
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
|
||
</svg>
|
||
Preview
|
||
</button>
|
||
<button type="button"
|
||
@click="submitOrTopup($event)"
|
||
class="flex flex-1 items-center justify-center gap-2 rounded-xl bg-indigo-600 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-700">
|
||
Create
|
||
</button>
|
||
</div>
|
||
<div class="mobile-action-bar__inset-fill" aria-hidden="true"></div>
|
||
</div>
|
||
|
||
{{-- Preview modal --}}
|
||
<div x-show="showPreviewModal" x-cloak
|
||
x-transition:enter="transition-opacity duration-200"
|
||
x-transition:enter-start="opacity-0"
|
||
x-transition:enter-end="opacity-100"
|
||
x-transition:leave="transition-opacity duration-150"
|
||
x-transition:leave-start="opacity-100"
|
||
x-transition:leave-end="opacity-0"
|
||
@keydown.escape.window="showPreviewModal = false"
|
||
class="fixed inset-0 z-[60] flex flex-col bg-black/60 backdrop-blur-sm lg:hidden"
|
||
style="padding-bottom: env(safe-area-inset-bottom, 0px)">
|
||
<div class="flex items-center justify-between px-5 py-4">
|
||
<span class="text-sm font-semibold text-white">Preview</span>
|
||
<button type="button" @click="showPreviewModal = false"
|
||
class="flex h-8 w-8 items-center justify-center rounded-full bg-white/20 text-white hover:bg-white/30 transition">
|
||
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
<div class="flex flex-1 items-center justify-center p-8">
|
||
<div class="w-full max-w-xs overflow-hidden rounded-2xl bg-white shadow-2xl"
|
||
:style="frameStyle === 'thin'
|
||
? 'box-shadow: 0 0 0 2px ' + frameColor + ', 0 25px 50px -12px rgb(0 0 0 / 0.25)'
|
||
: 'box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25), 0 0 0 1px rgb(0 0 0 / 0.06)'">
|
||
<div :class="{
|
||
'p-[8px]': frameStyle === 'thin',
|
||
'p-[14px]': frameStyle === 'scan_me' || frameStyle === 'tap_to_scan',
|
||
'p-4': frameStyle === 'none',
|
||
}">
|
||
<div x-ref="qrPreviewModal"
|
||
class="aspect-square w-full [&>svg]:block [&>svg]:h-full [&>svg]:w-full"></div>
|
||
<div x-show="frameStyle === 'scan_me'" x-cloak
|
||
:style="'border-top: 1px solid ' + frameColor + '40; color: ' + frameColor"
|
||
class="pt-2.5 pb-0.5 text-center">
|
||
<span x-text="(frameText && frameText.trim() ? frameText : 'SCAN ME').toUpperCase()"
|
||
class="text-[11px] font-bold tracking-[0.18em]"></span>
|
||
</div>
|
||
<div x-show="frameStyle === 'tap_to_scan'" x-cloak class="mt-2.5 flex justify-center pb-0.5">
|
||
<span x-text="(frameText && frameText.trim() ? frameText : 'TAP TO SCAN').toUpperCase()"
|
||
:style="'background-color: ' + frameColor + '; color: ' + frameTextColor"
|
||
class="rounded-full px-5 py-1.5 text-[10px] font-bold tracking-widest"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<x-user.service-topup-modal
|
||
:id="$modalId"
|
||
title="Add QR credits"
|
||
description="Top up your QR balance to create codes."
|
||
:topup-action="route('qr-codes.topup')"
|
||
:min-topup="$minTopup"
|
||
:suggested-amount="max($minTopup, $pricePerQr)"
|
||
:ladill-wallet-balance="$ladillWalletBalance"
|
||
:service-balance="$wallet->spendableBalance()"
|
||
service-balance-label="QR balance"
|
||
:return-url="url()->current()"
|
||
:open-on-load="$openTopup"
|
||
/>
|
||
</x-user-layout>
|