Add Portfolio and Bookshop QR types with tailored landing pages.
Deploy Ladill QR Plus / deploy (push) Successful in 58s
Deploy Ladill QR Plus / deploy (push) Successful in 58s
Portfolio showcases freelancer work projects; Bookshop lists author titles with multi-platform purchase links. Includes create/edit forms, public assets, and tests.
This commit is contained in:
@@ -1094,6 +1094,213 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== PORTFOLIO ===== --}}
|
||||
@elseif($type === \App\Models\QrCode::TYPE_PORTFOLIO)
|
||||
@php
|
||||
$pName = $content['name'] ?? $qrCode->label;
|
||||
$pRole = $content['role'] ?? '';
|
||||
$pColor = $content['brand_color'] ?? '#4f46e5';
|
||||
$pAvatar = !empty($content['avatar_path']) ? $qrCode->publicPath('portfolio-avatar') : null;
|
||||
$pCover = !empty($content['cover_path']) ? $qrCode->publicPath('portfolio-cover') : null;
|
||||
$pSocial = [];
|
||||
foreach (($content['social'] ?? []) as $platform => $url) {
|
||||
$pSocial[$platform] = \App\Services\Qr\QrPayloadValidator::normalizeSocialUrl($platform, trim((string) $url));
|
||||
}
|
||||
$pInitials = strtoupper(substr($pName, 0, 1));
|
||||
$socialIcons = [
|
||||
'linkedin' => 'linkedin.svg', 'twitter' => 'twitter.svg', 'instagram' => 'instagram.svg',
|
||||
'facebook' => 'facebook.svg', 'youtube' => 'youtube.svg', 'whatsapp' => 'whatsapp.svg',
|
||||
];
|
||||
@endphp
|
||||
<div class="min-h-screen bg-slate-100 pb-12">
|
||||
<div class="relative h-44 overflow-hidden">
|
||||
@if($pCover)
|
||||
<img src="{{ $pCover }}" alt="" class="absolute inset-0 h-full w-full object-cover">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/10 to-black/55"></div>
|
||||
@else
|
||||
<div class="absolute inset-0" style="background: linear-gradient(135deg, {{ $pColor }}, {{ $pColor }}aa);"></div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="relative mx-auto -mt-14 max-w-md px-4">
|
||||
<div class="overflow-hidden rounded-3xl bg-white shadow-xl">
|
||||
<div class="px-6 pb-6 pt-4 text-center">
|
||||
<div class="mx-auto -mt-12 mb-3 flex h-24 w-24 items-center justify-center overflow-hidden rounded-full border-4 border-white shadow-lg" style="background: {{ $pColor }};">
|
||||
@if($pAvatar)
|
||||
<img src="{{ $pAvatar }}" alt="{{ $pName }}" class="h-full w-full object-cover">
|
||||
@else
|
||||
<span class="text-3xl font-bold text-white">{{ $pInitials }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<h1 class="text-xl font-bold text-slate-900">{{ $pName }}</h1>
|
||||
@if($pRole)<p class="mt-1 text-sm font-medium" style="color: {{ $pColor }};">{{ $pRole }}</p>@endif
|
||||
@if(!empty($content['bio']))<p class="mt-3 text-sm leading-relaxed text-slate-600">{{ $content['bio'] }}</p>@endif
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
@if(!empty($content['email']))
|
||||
<a href="mailto:{{ $content['email'] }}" class="rounded-full px-3 py-1.5 text-xs font-semibold text-white" style="background: {{ $pColor }};">Email</a>
|
||||
@endif
|
||||
@if(!empty($content['phone']))
|
||||
<a href="tel:{{ $content['phone'] }}" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700">Call</a>
|
||||
@endif
|
||||
@if(!empty($content['website']))
|
||||
<a href="{{ $content['website'] }}" target="_blank" rel="noopener" class="rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700">Website</a>
|
||||
@endif
|
||||
</div>
|
||||
@if($pSocial !== [])
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-2">
|
||||
@foreach($pSocial as $platform => $url)
|
||||
@if(isset($socialIcons[$platform]))
|
||||
<a href="{{ $url }}" target="_blank" rel="noopener" class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100">
|
||||
<img src="/images/qr-icons/{{ $socialIcons[$platform] }}" alt="{{ $platform }}" class="h-4 w-4">
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="border-t border-slate-100 px-4 py-5">
|
||||
<h2 class="mb-3 px-2 text-xs font-semibold uppercase tracking-wider text-slate-400">Selected work</h2>
|
||||
<div class="space-y-3">
|
||||
@foreach(($content['projects'] ?? []) as $index => $project)
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-100 bg-slate-50">
|
||||
@if(!empty($project['image_path']))
|
||||
<img src="{{ $qrCode->publicPath('portfolio-project/'.$index) }}" alt="{{ $project['title'] ?? '' }}"
|
||||
class="h-40 w-full object-cover">
|
||||
@endif
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-bold text-slate-900">{{ $project['title'] ?? '' }}</h3>
|
||||
@if(!empty($project['description']))
|
||||
<p class="mt-1 text-sm leading-relaxed text-slate-600">{{ $project['description'] }}</p>
|
||||
@endif
|
||||
@if(!empty($project['tags']))
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
@foreach($project['tags'] as $tag)
|
||||
<span class="rounded-full px-2 py-0.5 text-[10px] font-semibold" style="background: {{ $pColor }}14; color: {{ $pColor }};">{{ $tag }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($project['url']))
|
||||
<a href="{{ $project['url'] }}" target="_blank" rel="noopener"
|
||||
class="mt-3 inline-flex items-center gap-1 text-xs font-semibold" style="color: {{ $pColor }};">
|
||||
View project
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"/></svg>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== BOOKSHOP ===== --}}
|
||||
@elseif($type === \App\Models\QrCode::TYPE_BOOKSHOP)
|
||||
@php
|
||||
$aName = $content['author_name'] ?? $qrCode->label;
|
||||
$aTagline = $content['tagline'] ?? '';
|
||||
$aColor = $content['brand_color'] ?? '#0f766e';
|
||||
$aAvatar = !empty($content['avatar_path']) ? $qrCode->publicPath('bookshop-avatar') : null;
|
||||
$aCover = !empty($content['cover_path']) ? $qrCode->publicPath('bookshop-cover') : null;
|
||||
$aSocial = [];
|
||||
foreach (($content['social'] ?? []) as $platform => $url) {
|
||||
$aSocial[$platform] = \App\Services\Qr\QrPayloadValidator::normalizeSocialUrl($platform, trim((string) $url));
|
||||
}
|
||||
$aInitials = strtoupper(substr($aName, 0, 1));
|
||||
$socialIcons = [
|
||||
'linkedin' => 'linkedin.svg', 'twitter' => 'twitter.svg', 'instagram' => 'instagram.svg',
|
||||
'facebook' => 'facebook.svg', 'youtube' => 'youtube.svg', 'whatsapp' => 'whatsapp.svg',
|
||||
];
|
||||
@endphp
|
||||
<div class="min-h-screen bg-stone-100 pb-12">
|
||||
<div class="relative h-48 overflow-hidden">
|
||||
@if($aCover)
|
||||
<img src="{{ $aCover }}" alt="" class="absolute inset-0 h-full w-full object-cover">
|
||||
<div class="absolute inset-0 bg-gradient-to-b from-black/20 to-black/60"></div>
|
||||
@else
|
||||
<div class="absolute inset-0" style="background: linear-gradient(145deg, {{ $aColor }}, {{ $aColor }}99);"></div>
|
||||
@endif
|
||||
<div class="absolute inset-x-0 bottom-0 px-5 pb-5 text-white">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-white/80">Author bookshop</p>
|
||||
<h1 class="mt-1 text-2xl font-bold">{{ $aName }}</h1>
|
||||
@if($aTagline)<p class="mt-1 text-sm text-white/90">{{ $aTagline }}</p>@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-md px-4">
|
||||
<div class="relative -mt-8 rounded-3xl bg-white p-5 shadow-xl">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-2xl border-2 border-white shadow" style="background: {{ $aColor }};">
|
||||
@if($aAvatar)
|
||||
<img src="{{ $aAvatar }}" alt="{{ $aName }}" class="h-full w-full object-cover">
|
||||
@else
|
||||
<span class="text-2xl font-bold text-white">{{ $aInitials }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
@if(!empty($content['bio']))
|
||||
<p class="text-sm leading-relaxed text-slate-600">{{ $content['bio'] }}</p>
|
||||
@endif
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
@if(!empty($content['website']))
|
||||
<a href="{{ $content['website'] }}" target="_blank" rel="noopener" class="rounded-full px-3 py-1 text-xs font-semibold text-white" style="background: {{ $aColor }};">Website</a>
|
||||
@endif
|
||||
@if(!empty($content['email']))
|
||||
<a href="mailto:{{ $content['email'] }}" class="rounded-full border border-slate-200 px-3 py-1 text-xs font-semibold text-slate-700">Email</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($aSocial !== [])
|
||||
<div class="mt-4 flex flex-wrap gap-2 border-t border-slate-100 pt-4">
|
||||
@foreach($aSocial as $platform => $url)
|
||||
@if(isset($socialIcons[$platform]))
|
||||
<a href="{{ $url }}" target="_blank" rel="noopener" class="flex h-9 w-9 items-center justify-center rounded-full bg-slate-100">
|
||||
<img src="/images/qr-icons/{{ $socialIcons[$platform] }}" alt="{{ $platform }}" class="h-4 w-4">
|
||||
</a>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h2 class="mb-3 mt-8 text-xs font-semibold uppercase tracking-wider text-slate-500">Books</h2>
|
||||
<div class="space-y-4">
|
||||
@foreach(($content['books'] ?? []) as $index => $book)
|
||||
<div class="overflow-hidden rounded-3xl border border-stone-200 bg-white shadow-sm">
|
||||
<div class="flex gap-4 p-4">
|
||||
<div class="h-36 w-24 shrink-0 overflow-hidden rounded-xl bg-stone-100 shadow-inner">
|
||||
@if(!empty($book['cover_path']))
|
||||
<img src="{{ $qrCode->publicPath('bookshop-book/'.$index) }}" alt="{{ $book['title'] ?? '' }}"
|
||||
class="h-full w-full object-cover">
|
||||
@else
|
||||
<div class="flex h-full w-full items-center justify-center px-2 text-center text-[10px] font-semibold text-stone-400">
|
||||
{{ $book['title'] ?? 'Book' }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="text-base font-bold text-slate-900">{{ $book['title'] ?? '' }}</h3>
|
||||
@if(!empty($book['description']))
|
||||
<p class="mt-1 line-clamp-3 text-sm leading-relaxed text-slate-600">{{ $book['description'] }}</p>
|
||||
@endif
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
@foreach(($book['buy_links'] ?? []) as $buy)
|
||||
<a href="{{ $buy['url'] }}" target="_blank" rel="noopener"
|
||||
class="inline-flex items-center rounded-full px-3 py-1.5 text-xs font-semibold text-white shadow-sm"
|
||||
style="background: {{ $aColor }};">
|
||||
{{ $buy['label'] ?? 'Buy' }}
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ===== COUPON ===== --}}
|
||||
@elseif($type === \App\Models\QrCode::TYPE_COUPON)
|
||||
<div class="flex min-h-screen flex-col items-center justify-center px-4 py-12">
|
||||
|
||||
@@ -678,6 +678,193 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{-- Portfolio --}}
|
||||
<div x-show="type === 'portfolio'" x-cloak
|
||||
x-data="{
|
||||
projects: [{ title: '', description: '', url: '', tags: '' }],
|
||||
addProject() { this.projects.push({ title: '', description: '', url: '', tags: '' }) },
|
||||
removeProject(i) { this.projects.splice(i, 1); if (this.projects.length === 0) this.addProject() },
|
||||
avatarPreview: null,
|
||||
coverPreview: null,
|
||||
}"
|
||||
class="space-y-4">
|
||||
<div class="grid gap-3">
|
||||
<input type="text" name="name" value="{{ old('name') }}" placeholder="Your name *"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<input type="text" name="role" value="{{ old('role') }}" placeholder="Title / specialty (e.g. Product designer)"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<textarea name="bio" rows="3" placeholder="Short bio"
|
||||
class="rounded-xl border border-slate-200 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">{{ old('bio') }}</textarea>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="Email"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<input type="text" name="phone" value="{{ old('phone') }}" placeholder="Phone"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
</div>
|
||||
<input type="url" name="website" value="{{ old('website') }}" placeholder="Website / portfolio URL"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Photo</label>
|
||||
<input type="file" name="portfolio_avatar" accept="image/*"
|
||||
@change="avatarPreview = $event.target.files[0] ? URL.createObjectURL($event.target.files[0]) : null"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
<div x-show="avatarPreview" class="mt-2"><img :src="avatarPreview" class="h-14 w-14 rounded-full object-cover shadow" alt=""></div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Cover</label>
|
||||
<input type="file" name="portfolio_cover" accept="image/*"
|
||||
@change="coverPreview = $event.target.files[0] ? URL.createObjectURL($event.target.files[0]) : null"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
<x-qr.cover-image-hint />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Brand color</label>
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', '#4f46e5') }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold text-slate-600">Social links <span class="font-normal text-slate-400">(optional)</span></p>
|
||||
<div class="grid gap-2">
|
||||
@foreach(['linkedin' => 'LinkedIn', 'twitter' => 'X / Twitter', 'instagram' => 'Instagram', 'facebook' => 'Facebook', 'youtube' => 'YouTube', 'whatsapp' => 'WhatsApp'] as $platform => $label)
|
||||
<input type="text" name="social[{{ $platform }}]" value="{{ old('social.'.$platform) }}" placeholder="{{ $label }}"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-slate-600">Projects *</p>
|
||||
<button type="button" @click="addProject()" class="text-xs font-semibold text-indigo-600 hover:text-indigo-800">+ Add project</button>
|
||||
</div>
|
||||
<template x-for="(project, i) in projects" :key="i">
|
||||
<div class="space-y-2 rounded-xl border border-slate-200 bg-slate-50/60 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400" x-text="'Project ' + (i + 1)"></p>
|
||||
<button type="button" @click="removeProject(i)" class="text-xs text-red-600 hover:text-red-800">Remove</button>
|
||||
</div>
|
||||
<input type="text" :name="'projects['+i+'][title]'" x-model="project.title" placeholder="Project title *"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<textarea :name="'projects['+i+'][description]'" x-model="project.description" rows="2" placeholder="What you built / delivered"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30"></textarea>
|
||||
<input type="url" :name="'projects['+i+'][url]'" x-model="project.url" placeholder="Project or case-study URL"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<input type="text" :name="'projects['+i+'][tags]'" x-model="project.tags" placeholder="Tags (comma-separated)"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Project image</label>
|
||||
<input type="file" :name="'project_images['+i+']'" accept="image/*"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Bookshop --}}
|
||||
<div x-show="type === 'bookshop'" x-cloak
|
||||
x-data="{
|
||||
books: [{ title: '', description: '', buy_links: [{ label: 'Amazon', url: '' }] }],
|
||||
addBook() { this.books.push({ title: '', description: '', buy_links: [{ label: 'Amazon', url: '' }] }) },
|
||||
removeBook(i) { this.books.splice(i, 1); if (this.books.length === 0) this.addBook() },
|
||||
addLink(b) { this.books[b].buy_links.push({ label: '', url: '' }) },
|
||||
removeLink(b, l) { this.books[b].buy_links.splice(l, 1); if (this.books[b].buy_links.length === 0) this.addLink(b) },
|
||||
avatarPreview: null,
|
||||
coverPreview: null,
|
||||
}"
|
||||
class="space-y-4">
|
||||
<div class="grid gap-3">
|
||||
<input type="text" name="author_name" value="{{ old('author_name') }}" placeholder="Author name *"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<input type="text" name="tagline" value="{{ old('tagline') }}" placeholder="Tagline (e.g. Stories that stay with you)"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<textarea name="bio" rows="3" placeholder="About the author"
|
||||
class="rounded-xl border border-slate-200 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">{{ old('bio') }}</textarea>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="email" name="email" value="{{ old('email') }}" placeholder="Contact email"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
<input type="url" name="website" value="{{ old('website') }}" placeholder="Author website"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Author photo</label>
|
||||
<input type="file" name="bookshop_avatar" accept="image/*"
|
||||
@change="avatarPreview = $event.target.files[0] ? URL.createObjectURL($event.target.files[0]) : null"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Cover banner</label>
|
||||
<input type="file" name="bookshop_cover" accept="image/*"
|
||||
@change="coverPreview = $event.target.files[0] ? URL.createObjectURL($event.target.files[0]) : null"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
<x-qr.cover-image-hint />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Brand color</label>
|
||||
<input type="color" name="brand_color" value="{{ old('brand_color', '#0f766e') }}"
|
||||
class="mt-1 h-10 w-full cursor-pointer rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-2 text-xs font-semibold text-slate-600">Social links <span class="font-normal text-slate-400">(optional)</span></p>
|
||||
<div class="grid gap-2">
|
||||
@foreach(['instagram' => 'Instagram', 'twitter' => 'X / Twitter', 'facebook' => 'Facebook', 'youtube' => 'YouTube', 'linkedin' => 'LinkedIn'] as $platform => $label)
|
||||
<input type="text" name="social[{{ $platform }}]" value="{{ old('social.'.$platform) }}" placeholder="{{ $label }}"
|
||||
class="rounded-xl border border-slate-200 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">
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-slate-600">Books *</p>
|
||||
<button type="button" @click="addBook()" class="text-xs font-semibold text-indigo-600 hover:text-indigo-800">+ Add book</button>
|
||||
</div>
|
||||
<template x-for="(book, b) in books" :key="b">
|
||||
<div class="space-y-2 rounded-xl border border-slate-200 bg-slate-50/60 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-wide text-slate-400" x-text="'Book ' + (b + 1)"></p>
|
||||
<button type="button" @click="removeBook(b)" class="text-xs text-red-600 hover:text-red-800">Remove</button>
|
||||
</div>
|
||||
<input type="text" :name="'books['+b+'][title]'" x-model="book.title" placeholder="Book title *"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<textarea :name="'books['+b+'][description]'" x-model="book.description" rows="2" placeholder="Short description"
|
||||
class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30"></textarea>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Cover image</label>
|
||||
<input type="file" :name="'book_covers['+b+']'" accept="image/*"
|
||||
class="mt-1 block w-full text-xs text-slate-600 file:mr-2 file:rounded-lg file:border-0 file:bg-indigo-50 file:px-2 file:py-1 file:text-xs file:font-semibold file:text-indigo-700">
|
||||
<x-qr.cover-image-hint variant="book" />
|
||||
</div>
|
||||
<div class="space-y-2 rounded-lg border border-dashed border-slate-200 bg-white p-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[11px] font-semibold text-slate-500">Where to buy *</p>
|
||||
<button type="button" @click="addLink(b)" class="text-[11px] font-semibold text-indigo-600">+ Link</button>
|
||||
</div>
|
||||
<template x-for="(link, l) in book.buy_links" :key="l">
|
||||
<div class="grid gap-2 sm:grid-cols-[1fr_2fr_auto]">
|
||||
<input type="text" :name="'books['+b+'][buy_links]['+l+'][label]'" x-model="link.label" placeholder="Amazon, Bookshop.org…"
|
||||
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm">
|
||||
<input type="url" :name="'books['+b+'][buy_links]['+l+'][url]'" x-model="link.url" placeholder="https://…"
|
||||
class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm">
|
||||
<button type="button" @click="removeLink(b, l)" class="text-xs text-red-600">Remove</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- App --}}
|
||||
<div x-show="type === 'app'" x-cloak x-data="{ iconPreview: null }" class="grid gap-3">
|
||||
<input type="text" name="app_name" value="{{ old('app_name') }}" placeholder="App name *"
|
||||
|
||||
@@ -731,6 +731,159 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@elseif($qrCode->type === \App\Models\QrCode::TYPE_PORTFOLIO)
|
||||
@php
|
||||
$projects = collect($c['projects'] ?? [])->map(fn ($p) => [
|
||||
'title' => $p['title'] ?? '',
|
||||
'description' => $p['description'] ?? '',
|
||||
'url' => $p['url'] ?? '',
|
||||
'tags' => implode(', ', $p['tags'] ?? []),
|
||||
'image_path' => $p['image_path'] ?? null,
|
||||
])->values()->all();
|
||||
if ($projects === []) {
|
||||
$projects = [['title' => '', 'description' => '', 'url' => '', 'tags' => '', 'image_path' => null]];
|
||||
}
|
||||
@endphp
|
||||
<div x-data="{
|
||||
projects: @js($projects),
|
||||
addProject() { this.projects.push({ title: '', description: '', url: '', tags: '', image_path: null }) },
|
||||
removeProject(i) { this.projects.splice(i, 1); if (this.projects.length === 0) this.addProject() },
|
||||
}" class="space-y-4">
|
||||
<div class="grid gap-3">
|
||||
<input type="text" name="name" value="{{ $c['name'] ?? '' }}" placeholder="Your name *"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<input type="text" name="role" value="{{ $c['role'] ?? '' }}" placeholder="Title / specialty"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">
|
||||
<textarea name="bio" rows="3" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm focus:border-indigo-400 focus:outline-none focus:ring-1 focus:ring-indigo-400/30">{{ $c['bio'] ?? '' }}</textarea>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="email" name="email" value="{{ $c['email'] ?? '' }}" placeholder="Email" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
<input type="text" name="phone" value="{{ $c['phone'] ?? '' }}" placeholder="Phone" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
</div>
|
||||
<input type="url" name="website" value="{{ $c['website'] ?? '' }}" placeholder="Website" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
<input type="color" name="brand_color" value="{{ $c['brand_color'] ?? '#4f46e5' }}" class="h-10 w-full rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Replace photo</label>
|
||||
<input type="file" name="portfolio_avatar" accept="image/*" class="mt-1 block w-full text-xs">
|
||||
@if(!empty($c['avatar_path']))
|
||||
<img src="{{ route('qr.public.portfolio.avatar', $qrCode->short_code) }}" class="mt-2 h-14 w-14 rounded-full object-cover" alt="">
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Replace cover</label>
|
||||
<input type="file" name="portfolio_cover" accept="image/*" class="mt-1 block w-full text-xs">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
@foreach(['linkedin','twitter','instagram','facebook','youtube','whatsapp'] as $platform)
|
||||
<input type="text" name="social[{{ $platform }}]" value="{{ $c['social'][$platform] ?? '' }}" placeholder="{{ ucfirst($platform) }}"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-slate-600">Projects</p>
|
||||
<button type="button" @click="addProject()" class="text-xs font-semibold text-indigo-600">+ Add</button>
|
||||
</div>
|
||||
<template x-for="(project, i) in projects" :key="i">
|
||||
<div class="space-y-2 rounded-xl border border-slate-200 p-3">
|
||||
<input type="hidden" :name="'projects['+i+'][image_path]'" :value="project.image_path || ''">
|
||||
<input type="text" :name="'projects['+i+'][title]'" x-model="project.title" placeholder="Title *" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm">
|
||||
<textarea :name="'projects['+i+'][description]'" x-model="project.description" rows="2" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm"></textarea>
|
||||
<input type="url" :name="'projects['+i+'][url]'" x-model="project.url" placeholder="URL" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm">
|
||||
<input type="text" :name="'projects['+i+'][tags]'" x-model="project.tags" placeholder="Tags" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm">
|
||||
<input type="file" :name="'project_images['+i+']'" accept="image/*" class="block w-full text-xs">
|
||||
<button type="button" @click="removeProject(i)" class="text-xs text-red-600">Remove project</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@elseif($qrCode->type === \App\Models\QrCode::TYPE_BOOKSHOP)
|
||||
@php
|
||||
$books = collect($c['books'] ?? [])->map(function ($b) {
|
||||
$links = collect($b['buy_links'] ?? [])->map(fn ($l) => [
|
||||
'label' => $l['label'] ?? '',
|
||||
'url' => $l['url'] ?? '',
|
||||
])->values()->all();
|
||||
if ($links === []) {
|
||||
$links = [['label' => 'Amazon', 'url' => '']];
|
||||
}
|
||||
return [
|
||||
'title' => $b['title'] ?? '',
|
||||
'description' => $b['description'] ?? '',
|
||||
'cover_path' => $b['cover_path'] ?? null,
|
||||
'buy_links' => $links,
|
||||
];
|
||||
})->values()->all();
|
||||
if ($books === []) {
|
||||
$books = [['title' => '', 'description' => '', 'cover_path' => null, 'buy_links' => [['label' => 'Amazon', 'url' => '']]]];
|
||||
}
|
||||
@endphp
|
||||
<div x-data="{
|
||||
books: @js($books),
|
||||
addBook() { this.books.push({ title: '', description: '', cover_path: null, buy_links: [{ label: 'Amazon', url: '' }] }) },
|
||||
removeBook(i) { this.books.splice(i, 1); if (this.books.length === 0) this.addBook() },
|
||||
addLink(b) { this.books[b].buy_links.push({ label: '', url: '' }) },
|
||||
removeLink(b, l) { this.books[b].buy_links.splice(l, 1); if (this.books[b].buy_links.length === 0) this.addLink(b) },
|
||||
}" class="space-y-4">
|
||||
<div class="grid gap-3">
|
||||
<input type="text" name="author_name" value="{{ $c['author_name'] ?? '' }}" placeholder="Author name *" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
<input type="text" name="tagline" value="{{ $c['tagline'] ?? '' }}" placeholder="Tagline" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
<textarea name="bio" rows="3" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">{{ $c['bio'] ?? '' }}</textarea>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<input type="email" name="email" value="{{ $c['email'] ?? '' }}" placeholder="Email" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
<input type="url" name="website" value="{{ $c['website'] ?? '' }}" placeholder="Website" class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
</div>
|
||||
<input type="color" name="brand_color" value="{{ $c['brand_color'] ?? '#0f766e' }}" class="h-10 w-full rounded-xl border border-slate-200 p-1">
|
||||
</div>
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Replace author photo</label>
|
||||
<input type="file" name="bookshop_avatar" accept="image/*" class="mt-1 block w-full text-xs">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-[11px] font-medium text-slate-500">Replace cover banner</label>
|
||||
<input type="file" name="bookshop_cover" accept="image/*" class="mt-1 block w-full text-xs">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
@foreach(['instagram','twitter','facebook','youtube','linkedin'] as $platform)
|
||||
<input type="text" name="social[{{ $platform }}]" value="{{ $c['social'][$platform] ?? '' }}" placeholder="{{ ucfirst($platform) }}"
|
||||
class="rounded-xl border border-slate-200 px-3.5 py-2.5 text-sm">
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs font-semibold text-slate-600">Books</p>
|
||||
<button type="button" @click="addBook()" class="text-xs font-semibold text-indigo-600">+ Add book</button>
|
||||
</div>
|
||||
<template x-for="(book, b) in books" :key="b">
|
||||
<div class="space-y-2 rounded-xl border border-slate-200 p-3">
|
||||
<input type="hidden" :name="'books['+b+'][cover_path]'" :value="book.cover_path || ''">
|
||||
<input type="text" :name="'books['+b+'][title]'" x-model="book.title" placeholder="Title *" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm">
|
||||
<textarea :name="'books['+b+'][description]'" x-model="book.description" rows="2" class="w-full rounded-lg border border-slate-200 px-3 py-2 text-sm"></textarea>
|
||||
<input type="file" :name="'book_covers['+b+']'" accept="image/*" class="block w-full text-xs">
|
||||
<div class="space-y-2 rounded-lg border border-dashed border-slate-200 p-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-[11px] font-semibold text-slate-500">Buy links</p>
|
||||
<button type="button" @click="addLink(b)" class="text-[11px] font-semibold text-indigo-600">+ Link</button>
|
||||
</div>
|
||||
<template x-for="(link, l) in book.buy_links" :key="l">
|
||||
<div class="grid gap-2 sm:grid-cols-[1fr_2fr_auto]">
|
||||
<input type="text" :name="'books['+b+'][buy_links]['+l+'][label]'" x-model="link.label" placeholder="Amazon" class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm">
|
||||
<input type="url" :name="'books['+b+'][buy_links]['+l+'][url]'" x-model="link.url" placeholder="https://…" class="rounded-lg border border-slate-200 px-2.5 py-1.5 text-sm">
|
||||
<button type="button" @click="removeLink(b, l)" class="text-xs text-red-600">Remove</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<button type="button" @click="removeBook(b)" class="text-xs text-red-600">Remove book</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@elseif($qrCode->type === \App\Models\QrCode::TYPE_APP)
|
||||
<div x-data="{ iconPreview: null }" class="grid gap-3">
|
||||
<input type="text" name="app_name" value="{{ $c['name'] ?? '' }}" placeholder="App name"
|
||||
|
||||
Reference in New Issue
Block a user