diff --git a/app/Http/Controllers/Admin/DownloadProductController.php b/app/Http/Controllers/Admin/DownloadProductController.php index e8e7701..9e5a45b 100644 --- a/app/Http/Controllers/Admin/DownloadProductController.php +++ b/app/Http/Controllers/Admin/DownloadProductController.php @@ -8,18 +8,23 @@ use Illuminate\Support\Facades\Storage; final class DownloadProductController extends Controller { public function index() { return view('admin.download-products.index', ['products'=>DownloadProduct::latest()->get()]); } public function store(Request $request) { - $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:512000','is_published'=>'sometimes|boolean']); + $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'required|file|max:1048576','is_published'=>'sometimes|boolean']); $file=$request->file('package'); $path=$file->store('download-products','local'); DownloadProduct::create(['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize(),'is_published'=>$request->boolean('is_published')]); return back()->with('status','Product added.'); } public function update(Request $request, DownloadProduct $product) { - $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:512000','is_published'=>'sometimes|boolean']); + $data=$request->validate(['name'=>'required|string|max:150','description'=>'nullable|string|max:3000','version'=>'nullable|string|max:80','platform'=>'nullable|string|max:120','package'=>'nullable|file|max:1048576','is_published'=>'sometimes|boolean']); $changes=['name'=>$data['name'],'description'=>$data['description']??null,'version'=>$data['version']??null,'platform'=>$data['platform']??null,'is_published'=>$request->boolean('is_published')]; if ($request->hasFile('package')) { $file=$request->file('package'); $path=$file->store('download-products','local'); $old=$product->file_path; $changes+=['file_path'=>$path,'original_filename'=>$file->getClientOriginalName(),'file_size'=>$file->getSize()]; $product->update($changes); if($old) Storage::disk('local')->delete($old); } else $product->update($changes); return back()->with('status','Product updated.'); } + public function publication(Request $request, DownloadProduct $product) { + $data = $request->validate(['is_published' => 'required|boolean']); + $product->update(['is_published' => (bool) $data['is_published']]); + return back()->with('status', $product->is_published ? 'Product published.' : 'Product unpublished.'); + } public function destroy(DownloadProduct $product) { if($product->file_path) Storage::disk('local')->delete($product->file_path); $product->delete(); return back()->with('status','Product deleted.'); } } diff --git a/app/Http/Controllers/ReleaseController.php b/app/Http/Controllers/ReleaseController.php index b961246..b51954b 100644 --- a/app/Http/Controllers/ReleaseController.php +++ b/app/Http/Controllers/ReleaseController.php @@ -15,9 +15,13 @@ final class ReleaseController extends Controller { $latest = SonarDeskRelease::published()->where('is_featured', true)->orderByDesc('published_at')->first() ?? SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->orderByDesc('id')->first(); - $releases = SonarDeskRelease::published()->orderByDesc('published_at')->paginate(12); + $releases = SonarDeskRelease::published() + ->when($latest, fn ($query) => $query->whereKeyNot($latest->getKey())) + ->orderByDesc('published_at')->paginate(12); $settings = $this->settings(); - $products = DownloadProduct::where('is_published', true)->whereNotNull('file_path')->orderBy('name')->get(); + $products = DownloadProduct::where('is_published', true)->whereNotNull('file_path') + ->when($latest, fn ($query) => $query->whereRaw('LOWER(name) <> ?', ['sonardesk'])) + ->orderBy('name')->get(); return view('downloads.index', compact('latest', 'releases', 'settings', 'products')); } diff --git a/resources/views/admin/download-products/index.blade.php b/resources/views/admin/download-products/index.blade.php index 3ab2973..c63541b 100644 --- a/resources/views/admin/download-products/index.blade.php +++ b/resources/views/admin/download-products/index.blade.php @@ -1,12 +1,15 @@ @extends('layouts.app', ['title' => 'Download products — Admin']) @section('content') -
Distribution

Other download products

Add named products and files to the public downloads area.

Back to releases
-@if(session('status'))

{{ session('status') }}

@endif -@if($errors->any())
@endif -

Add product

@csrf -

-@foreach($products as $product) -

{{ $product->name }}

{{ $product->download_count }} downloads · {{ $product->formatted_size }} · {{ $product->is_published ? 'Published' : 'Draft' }}

@csrf @method('PUT') -

@csrf @method('DELETE')
-@endforeach +
+
Distribution / Product management

Download products

Manage installers, releases and publication status in one place.

+ @if(session('status'))
{{ session('status') }}
@endif + @if($errors->any())@endif +
Total products{{ $products->count() }}
Published{{ $products->where('is_published', true)->count() }}
Drafts{{ $products->where('is_published', false)->count() }}
Total downloads{{ number_format($products->sum('download_count')) }}
+

Product catalogue

Publish or unpublish products instantly, or edit their details.

+ Add product
+ @forelse($products as $product) + @empty@endforelse +
ProductVersion / PlatformDownloadsStatusActions
{{ $product->name }}{{ \Illuminate\Support\Str::limit($product->description ?: $product->original_filename, 70) }}{{ $product->version ?: '—' }}{{ $product->platform ?: 'Unspecified' }} · {{ $product->formatted_size }}{{ number_format($product->download_count) }}{{ $product->is_published ? 'Published' : 'Draft' }}
Edit
@csrf @method('PATCH')
No additional products yet. Add your first installer below.
+
any() && !old('_method')) open @endif>Add a new productUpload an installer and choose whether to publish it.+
@csrf
+ @foreach($products as $product)
Edit {{ $product->name }}{{ $product->original_filename }} · {{ $product->formatted_size }}+
@csrf @method('PUT')
@csrf @method('DELETE')
@endforeach +
@endsection diff --git a/resources/views/downloads/index.blade.php b/resources/views/downloads/index.blade.php index 166b5b5..f360b60 100644 --- a/resources/views/downloads/index.blade.php +++ b/resources/views/downloads/index.blade.php @@ -2,8 +2,10 @@ @section('fullbleed')
{{ $settings['eyebrow'] }}

