Compare commits
6
Commits
2139e55137
...
c6411075d9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6411075d9 | ||
|
|
ae7b36a4bf | ||
|
|
e7c9e839fb | ||
|
|
259b1eac12 | ||
|
|
9ae51ada72 | ||
|
|
85f9e56b03 |
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace App\Http\Controllers\Admin;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DownloadProduct;
|
||||
use Illuminate\Http\Request;
|
||||
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: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: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.'); }
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\SiteSetting;
|
||||
use App\Models\DownloadProduct;
|
||||
use App\Models\SonarDeskRelease;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -14,10 +15,15 @@ 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')
|
||||
->when($latest, fn ($query) => $query->whereRaw('LOWER(name) <> ?', ['sonardesk']))
|
||||
->orderBy('name')->get();
|
||||
|
||||
return view('downloads.index', compact('latest', 'releases', 'settings'));
|
||||
return view('downloads.index', compact('latest', 'releases', 'settings', 'products'));
|
||||
}
|
||||
|
||||
public function latest(): BinaryFileResponse
|
||||
@@ -32,6 +38,13 @@ final class ReleaseController extends Controller
|
||||
return $this->send($release);
|
||||
}
|
||||
|
||||
public function productDownload(DownloadProduct $product): BinaryFileResponse
|
||||
{
|
||||
abort_unless($product->is_published && $product->file_path && Storage::disk('local')->exists($product->file_path), 404);
|
||||
$product->increment('download_count');
|
||||
return response()->download(Storage::disk('local')->path($product->file_path), $product->original_filename);
|
||||
}
|
||||
|
||||
public function appcast(): Response
|
||||
{
|
||||
$releases = SonarDeskRelease::published()->where('channel', 'stable')->orderByDesc('published_at')->limit(20)->get();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
final class DownloadProduct extends Model {
|
||||
protected $fillable = ['name','description','version','platform','file_path','original_filename','file_size','download_count','is_published'];
|
||||
protected function casts(): array { return ['is_published'=>'boolean','file_size'=>'integer','download_count'=>'integer']; }
|
||||
public function getFormattedSizeAttribute(): string { return $this->file_size >= 1048576 ? number_format($this->file_size/1048576,1).' MB' : number_format($this->file_size/1024,1).' KB'; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration {
|
||||
public function up(): void {
|
||||
Schema::create('download_products', function (Blueprint $table) {
|
||||
$table->id(); $table->string('name'); $table->text('description')->nullable();
|
||||
$table->string('version')->nullable(); $table->string('platform')->nullable();
|
||||
$table->string('file_path')->nullable(); $table->string('original_filename')->nullable();
|
||||
$table->unsignedBigInteger('file_size')->default(0); $table->unsignedBigInteger('download_count')->default(0);
|
||||
$table->boolean('is_published')->default(false); $table->timestamps();
|
||||
});
|
||||
}
|
||||
public function down(): void { Schema::dropIfExists('download_products'); }
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
@extends('layouts.app', ['title' => 'Download products — Admin'])
|
||||
@section('content')
|
||||
<div class="distribution-admin">
|
||||
<section class="portal-header distribution-header"><div><span class="eyebrow">Distribution / Product management</span><h1>Download products</h1><p class="lead compact">Manage installers, releases and publication status in one place.</p></div><div class="distribution-header-actions"><a class="btn btn-secondary" href="{{ route('release') }}" target="_blank" rel="noopener">View public downloads ↗</a><a class="btn btn-secondary" href="{{ route('admin.releases.index') }}">Releases</a></div></section>
|
||||
@if(session('status'))<div class="distribution-notice" role="status">{{ session('status') }}</div>@endif
|
||||
@if($errors->any())<div class="distribution-errors" role="alert"><strong>Please correct the following:</strong><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
|
||||
<div class="distribution-summary"><div><span>Total products</span><strong>{{ $products->count() }}</strong></div><div><span>Published</span><strong>{{ $products->where('is_published', true)->count() }}</strong></div><div><span>Drafts</span><strong>{{ $products->where('is_published', false)->count() }}</strong></div><div><span>Total downloads</span><strong>{{ number_format($products->sum('download_count')) }}</strong></div></div>
|
||||
<section class="admin-table-card distribution-table"><div class="admin-table-title"><div><h2>Product catalogue</h2><p>Publish or unpublish products instantly, or edit their details.</p></div><button class="btn btn-primary" type="button" onclick="const panel=document.getElementById('new-product');panel.open=true;panel.scrollIntoView({behavior:'smooth',block:'start'});">+ Add product</button></div><div class="table-scroll"><table class="pro-table"><thead><tr><th>Product</th><th>Version / Platform</th><th>Downloads</th><th>Status</th><th style="text-align:right">Actions</th></tr></thead><tbody>
|
||||
@forelse($products as $product)<tr><td><strong>{{ $product->name }}</strong><small>{{ \Illuminate\Support\Str::limit($product->description ?: $product->original_filename, 70) }}</small></td><td><strong>{{ $product->version ?: '—' }}</strong><small>{{ $product->platform ?: 'Unspecified' }} · {{ $product->formatted_size }}</small></td><td>{{ number_format($product->download_count) }}</td><td><span class="status-chip {{ $product->is_published ? 'success' : 'muted' }}">{{ $product->is_published ? 'Published' : 'Draft' }}</span></td><td><div class="distribution-row-actions"><button type="button" class="dist-action" onclick="const panel=document.getElementById('edit-product-{{ $product->id }}');panel.open=true;panel.scrollIntoView({behavior:'smooth',block:'start'});">Edit</button><form method="post" action="{{ route('admin.download-products.publication', $product) }}">@csrf @method('PATCH')<input type="hidden" name="is_published" value="{{ $product->is_published ? '0' : '1' }}"><button type="submit" class="dist-action {{ $product->is_published ? 'dist-unpublish' : 'dist-publish' }}">{{ $product->is_published ? 'Unpublish' : 'Publish' }}</button></form></div></td></tr>
|
||||
@empty<tr><td colspan="5" class="empty-cell">No additional products yet. Add your first installer below.</td></tr>@endforelse
|
||||
</tbody></table></div></section>
|
||||
<details class="distribution-panel" id="new-product" @if($errors->any() && !old('_method')) open @endif><summary><span><strong>Add a new product</strong><small>Upload an installer and choose whether to publish it.</small></span><span class="dist-chevron">+</span></summary><form class="distribution-form" method="post" action="{{ route('admin.download-products.store') }}" enctype="multipart/form-data">@csrf<div class="form-grid"><label>Product name<input name="name" value="{{ old('name') }}" required maxlength="150" placeholder="e.g. SonarDesk CLI"></label><label>Version<input name="version" value="{{ old('version') }}" maxlength="80" placeholder="e.g. 2.1.0"></label><label>Platform<input name="platform" value="{{ old('platform') }}" maxlength="120" placeholder="macOS, Windows, Linux…"></label></div><label>Description<textarea name="description" rows="3" maxlength="3000" placeholder="Describe what this product does">{{ old('description') }}</textarea></label><label class="distribution-file">Installer / package <small>Maximum 1 GB</small><input type="file" name="package" required></label><label class="distribution-check"><input type="checkbox" name="is_published" value="1" @checked(old('is_published'))><span><strong>Publish immediately</strong><small>Make this download available on the public page.</small></span></label><button class="btn btn-primary" type="submit">Add product</button></form></details>
|
||||
@foreach($products as $product)<details class="distribution-panel" id="edit-product-{{ $product->id }}"><summary><span><strong>Edit {{ $product->name }}</strong><small>{{ $product->original_filename }} · {{ $product->formatted_size }}</small></span><span class="dist-chevron">+</span></summary><form class="distribution-form" method="post" action="{{ route('admin.download-products.update',$product) }}" enctype="multipart/form-data">@csrf @method('PUT')<div class="form-grid"><label>Product name<input name="name" value="{{ $product->name }}" required maxlength="150"></label><label>Version<input name="version" value="{{ $product->version }}" maxlength="80"></label><label>Platform<input name="platform" value="{{ $product->platform }}" maxlength="120"></label></div><label>Description<textarea name="description" rows="3" maxlength="3000">{{ $product->description }}</textarea></label><label class="distribution-file">Replace package (optional)<small>Current: {{ $product->original_filename }} · Maximum 1 GB</small><input type="file" name="package"></label><label class="distribution-check"><input type="checkbox" name="is_published" value="1" @checked($product->is_published)><span><strong>Published</strong><small>Untick to save this product as a draft.</small></span></label><button class="btn btn-primary" type="submit">Save changes</button></form><form class="distribution-delete" method="post" action="{{ route('admin.download-products.destroy',$product) }}" onsubmit="return confirm('Permanently delete this product and its installer?')">@csrf @method('DELETE')<button type="submit" class="danger-outline">Delete product</button></form></details>@endforeach
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,6 +1,6 @@
|
||||
@extends('layouts.app', ['title' => 'Releases — SonarDesk Admin'])
|
||||
@section('content')
|
||||
<section class="portal-header admin-release-head"><div><span class="eyebrow">Distribution</span><h1>Release management</h1><p class="lead compact">Publish SonarDesk builds, manage the public download experience and keep Sparkle updates under control.</p></div><div class="support-actions"><a class="btn btn-secondary" href="{{ route('admin.releases.settings') }}">Download page settings</a><a class="btn btn-primary" href="{{ route('admin.releases.create') }}">New release</a></div></section>
|
||||
<section class="portal-header admin-release-head"><div><span class="eyebrow">Distribution</span><h1>Release management</h1><p class="lead compact">Publish SonarDesk builds, manage the public download experience and keep Sparkle updates under control.</p></div><div class="support-actions"><a class="btn btn-secondary" href="{{ route('admin.download-products.index') }}">Other products</a><a class="btn btn-secondary" href="{{ route('admin.releases.settings') }}">Download page settings</a><a class="btn btn-primary" href="{{ route('admin.releases.create') }}">New release</a></div></section>
|
||||
<div class="grid admin-metrics"><div class="card"><strong>{{ $releases->total() }}</strong><p>Total releases</p></div><div class="card"><strong>{{ $publishedCount }}</strong><p>Published</p></div><div class="card"><strong>{{ number_format($downloadCount) }}</strong><p>Total downloads</p></div><div class="card"><strong>Live</strong><p>Sparkle feed</p></div></div>
|
||||
<div class="admin-table-card"><div class="admin-table-title"><div><h2>SonarDesk builds</h2><p>Only published builds with an uploaded package are available publicly.</p></div><a href="{{ route('release') }}" class="btn btn-secondary" target="_blank">View public page</a></div><div class="table-scroll"><table class="pro-table"><thead><tr><th>Release</th><th>Channel</th><th>Status</th><th>Package</th><th>Downloads</th><th>Published</th><th></th></tr></thead><tbody>@forelse($releases as $release)<tr><td><strong>SonarDesk {{ $release->version }}</strong><small>Build {{ $release->build_number }} · macOS {{ $release->minimum_macos ?: 'Any supported' }}</small></td><td><span class="status-chip">{{ ucfirst($release->channel) }}</span></td><td>@if($release->is_published)<span class="status-chip success">Published</span>@else<span class="status-chip muted">Draft</span>@endif</td><td>{{ $release->file_path ? $release->formatted_size : 'No package' }}</td><td>{{ number_format($release->download_count) }}</td><td>{{ $release->published_at?->format('j M Y H:i') ?: '—' }}</td><td class="table-actions"><a class="btn btn-secondary" href="{{ route('admin.releases.edit',$release) }}">Edit</a><form method="post" action="{{ route('admin.releases.destroy',$release) }}" onsubmit="return confirm('Delete this release and its package?')">@csrf @method('DELETE')<button class="danger-outline" type="submit">Delete</button></form></td></tr>@empty<tr><td colspan="7" class="empty-cell">No releases yet. Create your first signed SonarDesk build.</td></tr>@endforelse</tbody></table></div></div>
|
||||
{{ $releases->links() }}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
@section('fullbleed')
|
||||
<main class="download-page"><section class="download-hero"><div class="download-shell"><div class="download-copy"><span class="eyebrow">{{ $settings['eyebrow'] }}</span><h1>{{ $settings['title'] }}</h1><p>{{ $settings['intro'] }}</p>@if($latest)<div class="download-actions"><a class="btn download-primary" href="{{ route('download.latest') }}"><span class="download-icon">↓</span><span><strong>Download SonarDesk {{ $latest->version }}</strong><small>{{ $latest->formatted_size }} · macOS {{ $latest->minimum_macos ?: 'supported versions' }}</small></span></a><a class="btn download-secondary" href="#release-notes">View release notes</a></div><div class="release-verification"><span>✓ Signed distribution</span><span>✓ SHA-256 verified</span><span>✓ Secure licence activation</span></div>@else<div class="no-release"><strong>The next SonarDesk release is being prepared.</strong><span>Check back shortly or contact support.</span></div>@endif</div><div class="product-visual"><div class="mac-window"><div class="mac-titlebar"><i></i><i></i><i></i><span>SonarDesk</span></div><div class="mac-content"><div class="mini-sidebar"><img src="{{ asset('icons/sonardesk-icon-64.png') }}" alt=""><b>SonarDesk</b><span class="active"></span><span></span><span></span><span></span><span></span></div><div class="mini-dashboard"><div class="mini-head"><b>Project health</b><em>All systems monitored</em></div><div class="mini-stats"><i></i><i></i><i></i><i></i></div><div class="mini-chart"><span></span><span></span><span></span><span></span><span></span><span></span></div></div></div></div><div class="visual-badge"><span>Native</span><strong>Built for macOS</strong></div></div></div></section>
|
||||
<section class="download-confidence"><div class="download-shell confidence-grid"><div><span>Native macOS</span><strong>Fast, focused desktop workflow</strong></div><div><span>SonarQube</span><strong>Your server remains the source of truth</strong></div><div><span>Security</span><strong>Tokens protected by macOS Keychain</strong></div><div><span>Updates</span><strong>Professional in-app release delivery</strong></div></div></section>
|
||||
<div class="download-shell download-content"><section id="release-notes" class="release-notes-panel">@if($latest)<div class="release-heading"><div><span class="eyebrow">Latest stable release</span><h2>SonarDesk {{ $latest->version }}</h2><p>Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}</p></div><a class="btn btn-primary" href="{{ route('download.release',$latest) }}">Download {{ $latest->version }}</a></div><div class="release-note-copy">{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}</div><div class="checksum-box"><span>SHA-256</span><code>{{ $latest->sha256 }}</code></div>@endif</section><aside class="requirements-panel"><span class="eyebrow">Before you install</span><h2>System requirements</h2><p>{{ $settings['requirements'] }}</p><ol><li><span>1</span><div><strong>Download</strong><small>Get the latest signed SonarDesk package.</small></div></li><li><span>2</span><div><strong>Install</strong><small>Open the DMG and move SonarDesk to Applications.</small></div></li><li><span>3</span><div><strong>Activate</strong><small>Enter the licence key delivered after purchase.</small></div></li></ol><a href="{{ route('buy') }}">Need a licence? View pricing →</a></aside></div>
|
||||
<div class="download-shell download-content">@if($latest)<section id="release-notes" class="release-notes-panel"><div class="release-heading"><div><span class="eyebrow">Latest stable release</span><h2>SonarDesk {{ $latest->version }}</h2><p>Build {{ $latest->build_number }} · Released {{ $latest->published_at?->format('j F Y') }}</p></div><a class="btn btn-primary" href="{{ route('download.release',$latest) }}">Download {{ $latest->version }}</a></div><div class="release-note-copy">{!! nl2br(e($latest->release_notes ?: 'This release contains the latest SonarDesk improvements and maintenance updates.')) !!}</div><details class="checksum-box"><summary>SHA-256 checksum · Verify download</summary><code>{{ $latest->sha256 }}</code></details></section>@endif<aside class="requirements-panel"><span class="eyebrow">Before you install</span><h2>System requirements</h2><p>{{ $settings['requirements'] }}</p><ol><li><span>1</span><div><strong>Download</strong><small>Get the latest signed SonarDesk package.</small></div></li><li><span>2</span><div><strong>Install</strong><small>Open the DMG and move SonarDesk to Applications.</small></div></li><li><span>3</span><div><strong>Activate</strong><small>Enter the licence key delivered after purchase.</small></div></li></ol><a href="{{ route('buy') }}">Need a licence? View pricing →</a></aside></div>
|
||||
@if($products->isNotEmpty())
|
||||
<section class="distribution-catalogue"><div class="download-shell"><div class="section-heading"><span class="eyebrow">Software catalogue</span><h2>More downloads</h2><p>Additional tools and installers available from our team.</p></div><div class="distribution-product-grid">@foreach($products as $product)<article class="distribution-product-card"><div class="distribution-product-top"><span class="distribution-platform">{{ $product->platform ?: 'Software' }}</span><span class="distribution-version">{{ $product->version ? 'v'.$product->version : 'Latest' }}</span></div><div class="distribution-product-icon" aria-hidden="true">↓</div><h3>{{ $product->name }}</h3><p>{{ $product->description ?: 'Download the latest available package.' }}</p><div class="distribution-product-footer"><span>{{ $product->formatted_size }}</span><a class="btn btn-primary" href="{{ route('download.product',$product) }}" aria-label="Download {{ $product->name }}">Download →</a></div></article>@endforeach</div></div></section>
|
||||
@endif
|
||||
<section class="release-history"><div class="download-shell"><div class="section-heading"><span class="eyebrow">Release archive</span><h2>Previous versions</h2><p>Published SonarDesk builds remain available here for customers who need a previous release.</p></div><div class="release-history-list">@forelse($releases as $release)<article><div><strong>SonarDesk {{ $release->version }}</strong><span>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}</span></div><div><span>{{ $release->formatted_size }}</span><a href="{{ route('download.release',$release) }}">Download</a></div></article>@empty<p>No published releases are available yet.</p>@endforelse</div>{{ $releases->links() }}</div></section>
|
||||
<section class="download-support"><div class="download-shell"><div><span class="eyebrow">Support</span><h2>Installation or activation help?</h2><p>{{ $settings['support_text'] }}</p></div><a class="btn btn-secondary" href="{{ route('support') }}">Contact SonarDesk support</a></div></section></main>
|
||||
@endsection
|
||||
|
||||
Executable → Regular
+76
@@ -198,6 +198,81 @@
|
||||
.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}}
|
||||
|
||||
/* SonarDesk compact downloads layout */
|
||||
.download-page .download-content {
|
||||
display: block;
|
||||
padding-top: 28px;
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
|
||||
.download-page .release-notes-panel {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.download-page .requirements-panel {
|
||||
padding: 22px 26px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.download-page .requirements-panel ol {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.download-page .requirements-panel li {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.download-page .distribution-catalogue {
|
||||
padding: 36px 0 48px;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-card {
|
||||
padding: 18px 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-card h3 {
|
||||
font-size: 17px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-card p {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.download-page .distribution-product-footer {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.download-page .release-history {
|
||||
padding: 36px 0;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.download-page .distribution-product-grid,
|
||||
.download-page .requirements-panel ol {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@stack('head')
|
||||
</head>
|
||||
@@ -231,6 +306,7 @@
|
||||
<a href="{{ route('admin.index') }}">Admin</a>
|
||||
<a href="{{ route('admin.users.index') }}">Users</a>
|
||||
<a href="{{ route('admin.releases.index') }}">Releases</a>
|
||||
<a href="{{ route('admin.download-products.index') }}">Download products</a>
|
||||
<a href="{{ route('admin.support.index') }}">
|
||||
Support
|
||||
@if ($supportUnread > 0)
|
||||
|
||||
Executable → Regular
+7
@@ -13,6 +13,7 @@ use App\Http\Controllers\Auth\EmailVerificationController;
|
||||
use App\Http\Controllers\Auth\PasswordController;
|
||||
use App\Http\Controllers\Auth\RegisteredUserController;
|
||||
use App\Http\Controllers\ReleaseController;
|
||||
use App\Http\Controllers\Admin\DownloadProductController;
|
||||
use App\Http\Controllers\Support\SupportController;
|
||||
use App\Http\Middleware\AdminOnly;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
@@ -25,6 +26,7 @@ Route::get('/', fn () => view('portal.welcome', ['latestRelease' => \App\Models\
|
||||
Route::view('/features', 'features')->name('features');
|
||||
Route::get('/release', [ReleaseController::class, 'index'])->name('release');
|
||||
Route::get('/download', [ReleaseController::class, 'latest'])->name('download.latest');
|
||||
Route::get('/downloads/products/{product}', [ReleaseController::class, 'productDownload'])->name('download.product');
|
||||
Route::get('/downloads/{release}', [ReleaseController::class, 'download'])->name('download.release');
|
||||
Route::get('/updates/appcast.xml', [ReleaseController::class, 'appcast'])->name('updates.appcast');
|
||||
Route::get('/support', [SupportController::class, 'create'])->name('support');
|
||||
@@ -74,6 +76,11 @@ Route::middleware(['auth', 'verified', AdminOnly::class])->prefix('admin')->name
|
||||
Route::get('/cms/{page}/edit', [AdminCmsPageController::class, 'edit'])->name('cms.edit');
|
||||
Route::put('/cms/{page}', [AdminCmsPageController::class, 'update'])->name('cms.update');
|
||||
Route::delete('/cms/{page}', [AdminCmsPageController::class, 'destroy'])->name('cms.destroy');
|
||||
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');
|
||||
Route::post('/releases', [AdminReleaseController::class, 'store'])->name('releases.store');
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
@extends('layouts.app', ['title' => 'Downloads — SonarDesk'])
|
||||
@section('fullbleed')
|
||||
<main class="download-page sd-downloads-v3">
|
||||
<div class="download-shell sd3-container">
|
||||
<header class="sd3-heading"><div><span class="eyebrow">SOFTWARE DISTRIBUTION</span><h1>Downloads</h1><p>Installers and releases, all in one place.</p></div></header>
|
||||
@if($latest)
|
||||
<section class="sd3-featured" aria-label="Featured SonarDesk release"><div><span class="sd3-tag">FEATURED · macOS</span><h2>SonarDesk <span>{{ $latest->version }}</span></h2><p>A native SonarQube workspace for macOS. Monitor project health and manage quality from your desktop.</p><div class="sd3-meta">{{ $latest->formatted_size }} · Build {{ $latest->build_number }} · {{ $latest->published_at?->format('j M Y') }}</div></div><div class="sd3-feature-actions"><a class="btn btn-primary" href="{{ route('download.latest') }}">Download for macOS ↓</a><a href="#release-notes">Release notes →</a></div></section>
|
||||
@endif
|
||||
@if($products->isNotEmpty())
|
||||
<section class="sd3-section" aria-label="Software catalogue"><div class="sd3-section-title"><div><span class="eyebrow">SOFTWARE CATALOGUE</span><h2>Available products</h2></div><span>{{ $products->count() }} {{ $products->count() === 1 ? 'product' : 'products' }}</span></div>
|
||||
<div class="sd3-products">@foreach($products as $product)
|
||||
<article class="sd3-product"><div class="sd3-product-mark" aria-hidden="true">↓</div><div class="sd3-product-copy"><h3>{{ $product->name }}</h3><p>{{ $product->description ?: 'Software installer available for download.' }}</p><div class="sd3-product-meta"><span>{{ $product->platform ?: 'Software' }}</span><span>{{ $product->version ? 'v'.$product->version : 'Latest' }}</span><span>{{ $product->formatted_size }}</span></div></div><a class="sd3-download-link" href="{{ route('download.product', $product) }}" aria-label="Download {{ $product->name }}">Download ↓</a></article>
|
||||
@endforeach</div></section>
|
||||
@endif
|
||||
@if($latest)
|
||||
<details class="sd3-details" id="release-notes"><summary>SonarDesk {{ $latest->version }} release notes <span>Show details +</span></summary><div class="sd3-details-body"><p>{!! nl2br(e($latest->release_notes ?: 'Latest SonarDesk improvements and maintenance updates.')) !!}</p>@if($latest->sha256)<div><strong>SHA-256</strong><code>{{ $latest->sha256 }}</code></div>@endif</div></details>
|
||||
@endif
|
||||
<section class="sd3-section sd3-archive"><div class="sd3-section-title"><div><span class="eyebrow">RELEASE HISTORY</span><h2>Previous SonarDesk versions</h2></div></div><div class="sd3-history">@forelse($releases as $release)<div class="sd3-history-row"><div><strong>SonarDesk {{ $release->version }}</strong><span>Build {{ $release->build_number }} · {{ ucfirst($release->channel) }} · {{ $release->published_at?->format('j M Y') }}</span></div><span>{{ $release->formatted_size }}</span><a href="{{ route('download.release', $release) }}">Download ↓</a></div>@empty<p class="sd3-empty">No previous releases available.</p>@endforelse</div>{{ $releases->links() }}</section>
|
||||
<aside class="sd3-help"><div><h2>Need help installing or activating?</h2><p>{{ $settings['support_text'] }}</p></div><a class="btn btn-secondary" href="{{ route('support') }}">Contact support →</a></aside>
|
||||
</div>
|
||||
</main>
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user