refactor: improve UI responsiveness, add currency formatting to invoice inputs, update address formatting, and refine Docker setup instructions.
This commit is contained in:
12
How-To.txt
12
How-To.txt
@@ -1,6 +1,14 @@
|
||||
git clone this repository
|
||||
# On local machine
|
||||
#git clone this repository
|
||||
composer install
|
||||
#php artisan key:generate
|
||||
php artisan key:generate
|
||||
touch database/database.sqlite && php artisan migrate
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# On Docker
|
||||
cp .env.example .env.docker
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
|
||||
|
||||
|
||||
@@ -57,14 +57,19 @@ class Client extends Model
|
||||
|
||||
public function getFullAddressAttribute(): string
|
||||
{
|
||||
$line3_parts = array_filter([$this->city, $this->state]);
|
||||
$country_part = array_filter([$this->postal_code, $this->country]);
|
||||
$country_str = implode(' - ', $country_part);
|
||||
if ($country_str) {
|
||||
$line3_parts[] = $country_str;
|
||||
}
|
||||
$line3 = implode(', ', $line3_parts);
|
||||
|
||||
return collect([
|
||||
$this->address_line1,
|
||||
$this->address_line2,
|
||||
$this->city,
|
||||
$this->state,
|
||||
$this->postal_code,
|
||||
$this->country,
|
||||
])->filter()->implode(', ');
|
||||
$line3
|
||||
])->filter()->implode("\n");
|
||||
}
|
||||
|
||||
public function getInitialsAttribute(): string
|
||||
|
||||
@@ -133,7 +133,7 @@
|
||||
@if($client->full_address)
|
||||
<div class="flex gap-2 items-start">
|
||||
<span class="text-gray-400 shrink-0">📍</span>
|
||||
<span class="text-gray-600">{{ $client->full_address }}</span>
|
||||
<span class="text-gray-600">{!! nl2br(e($client->full_address)) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($client->notes)
|
||||
|
||||
@@ -8,19 +8,6 @@
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<a href="{{ route('content.create') }}" class="btn-sakura text-sm shrink-0">+ New Brief</a>
|
||||
@foreach(['instagram', 'tiktok', 'youtube'] as $p)
|
||||
<a href="{{ request()->fullUrlWithQuery(['platform' => $p]) }}"
|
||||
class="px-3 py-1.5 rounded-full text-xs font-semibold transition-colors
|
||||
{{ request('platform') === $p ? 'text-white' : 'bg-white text-gray-500 hover:bg-pink-50 border border-pink-100' }}"
|
||||
style="{{ request('platform') === $p ? 'background:#FFB7C5' : '' }}">
|
||||
{{ ['instagram' => '📸', 'tiktok' => '🎵', 'youtube' => '▶️'][$p] }} {{ ucfirst($p) }}
|
||||
</a>
|
||||
@endforeach
|
||||
@if(request('platform'))
|
||||
<a href="{{ route('content.index') }}"
|
||||
class="px-3 py-1.5 rounded-full text-xs text-gray-400 hover:text-gray-600 border border-gray-200 bg-white">✕
|
||||
Clear</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -36,8 +23,19 @@
|
||||
@php $status = $col['status']; @endphp
|
||||
|
||||
{{-- Column shell — layout only, NOT the sort target --}}
|
||||
@php
|
||||
$bgColors = [
|
||||
'drafting' => '#f2f2f2',
|
||||
'filming' => 'rgba(251, 189, 35, 0.2)',
|
||||
'editing' => 'rgba(58, 191, 248, 0.2)',
|
||||
'review' => 'rgba(240, 0, 184, 0.2)',
|
||||
'posted' => 'rgba(54, 211, 153, 0.2)',
|
||||
'archived' => 'rgba(42, 50, 60, 0.2)',
|
||||
];
|
||||
$bgStyle = isset($bgColors[$status->value]) ? 'background-color: ' . $bgColors[$status->value] . ';' : '';
|
||||
@endphp
|
||||
<div class="{{ $status->colorClass() }} flex flex-col w-full lg:flex-1 lg:min-w-36 rounded-2xl p-3"
|
||||
data-status="{{ $status->value }}">
|
||||
style="{{ $bgStyle }}" data-status="{{ $status->value }}">
|
||||
|
||||
{{-- Column header --}}
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -54,7 +52,7 @@
|
||||
|
||||
@foreach($col['items'] as $brief)
|
||||
<div class="bg-white rounded-2xl p-3 shadow-sm border border-pink-50 select-none
|
||||
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
||||
cursor-grab active:cursor-grabbing hover:shadow-md transition-shadow kanban-item"
|
||||
data-id="{{ $brief->id }}">
|
||||
<div class="flex items-start gap-2 mb-1.5">
|
||||
<span class="text-sm shrink-0">{{ $brief->platform_icon }}</span>
|
||||
@@ -93,7 +91,7 @@
|
||||
{{-- Add button --}}
|
||||
<a href="{{ route('content.create') }}?status={{ $status->value }}"
|
||||
class="mt-3 text-xs text-gray-400 hover:text-pink-500 text-center py-2
|
||||
border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block">
|
||||
border-2 border-dashed border-pink-100 rounded-2xl hover:border-pink-300 transition-colors block">
|
||||
+ Add
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -78,9 +78,16 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
||||
<input type="number" :name="`items[${index}][unit_price]`"
|
||||
x-model.number="item.unit_price" @input="recalc()"
|
||||
class="input-soft text-sm bg-white" min="0" step="1000" placeholder="0">
|
||||
<input type="hidden" :name="`items[${index}][unit_price]`" :value="item.unit_price">
|
||||
<input type="text"
|
||||
:value="item.unit_price ? formatNum(item.unit_price) : ''"
|
||||
@input="
|
||||
let raw = $event.target.value.replace(/\D/g, '');
|
||||
item.unit_price = raw ? parseInt(raw, 10) : 0;
|
||||
$event.target.value = item.unit_price ? formatNum(item.unit_price) : '';
|
||||
recalc();
|
||||
"
|
||||
class="input-soft text-sm bg-white" placeholder="0">
|
||||
</div>
|
||||
<div class="w-24 shrink-0 text-right">
|
||||
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
||||
|
||||
@@ -75,9 +75,16 @@
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="text-xs text-gray-400 block mb-0.5">Unit Price</label>
|
||||
<input type="number" :name="`items[${index}][unit_price]`"
|
||||
x-model.number="item.unit_price" @input="recalc()"
|
||||
class="input-soft text-sm bg-white" min="0" step="1000">
|
||||
<input type="hidden" :name="`items[${index}][unit_price]`" :value="item.unit_price">
|
||||
<input type="text"
|
||||
:value="item.unit_price ? formatNum(item.unit_price) : ''"
|
||||
@input="
|
||||
let raw = $event.target.value.replace(/\D/g, '');
|
||||
item.unit_price = raw ? parseInt(raw, 10) : 0;
|
||||
$event.target.value = item.unit_price ? formatNum(item.unit_price) : '';
|
||||
recalc();
|
||||
"
|
||||
class="input-soft text-sm bg-white" placeholder="0">
|
||||
</div>
|
||||
<div class="w-24 shrink-0 text-right">
|
||||
<label class="text-xs text-gray-400 block mb-0.5">Total</label>
|
||||
|
||||
@@ -271,7 +271,7 @@
|
||||
@if($invoice->client->email)
|
||||
<div class="billing-detail">{{ $invoice->client->email }}</div> @endif
|
||||
@if($invoice->client->full_address)
|
||||
<div class="billing-detail">{{ $invoice->client->full_address }}</div> @endif
|
||||
<div class="billing-detail">{!! nl2br(e($invoice->client->full_address)) !!}</div> @endif
|
||||
@else
|
||||
<div class="billing-detail">—</div>
|
||||
@endif
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
@if($invoice->client->email)
|
||||
<div class="text-sm text-gray-500">{{ $invoice->client->email }}</div> @endif
|
||||
@if($invoice->client->full_address)
|
||||
<div class="text-sm text-gray-400">{{ $invoice->client->full_address }}</div> @endif
|
||||
<div class="text-sm text-gray-400">{!! nl2br(e($invoice->client->full_address)) !!}</div> @endif
|
||||
@else
|
||||
<div class="text-gray-500 text-sm">No client</div>
|
||||
@endif
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
|
||||
@section('content')
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<a href="{{ route('rate-cards.create') }}" class="btn-sakura text-sm shrink-0">+ Add Service</a>
|
||||
<div class="flex flex-wrap items-center gap-2 w-full">
|
||||
<a href="{{ route('rate-cards.create') }}" class="btn-sakura text-xs sm:text-sm shrink-0 px-3 py-1.5 sm:px-4 sm:py-2 flex items-center gap-1">+ Add Service</a>
|
||||
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-pink-200 text-pink-500 hover:bg-pink-50 flex items-center gap-1">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-pink-200 text-pink-600 hover:bg-pink-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||
🔗 Public Page
|
||||
</a>
|
||||
<a href="{{ route('rate-cards.pdf') }}"
|
||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-blue-200 text-blue-500 hover:bg-blue-50 flex items-center gap-1">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-blue-200 text-blue-600 hover:bg-blue-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||
📄 PDF
|
||||
</a>
|
||||
<a href="{{ route('rate-cards.profile') }}"
|
||||
class="btn-sakura text-sm px-3 py-2 rounded-full border border-purple-200 text-purple-500 hover:bg-purple-50 flex items-center gap-1">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-purple-200 text-purple-600 hover:bg-purple-50 flex items-center gap-1 shadow-sm shrink-0">
|
||||
🎨 Profile
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -282,8 +282,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Past Brands --}}
|
||||
<div class="cat-card overflow-hidden">
|
||||
<div class="flex items-center gap-3 px-5 py-4 border-b border-pink-50">
|
||||
@@ -356,18 +354,18 @@
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<button type="submit" class="btn-sakura px-6 py-2.5">💾 Save Profile</button>
|
||||
<div class="flex flex-wrap items-center gap-2 w-full">
|
||||
<button type="submit" class="btn-sakura text-xs sm:text-sm shrink-0 px-4 py-2 sm:px-6 sm:py-2.5">💾 Save Profile</button>
|
||||
<a href="{{ route('rate-cards.public') }}" target="_blank"
|
||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-pink-200 text-pink-500 hover:bg-pink-50">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-pink-200 text-pink-600 hover:bg-pink-50 shadow-sm shrink-0">
|
||||
👁️ Preview
|
||||
</a>
|
||||
<a href="{{ route('rate-cards.pdf') }}"
|
||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-blue-200 text-blue-500 hover:bg-blue-50">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-blue-200 text-blue-600 hover:bg-blue-50 shadow-sm shrink-0">
|
||||
📄 Download PDF
|
||||
</a>
|
||||
<a href="{{ route('rate-cards.index') }}"
|
||||
class="btn-sakura text-sm px-4 py-2.5 rounded-full border border-gray-200 text-gray-500 hover:bg-gray-50">
|
||||
class="bg-white text-xs sm:text-sm px-3 py-1.5 sm:px-4 sm:py-2 rounded-full border border-gray-200 text-gray-600 hover:bg-gray-50 shadow-sm shrink-0">
|
||||
← Back
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +91,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: isMobile ? 'listWeek,timeGridDay' : 'dayGridMonth,timeGridWeek,listWeek',
|
||||
right: isMobile ? 'dayGridMonth,listWeek,timeGridDay' : 'dayGridMonth,timeGridWeek,listWeek',
|
||||
},
|
||||
editable: !isMobile,
|
||||
selectable: true,
|
||||
@@ -99,7 +99,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
height: 'auto',
|
||||
windowResize(view) {
|
||||
const mob = window.innerWidth < 640;
|
||||
if (mob && calendar.view.type !== 'listWeek' && calendar.view.type !== 'timeGridDay') {
|
||||
if (mob && calendar.view.type !== 'listWeek' && calendar.view.type !== 'timeGridDay' && calendar.view.type !== 'dayGridMonth') {
|
||||
calendar.changeView('listWeek');
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user