{{ $settings['title'] }}

{{ $settings['intro'] }}

@if($latest)
✓ Signed distribution✓ SHA-256 verified✓ Secure licence activation
@else
The next SonarDesk release is being prepared.Check back shortly or contact support.
@endif
SonarDesk
SonarDesk
Project healthAll systems monitored
NativeBuilt for macOS
Native macOSFast, focused desktop workflow
SonarQubeYour server remains the source of truth
SecurityTokens protected by macOS Keychain
UpdatesProfessional in-app release delivery
-
@if($latest)
Latest stable release

SonarDesk {{ $latest->version }}

Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}

Download {{ $latest->version }}
{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256{{ $latest->sha256 }}
@endif
+
@if($latest)
Latest stable release

SonarDesk {{ $latest->version }}

Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}

Download {{ $latest->version }}
{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}
SHA-256 checksum · Verify download{{ $latest->sha256 }}
@endif
@if($products->isNotEmpty()) +
Software catalogue

More downloads

Additional tools and installers available from our team.

@foreach($products as $product)
{{ $product->platform ?: 'Software' }}{{ $product->version ? 'v'.$product->version : 'Latest' }}

{{ $product->name }}

{{ $product->description ?: 'Download the latest available package.' }}

@endforeach
+@endif
More downloads

Other products

Explore our other available software and downloads.

@foreach($products as $product)
{{ $product->name }}@if($product->version) {{ $product->version }}@endif{{ $product->description }}@if($product->platform){{ $product->platform }}@endif
{{ $product->formatted_size }}Download
@endforeach
@endif
Release archive

Previous versions

Published SonarDesk builds remain available here for customers who need a previous release.

@forelse($releases as $release)
SonarDesk {{ $release->version }}Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}
{{ $release->formatted_size }}Download
@empty

No published releases are available yet.

@endforelse
{{ $releases->links() }}
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 32426c2..3a379d0 100755 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -198,6 +198,10 @@ .home-download-showcase{display:grid;grid-template-columns:1.25fr .75fr;gap:36px;align-items:center;margin:0 0 64px;padding:38px;background:linear-gradient(135deg,#f8fafc,#eff6ff);border:1px solid #dbeafe;border-radius:22px}.home-download-copy h2{font-size:38px;letter-spacing:-.035em;margin:7px 0 10px}.home-download-copy p{color:#667085;font-size:16px;max-width:680px}.home-download-meta{background:#0b1220;color:#fff;border-radius:16px;padding:24px;box-shadow:0 18px 40px rgba(15,23,42,.16)}.home-download-meta>span{display:block;color:#93c5fd;text-transform:uppercase;font-size:9px;font-weight:900;letter-spacing:.12em}.home-download-meta>strong{display:block;font-size:24px;margin:7px 0 3px}.home-download-meta>small{color:#94a3b8}.home-download-meta>div{margin-top:20px;padding-top:16px;border-top:1px solid rgba(255,255,255,.1)}.home-download-meta b,.home-download-meta code{display:block}.home-download-meta b{font-size:9px;color:#64748b;letter-spacing:.1em}.home-download-meta code{font-size:10px;color:#cbd5e1;margin-top:5px}@media(max-width:760px){.home-download-showcase{grid-template-columns:1fr;padding:24px}} .seo-admin-form{display:grid;gap:18px}.seo-section{padding:28px}.seo-section h2{margin:4px 0 4px;font-size:22px;letter-spacing:-.02em}.seo-section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:20px;padding-bottom:20px;margin-bottom:22px;border-bottom:1px solid var(--line)}.seo-section-head p{margin:0;color:var(--muted);max-width:680px}.seo-badge{display:inline-flex;align-items:center;padding:6px 10px;border-radius:999px;background:var(--blue-soft);color:var(--blue-dark);font-size:11px;font-weight:850;text-transform:uppercase;letter-spacing:.08em;white-space:nowrap}.seo-badge.success{background:var(--green-soft);color:var(--green)}.seo-dns-callout{display:grid;gap:5px;margin-bottom:22px;padding:16px 18px;border:1px solid #bfdbfe;border-radius:12px;background:#eff6ff;color:#1e3a8a}.seo-dns-callout span{color:#475569}.seo-dns-callout code{font-weight:800}.seo-savebar{position:sticky;bottom:18px;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:18px;padding:15px 18px;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.96);backdrop-filter:blur(14px);box-shadow:0 14px 38px rgba(15,23,42,.14)}.seo-savebar strong,.seo-savebar span{display:block}.seo-savebar span{font-size:12px;color:var(--muted);margin-top:2px}.seo-preview{border:1px solid var(--line);border-radius:12px;padding:18px;background:#fff}.seo-preview-label{font-size:10px;font-weight:850;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin-bottom:10px}.seo-preview-title{color:#1a0dab;font-size:19px;line-height:1.3}.seo-preview-url{color:#188038;font-size:12px;margin:4px 0}.seo-preview-description{color:#4d5156;font-size:13px;line-height:1.45}@media(max-width:700px){.seo-section-head,.seo-savebar{align-items:stretch;flex-direction:column}.seo-savebar .btn{width:100%}} + +/* Download catalogue and distribution admin */ +.distribution-admin{max-width:1200px;margin:auto;padding-bottom:55px}.distribution-header{display:flex;justify-content:space-between;align-items:end;gap:24px}.distribution-header-actions{display:flex;gap:10px;flex-wrap:wrap}.distribution-summary{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:25px 0}.distribution-summary>div{background:#fff;border:1px solid #e4e7ec;border-radius:14px;padding:18px 20px}.distribution-summary span,.distribution-summary strong{display:block}.distribution-summary span{font-size:12px;color:#667085}.distribution-summary strong{font-size:28px;color:#101828;margin-top:5px}.distribution-notice,.distribution-errors{padding:14px 18px;border-radius:10px;margin:16px 0;background:#ecfdf3;border:1px solid #a6f4c5;color:#067647}.distribution-errors{background:#fef3f2;border-color:#fecdca;color:#b42318}.distribution-table{margin-bottom:22px}.distribution-row-actions{display:flex;gap:14px;justify-content:flex-end;align-items:center;white-space:nowrap}.distribution-row-actions form{margin:0}.dist-action{background:none;border:0;padding:4px 0;color:#245bc6;font-weight:750;font-size:13px;cursor:pointer;text-decoration:none}.dist-unpublish{color:#b54708}.dist-publish{color:#067647}.distribution-panel{background:#fff;border:1px solid #e4e7ec;border-radius:14px;margin:12px 0;overflow:hidden}.distribution-panel>summary{list-style:none;display:flex;justify-content:space-between;align-items:center;gap:12px;cursor:pointer;padding:19px 24px}.distribution-panel>summary::-webkit-details-marker{display:none}.distribution-panel>summary strong,.distribution-panel>summary small{display:block}.distribution-panel>summary strong{font-size:16px;color:#101828}.distribution-panel>summary small{font-size:12px;color:#667085;margin-top:4px}.dist-chevron{font-size:22px;color:#667085}.distribution-panel[open] .dist-chevron{transform:rotate(45deg)}.distribution-form{padding:24px;border-top:1px solid #eef2f6;display:grid;gap:16px}.distribution-form label{display:grid;gap:6px;font-size:13px;font-weight:750;color:#344054}.distribution-form input:not([type=checkbox]),.distribution-form textarea{width:100%;border:1px solid #d0d5dd;border-radius:9px;padding:11px 12px;background:#fff;color:#101828;font:inherit}.distribution-form .form-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.distribution-file{border:1px dashed #cbd5e1;border-radius:12px;padding:16px;background:#f8fafc}.distribution-file small{color:#667085;font-weight:400}.distribution-file input{background:transparent!important;border:0!important;padding-left:0!important}.distribution-check{display:flex!important;align-items:center;gap:12px!important;padding:12px 0}.distribution-check input{width:18px;height:18px;flex:none}.distribution-check strong,.distribution-check small{display:block}.distribution-check small{font-weight:400;color:#667085;margin-top:3px}.distribution-form>.btn{justify-self:start}.distribution-delete{padding:0 24px 22px}.distribution-catalogue{background:#f6f8fc;padding:76px 0;border-top:1px solid #e4e7ec}.distribution-product-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;margin-top:28px}.distribution-product-card{background:#fff;border:1px solid #e4e7ec;border-radius:18px;padding:26px;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(16,24,40,.04)}.distribution-product-top,.distribution-product-footer{display:flex;justify-content:space-between;align-items:center;gap:12px}.distribution-platform{background:#eff6ff;color:#175cd3;border-radius:20px;padding:5px 10px;font-size:11px;font-weight:800}.distribution-version{font-size:12px;color:#667085}.distribution-product-icon{display:grid;place-items:center;width:48px;height:48px;background:#0b1730;color:#e8bc66;border-radius:12px;font-size:23px;margin:26px 0 12px}.distribution-product-card h3{font-size:22px;letter-spacing:-.03em;margin:0 0 8px;color:#101828}.distribution-product-card p{font-size:14px;line-height:1.6;color:#667085;flex:1;margin:0 0 28px}.distribution-product-footer{border-top:1px solid #eef2f6;padding-top:20px}.distribution-product-footer>span{font-size:12px;color:#667085}.checksum-box summary{cursor:pointer;font-size:12px;font-weight:750;color:#475467}.checksum-box code{display:block;margin-top:12px}.download-primary{background:#e8bc66!important;color:#101828!important}.download-icon{background:#101828!important;color:#e8bc66!important} +@media(max-width:900px){.distribution-product-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.distribution-header{align-items:start;flex-direction:column}}@media(max-width:640px){.distribution-product-grid,.distribution-summary{grid-template-columns:1fr}.distribution-form .form-grid{grid-template-columns:1fr}.distribution-row-actions{flex-wrap:wrap}.distribution-header-actions{width:100%}.distribution-table .pro-table{min-width:730px}} @stack('head') diff --git a/routes/web.php b/routes/web.php index f1ae5c0..a25dd50 100755 --- a/routes/web.php +++ b/routes/web.php @@ -79,6 +79,7 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name Route::get('/download-products', [DownloadProductController::class, 'index'])->name('download-products.index'); Route::post('/download-products', [DownloadProductController::class, 'store'])->name('download-products.store'); Route::put('/download-products/{product}', [DownloadProductController::class, 'update'])->name('download-products.update'); + Route::patch('/download-products/{product}/publication', [DownloadProductController::class, 'publication'])->name('download-products.publication'); Route::delete('/download-products/{product}', [DownloadProductController::class, 'destroy'])->name('download-products.destroy'); Route::get('/releases', [AdminReleaseController::class, 'index'])->name('releases.index'); Route::get('/releases/create', [AdminReleaseController::class, 'create'])->name('releases.create');