added theme support
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">CMS</span><h1>{{ $page->exists ? 'Edit page' : 'New page' }}</h1></div><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Back to pages</a></section>
|
||||
<div class="panel"><form method="post" action="{{ $page->exists ? route('admin.cms.update',$page) : route('admin.cms.store') }}">@csrf @if($page->exists) @method('PUT') @endif
|
||||
<label>Title<input name="title" value="{{ old('title',$page->title) }}" required maxlength="160"></label>
|
||||
<label>Slug<input name="slug" value="{{ old('slug',$page->slug) }}" placeholder="generated-from-title" maxlength="180"></label>
|
||||
<label>Excerpt<textarea name="excerpt" rows="3" maxlength="1000">{{ old('excerpt',$page->excerpt) }}</textarea></label>
|
||||
<label>Body<textarea name="body" rows="18" required>{{ old('body',$page->body) }}</textarea><small>Plain text is rendered safely with paragraphs and line breaks.</small></label>
|
||||
<section class="seo-section" style="padding:24px 0 4px;border-top:1px solid var(--line);margin-top:24px">
|
||||
<div class="seo-section-head"><div><span class="eyebrow">Search appearance</span><h2>Page SEO</h2><p>Override global defaults only where this page needs specific search or social metadata.</p></div><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">Global SEO settings</a></div>
|
||||
<div class="form-grid two"><label>SEO title<input name="meta_title" value="{{ old('meta_title',$page->meta_title) }}" maxlength="160" placeholder="Defaults to the page title"></label><label>Canonical URL<input type="url" name="canonical_url" value="{{ old('canonical_url',$page->canonical_url) }}" maxlength="2048" placeholder="Leave blank for this page URL"></label></div>
|
||||
<label>SEO description<textarea name="meta_description" rows="3" maxlength="320" placeholder="Defaults to the excerpt">{{ old('meta_description',$page->meta_description) }}</textarea></label>
|
||||
<label>Keywords<input name="meta_keywords" value="{{ old('meta_keywords',$page->meta_keywords) }}" maxlength="500" placeholder="Optional comma-separated search topics"></label>
|
||||
<div class="form-grid two"><label>Open Graph title<input name="og_title" value="{{ old('og_title',$page->og_title) }}" maxlength="160" placeholder="Defaults to SEO title"></label><label>Open Graph image URL<input type="url" name="og_image" value="{{ old('og_image',$page->og_image) }}" maxlength="2048" placeholder="Defaults to global social image"></label></div>
|
||||
<label>Open Graph description<textarea name="og_description" rows="3" maxlength="320" placeholder="Defaults to SEO description">{{ old('og_description',$page->og_description) }}</textarea></label>
|
||||
<div class="form-grid two"><label>Sitemap priority<input type="number" name="sitemap_priority" value="{{ old('sitemap_priority',$page->sitemap_priority ?? '0.5') }}" min="0" max="1" step="0.1" required></label><label>Change frequency<select name="sitemap_change_frequency" required>@foreach(['always','hourly','daily','weekly','monthly','yearly','never'] as $frequency)<option value="{{ $frequency }}" @selected(old('sitemap_change_frequency',$page->sitemap_change_frequency ?? 'monthly') === $frequency)>{{ ucfirst($frequency) }}</option>@endforeach</select></label></div>
|
||||
<div class="form-grid two"><label class="consent-check"><input type="checkbox" name="robots_index" value="1" @checked(old('robots_index',$page->exists ? $page->robots_index : true))><span>Allow search engines to index this page</span></label><label class="consent-check"><input type="checkbox" name="robots_follow" value="1" @checked(old('robots_follow',$page->exists ? $page->robots_follow : true))><span>Allow crawlers to follow links</span></label></div>
|
||||
<label class="consent-check"><input type="checkbox" name="include_in_sitemap" value="1" @checked(old('include_in_sitemap',$page->exists ? $page->include_in_sitemap : true))><span>Include this page in sitemap.xml</span></label>
|
||||
</section>
|
||||
<label class="consent-check"><input type="checkbox" name="is_published" value="1" @checked(old('is_published',$page->is_published))><span>Published</span></label>
|
||||
<div class="support-actions"><button type="submit">Save page</button>@if($page->exists && $page->is_published)<a class="btn btn-secondary" target="_blank" href="{{ route('cms.show',$page->slug) }}">View page</a>@endif</div>
|
||||
</form></div>
|
||||
@if($page->exists)<form method="post" action="{{ route('admin.cms.destroy',$page) }}" style="margin-top:16px" onsubmit="return confirm('Delete this page?')">@csrf @method('DELETE')<button class="danger-outline">Delete page</button></form>@endif
|
||||
@endsection
|
||||
@@ -1,9 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">CMS</span><h1>Content pages</h1><p class="lead compact">Manage SonarDesk website content.</p></div><a class="btn" href="{{ route('admin.cms.create') }}">New page</a></section>
|
||||
<div class="panel"><div class="admin-ticket-list">
|
||||
@forelse($pages as $page)
|
||||
<a class="admin-ticket-row" href="{{ route('admin.cms.edit', $page) }}"><div><strong>{{ $page->title }}</strong><small>/pages/{{ $page->slug }}</small></div><span class="ticket-status {{ $page->is_published ? 'status-open' : '' }}">{{ $page->is_published ? 'Published' : 'Draft' }}</span><time>{{ $page->updated_at->diffForHumans() }}</time><span>›</span></a>
|
||||
@empty <p class="muted">No CMS pages yet.</p> @endforelse
|
||||
</div>{{ $pages->links() }}</div>
|
||||
@endsection
|
||||
@@ -1,15 +0,0 @@
|
||||
@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 (Markdown supported)<textarea name="description" rows="12" maxlength="12000" placeholder="## Overview Describe the product. ## Features - Feature one - Feature two">{{ 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 (Markdown supported)<textarea name="description" rows="12" maxlength="12000">{{ $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 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Licensing administration</h1><p class="lead compact">Manage licences, activations and customer support.</p></div></section>
|
||||
<div class="grid"><div class="card"><strong>{{ $licenseCount ?? '—' }}</strong><p>Licences</p></div><div class="card"><strong>{{ $activeCount ?? '—' }}</strong><p>Not revoked</p></div><div class="card"><strong>{{ $activationCount ?? '—' }}</strong><p>Active installations</p></div><div class="card"><strong>{{ $userCount ?? '—' }}</strong><p>Portal users</p></div></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:18px"><a class="btn btn-primary" href="{{ route('admin.releases.index') }}">Manage releases</a><a class="btn" href="{{ route('admin.licenses') }}">Manage licences</a><a class="btn" href="{{ route('admin.users.index') }}">Manage users</a><a class="btn btn-secondary" href="{{ route('admin.cms.index') }}">Manage content</a><a class="btn btn-secondary" href="{{ route('stylist.index') }}">Manage themes</a><a class="btn btn-secondary" href="{{ route('admin.seo.edit') }}">SEO & search</a><a class="btn btn-secondary" href="{{ route('admin.support.index') }}">Support inbox</a></div>
|
||||
@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Licence •••• {{ $license->license_key_last4 }}</h1><p>{{ $license->customer_name }} · {{ $license->customer_email }}</p><p>{{ $license->edition }} · {{ $license->activation_limit }} seats</p>@if($license->revoked_at)<form method="post" action="{{ route('admin.restore',$license) }}">@csrf<button>Restore licence</button></form>@else<form method="post" action="{{ route('admin.revoke',$license) }}">@csrf<label>Reason</label><input name="reason"><button class="danger">Revoke licence</button></form>@endif</div><div class="card"><h2>Activations</h2><table><tr><th>ID</th><th>Platform</th><th>Version</th><th>Status</th></tr>@foreach($license->activations as $a)<tr><td>{{ $a->installation_name ?: $a->installation_id }}</td><td>{{ $a->platform }}</td><td>{{ $a->app_version }}</td><td>{{ $a->deactivated_at ? 'Released':'Active' }}</td></tr>@endforeach</table></div>@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<h1>Licences</h1><form><input name="q" value="{{ request('q') }}" placeholder="Search email, name or last 4"><button>Search</button></form><div class="card"><table><tr><th>Customer</th><th>Edition</th><th>Key</th><th>Status</th></tr>@foreach($licenses as $l)<tr><td><a href="{{ route('admin.license',$l) }}">{{ $l->customer_email ?: 'Manual' }}</a></td><td>{{ $l->edition }}</td><td>•••• {{ $l->license_key_last4 }}</td><td>{{ $l->revoked_at ? 'Revoked':'Active' }}</td></tr>@endforeach</table>{{ $licenses->links() }}</div>@endsection
|
||||
@@ -1,9 +0,0 @@
|
||||
@extends('layouts.app', ['title' => ($release->exists ? 'Edit release' : 'New release').' — SonarDesk Admin'])
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Distribution</span><h1>{{ $release->exists ? 'Edit SonarDesk '.$release->version : 'Create a release' }}</h1><p class="lead compact">Upload the notarized distribution package and control exactly when it becomes public.</p></div><a class="btn btn-secondary" href="{{ route('admin.releases.index') }}">Back to releases</a></section>
|
||||
<form class="release-editor" method="post" enctype="multipart/form-data" action="{{ $release->exists ? route('admin.releases.update',$release) : route('admin.releases.store') }}">@csrf @if($release->exists) @method('PUT') @endif
|
||||
<div class="editor-main"><section class="editor-card"><div class="editor-card-head"><span>01</span><div><h2>Release identity</h2><p>Version and compatibility information shown to customers.</p></div></div><div class="form-grid"><label>Version<input name="version" value="{{ old('version',$release->version) }}" placeholder="1.2.0" required></label><label>Build number<input type="number" min="1" name="build_number" value="{{ old('build_number',$release->build_number ?: 1) }}" required></label><label>Channel<select name="channel"><option value="stable" @selected(old('channel',$release->channel)==='stable')>Stable</option><option value="beta" @selected(old('channel',$release->channel)==='beta')>Beta</option></select></label><label>Minimum macOS<input name="minimum_macos" value="{{ old('minimum_macos',$release->minimum_macos) }}" placeholder="14.0"></label></div></section>
|
||||
<section class="editor-card"><div class="editor-card-head"><span>02</span><div><h2>Distribution package</h2><p>DMG or ZIP. SHA-256 is calculated automatically after upload.</p></div></div>@if($release->file_path)<div class="package-current"><strong>{{ $release->original_filename }}</strong><span>{{ $release->formatted_size }} · SHA-256 {{ $release->sha256 }}</span></div>@endif<label class="upload-zone"><strong>{{ $release->file_path ? 'Replace package' : 'Choose distribution package' }}</strong><span>Signed and notarized .dmg or .zip · up to 1 GB</span><input type="file" name="package" accept=".dmg,.zip" {{ $release->exists ? '' : 'required' }}></label><label>Sparkle EdDSA signature<textarea name="sparkle_signature" rows="3" placeholder="Paste the signature generated by Sparkle's sign_update tool">{{ old('sparkle_signature',$release->sparkle_signature) }}</textarea></label></section>
|
||||
<section class="editor-card"><div class="editor-card-head"><span>03</span><div><h2>Release notes</h2><p>Professional customer-facing notes. Plain text is rendered safely with line breaks.</p></div></div><label>What's new<textarea name="release_notes" rows="12" placeholder="New in this release…">{{ old('release_notes',$release->release_notes) }}</textarea></label></section></div>
|
||||
<aside class="editor-side"><div class="editor-card sticky-card"><h2>Publishing</h2><p>A draft is never exposed publicly or in the Sparkle feed.</p><label class="toggle-row"><input type="checkbox" name="is_published" value="1" @checked(old('is_published',$release->is_published))><span><strong>Published</strong><small>Make this build downloadable</small></span></label><label class="toggle-row"><input type="checkbox" name="is_featured" value="1" @checked(old('is_featured',$release->is_featured))><span><strong>Featured release</strong><small>Mark as the highlighted build</small></span></label><button class="btn btn-primary full-width" type="submit">{{ $release->exists ? 'Save release' : 'Create release' }}</button>@if($release->is_published)<a class="btn btn-secondary full-width" href="{{ route('download.release',$release) }}">Test download</a>@endif</div></aside></form>
|
||||
@endsection
|
||||
@@ -1,7 +0,0 @@
|
||||
@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.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() }}
|
||||
@endsection
|
||||
@@ -1,5 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Download page — SonarDesk Admin'])
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Website</span><h1>Download page content</h1><p class="lead compact">Control the customer-facing download area without editing a template or deploying code.</p></div><a class="btn btn-secondary" href="{{ route('admin.releases.index') }}">Back to releases</a></section>
|
||||
<form class="editor-card settings-editor" method="post" action="{{ route('admin.releases.settings.update') }}">@csrf @method('PUT')<label>Eyebrow<input name="eyebrow" value="{{ old('eyebrow',$settings['downloads.eyebrow'] ?: 'SonarDesk for macOS') }}" required></label><label>Page title<input name="title" value="{{ old('title',$settings['downloads.title'] ?: 'Download SonarDesk') }}" required></label><label>Introduction<textarea name="intro" rows="4" required>{{ old('intro',$settings['downloads.intro'] ?: 'A focused native SonarQube workspace, designed for macOS and built for daily engineering work.') }}</textarea></label><label>System requirements<textarea name="requirements" rows="4" required>{{ old('requirements',$settings['downloads.requirements'] ?: 'Apple silicon and Intel Macs supported. A SonarQube server and valid SonarDesk licence are required.') }}</textarea></label><label>Support message<textarea name="support_text" rows="4" required>{{ old('support_text',$settings['downloads.support_text'] ?: 'Need help installing or activating SonarDesk? Our support team can help.') }}</textarea></label><div><button class="btn btn-primary" type="submit">Save website content</button> <a class="btn btn-secondary" target="_blank" href="{{ route('release') }}">Preview page</a></div></form>
|
||||
@endsection
|
||||
@@ -1,39 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header admin-release-head">
|
||||
<div><span class="eyebrow">Search & discovery</span><h1>SEO settings</h1><p class="lead compact">Control how SonarDesk appears in search engines and when pages are shared.</p></div>
|
||||
<div class="support-actions"><a class="btn btn-secondary" target="_blank" href="{{ route('sitemap') }}">View sitemap</a><a class="btn btn-secondary" target="_blank" href="{{ route('robots') }}">View robots.txt</a></div>
|
||||
</section>
|
||||
<form method="post" action="{{ route('admin.seo.update') }}" class="seo-admin-form">@csrf @method('PUT')
|
||||
<section class="panel seo-section">
|
||||
<div class="seo-section-head"><div><span class="eyebrow">Search appearance</span><h2>Site defaults</h2><p>Fallback metadata used whenever a page does not provide its own SEO values.</p></div><span class="seo-badge">Global</span></div>
|
||||
<div class="form-grid two">
|
||||
<label>Site name<input name="seo_site_name" value="{{ old('seo_site_name',$settings['seo_site_name'] ?: 'SonarDesk') }}" required maxlength="80"></label>
|
||||
<label>Title suffix<input name="seo_title_suffix" value="{{ old('seo_title_suffix',$settings['seo_title_suffix'] ?: 'SonarDesk') }}" maxlength="80"><small>Appended to page titles where appropriate.</small></label>
|
||||
</div>
|
||||
<label>Default SEO title<input name="seo_default_title" value="{{ old('seo_default_title',$settings['seo_default_title'] ?: 'SonarDesk — Native SonarQube Monitoring for macOS') }}" required maxlength="160"></label>
|
||||
<label>Default meta description<textarea name="seo_default_description" rows="3" maxlength="320" required>{{ old('seo_default_description',$settings['seo_default_description'] ?: 'Native SonarQube monitoring for macOS. Track projects, quality gates, bugs, vulnerabilities and code smells from one professional desktop app.') }}</textarea></label>
|
||||
<label>Default keywords<input name="seo_default_keywords" value="{{ old('seo_default_keywords',$settings['seo_default_keywords']) }}" maxlength="500" placeholder="SonarQube, macOS, code quality, developer tools"></label>
|
||||
<label>Canonical base URL<input type="url" name="seo_canonical_base_url" value="{{ old('seo_canonical_base_url',$settings['seo_canonical_base_url']) }}" maxlength="2048" placeholder="https://sonardesk.app"><small>Leave blank to use the application URL. Use the public production origin only.</small></label>
|
||||
</section>
|
||||
|
||||
<section class="panel seo-section">
|
||||
<div class="seo-section-head"><div><span class="eyebrow">Social sharing</span><h2>Open Graph & social</h2><p>Professional defaults for link previews across social networks and messaging applications.</p></div></div>
|
||||
<label>Default social image URL<input type="url" name="seo_default_og_image" value="{{ old('seo_default_og_image',$settings['seo_default_og_image']) }}" maxlength="2048" placeholder="https://…/og-sonardesk.png"><small>Recommended: 1200 × 630 px.</small></label>
|
||||
<div class="form-grid two"><label>Organization name<input name="seo_organization_name" value="{{ old('seo_organization_name',$settings['seo_organization_name'] ?: 'SonarDesk') }}" maxlength="120"></label><label>Organization logo URL<input type="url" name="seo_organization_logo" value="{{ old('seo_organization_logo',$settings['seo_organization_logo']) }}" maxlength="2048"></label></div>
|
||||
<label>X / Twitter account<input name="seo_twitter_site" value="{{ old('seo_twitter_site',$settings['seo_twitter_site']) }}" maxlength="40" placeholder="@SonarDesk"></label>
|
||||
</section>
|
||||
|
||||
<section class="panel seo-section">
|
||||
<div class="seo-section-head"><div><span class="eyebrow">Google Search Console</span><h2>Domain verification</h2><p>Use a Domain property in Google Search Console. Google will provide a TXT value that you add at your DNS provider.</p></div><span class="seo-badge success">DNS TXT</span></div>
|
||||
<div class="seo-dns-callout"><strong>Recommended verification method</strong><span>Add Google’s <code>google-site-verification=…</code> TXT record to the root domain. DNS verification covers all protocols and subdomains.</span></div>
|
||||
<label>Optional Google verification token<input name="seo_google_site_verification" value="{{ old('seo_google_site_verification',$settings['seo_google_site_verification']) }}" maxlength="255" placeholder="Only needed if you also choose HTML-tag verification"><small>DNS TXT verification does not require this field. It is available as a secondary verification method.</small></label>
|
||||
</section>
|
||||
|
||||
<section class="panel seo-section">
|
||||
<div class="seo-section-head"><div><span class="eyebrow">Crawler control</span><h2>Indexing</h2><p>Production crawler controls. Non-production environments remain blocked automatically.</p></div></div>
|
||||
<label class="consent-check"><input type="checkbox" name="seo_robots_enabled" value="1" @checked(old('seo_robots_enabled',$robotsEnabled))><span><strong>Allow production search-engine indexing</strong><small>When disabled, robots.txt blocks crawling and pages emit noindex.</small></span></label>
|
||||
</section>
|
||||
<div class="seo-savebar"><div><strong>Search configuration</strong><span>Changes apply to public metadata immediately.</span></div><button type="submit" class="btn btn-primary">Save SEO settings</button></div>
|
||||
</form>
|
||||
@endsection
|
||||
@@ -1,24 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Support Inbox — SonarDesk'])
|
||||
@section('content')
|
||||
<section class="portal-header"><div><span class="eyebrow">Admin</span><h1>Support inbox</h1><p class="lead compact">Customer, licence, billing and privacy requests.</p></div></section>
|
||||
<form method="get" class="filter-bar">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" name="status" onchange="this.form.submit()">
|
||||
<option value="">All</option>
|
||||
@foreach(['new','open','waiting','resolved','closed'] as $status)<option value="{{ $status }}" @selected(request('status') === $status)>{{ ucfirst($status) }}</option>@endforeach
|
||||
</select>
|
||||
</form>
|
||||
<div class="admin-ticket-list">
|
||||
@forelse($tickets as $ticket)
|
||||
<a class="admin-ticket-row" href="{{ route('admin.support.show', $ticket) }}">
|
||||
<div><span class="ticket-ref">{{ $ticket->reference }}</span><strong>{{ $ticket->subject }}</strong><small>{{ $ticket->name }} · {{ $ticket->email }}</small></div>
|
||||
<span class="ticket-category">{{ ucfirst($ticket->category) }}</span>
|
||||
<span class="ticket-status status-{{ $ticket->status }}">{{ ucfirst($ticket->status) }}</span>
|
||||
<time>{{ $ticket->created_at->diffForHumans() }}</time>
|
||||
</a>
|
||||
@empty
|
||||
<div class="empty-state"><h2>No support requests</h2><p>The support inbox is empty.</p></div>
|
||||
@endforelse
|
||||
</div>
|
||||
{{ $tickets->links() }}
|
||||
@endsection
|
||||
@@ -1,14 +0,0 @@
|
||||
@extends('layouts.app', ['title' => $ticket->reference.' — Support'])
|
||||
@section('content')
|
||||
<a class="back-link" href="{{ route('admin.support.index') }}">← Support inbox</a>
|
||||
<section class="ticket-detail-head"><div><span class="ticket-ref">{{ $ticket->reference }}</span><h1>{{ $ticket->subject }}</h1><p>{{ $ticket->name }} · <a href="mailto:{{ $ticket->email }}?subject=Re:%20{{ rawurlencode($ticket->reference.' '.$ticket->subject) }}">{{ $ticket->email }}</a></p></div><span class="ticket-status status-{{ $ticket->status }}">{{ ucfirst($ticket->status) }}</span></section>
|
||||
<div class="detail-grid">
|
||||
<article class="panel ticket-message"><h2>Request</h2><div class="ticket-meta"><span>{{ ucfirst($ticket->category) }}</span><span>{{ $ticket->created_at->format('j M Y H:i') }}</span></div><div class="message-body">{!! nl2br(e($ticket->message)) !!}</div><a class="btn btn-primary" href="mailto:{{ $ticket->email }}?subject=Re:%20{{ rawurlencode($ticket->reference.' '.$ticket->subject) }}">Reply by email</a></article>
|
||||
<aside class="panel"><h2>Manage</h2><form method="post" action="{{ route('admin.support.update', $ticket) }}">@csrf @method('PATCH')
|
||||
<label>Status<select name="status">@foreach(['new','open','waiting','resolved','closed'] as $status)<option value="{{ $status }}" @selected($ticket->status === $status)>{{ ucfirst($status) }}</option>@endforeach</select></label>
|
||||
<label>Priority<select name="priority">@foreach(['low','normal','high','urgent'] as $priority)<option value="{{ $priority }}" @selected($ticket->priority === $priority)>{{ ucfirst($priority) }}</option>@endforeach</select></label>
|
||||
<label>Assigned to<select name="assigned_to"><option value="">Unassigned</option>@foreach($admins as $admin)<option value="{{ $admin->id }}" @selected($ticket->assigned_to === $admin->id)>{{ $admin->name }} — {{ $admin->email }}</option>@endforeach</select></label>
|
||||
<button class="full-width">Save changes</button>
|
||||
</form></aside>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,31 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header">
|
||||
<div><span class="eyebrow">Admin</span><h1>Users</h1><p class="lead compact">Manage portal accounts, access and customer licences.</p></div>
|
||||
<a class="btn btn-secondary" href="{{ route('admin.index') }}">Admin dashboard</a>
|
||||
</section>
|
||||
|
||||
<form method="get" class="card" style="display:flex;gap:10px;align-items:end;flex-wrap:wrap;margin-bottom:18px">
|
||||
<label style="flex:1;min-width:240px">Search<input name="q" value="{{ request('q') }}" placeholder="Name or email"></label>
|
||||
<label>Status<select name="status"><option value="">All users</option><option value="admin" @selected(request('status')==='admin')>Administrators</option><option value="suspended" @selected(request('status')==='suspended')>Suspended</option><option value="unverified" @selected(request('status')==='unverified')>Unverified</option></select></label>
|
||||
<button>Search</button>
|
||||
@if(request()->hasAny(['q','status']))<a class="btn btn-secondary" href="{{ route('admin.users.index') }}">Clear</a>@endif
|
||||
</form>
|
||||
|
||||
<div class="admin-table-card">
|
||||
<div class="admin-table-title"><div><h2>Portal users</h2><p>{{ $users->total() }} account{{ $users->total() === 1 ? '' : 's' }}</p></div></div>
|
||||
<div class="table-scroll"><table class="pro-table"><thead><tr><th>User</th><th>Access</th><th>Email</th><th>Licences</th><th>Joined</th><th></th></tr></thead><tbody>
|
||||
@forelse($users as $user)
|
||||
<tr>
|
||||
<td><strong>{{ $user->name }}</strong><small>#{{ $user->id }}</small></td>
|
||||
<td>@if($user->disabled_at)<span class="status-chip muted">Suspended</span>@elseif($user->is_admin)<span class="status-chip">Admin</span>@else<span class="status-chip success">Active</span>@endif</td>
|
||||
<td><strong>{{ $user->email }}</strong><small>{{ $user->hasVerifiedEmail() ? 'Verified' : 'Not verified' }}</small></td>
|
||||
<td>{{ $licenseCounts[strtolower($user->email)] ?? 0 }}</td>
|
||||
<td>{{ $user->created_at?->format('d M Y') }}</td>
|
||||
<td><div class="table-actions"><a class="btn btn-secondary" href="{{ route('admin.users.show',$user) }}">Manage</a></div></td>
|
||||
</tr>
|
||||
@empty<tr><td colspan="6" class="empty-cell">No users found.</td></tr>@endforelse
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
<div style="margin-top:18px">{{ $users->links() }}</div>
|
||||
@endsection
|
||||
@@ -1,57 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
@section('content')
|
||||
<section class="portal-header">
|
||||
<div><span class="eyebrow">Admin · User</span><h1>{{ $user->name }}</h1><p class="lead compact">{{ $user->email }}</p></div>
|
||||
<a class="btn btn-secondary" href="{{ route('admin.users.index') }}">Back to users</a>
|
||||
</section>
|
||||
|
||||
@if($errors->any())<div class="card" style="border-color:#fda29b;margin-bottom:18px"><strong>Could not save changes</strong><ul>@foreach($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>@endif
|
||||
|
||||
<div class="release-editor">
|
||||
<div class="editor-main">
|
||||
<div class="editor-card">
|
||||
<div class="editor-card-head"><span>01</span><div><h2>Account details</h2><p>Edit the portal identity and administrator access.</p></div></div>
|
||||
<form method="post" action="{{ route('admin.users.update',$user) }}">@csrf @method('PUT')
|
||||
<div class="form-grid"><label>Name<input name="name" value="{{ old('name',$user->name) }}" required></label><label>Email<input type="email" name="email" value="{{ old('email',$user->email) }}" required></label></div>
|
||||
<label class="toggle-row"><input type="checkbox" name="is_admin" value="1" @checked($user->is_admin) @disabled(auth()->user()->is($user))><span><strong>Administrator</strong><small>Allows access to SonarDesk administration.</small></span></label>
|
||||
@if(auth()->user()->is($user))<input type="hidden" name="is_admin" value="1"><p><small>Your own administrator access is protected.</small></p>@endif
|
||||
<button>Save user</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="editor-card">
|
||||
<div class="editor-card-head"><span>02</span><div><h2>Licences</h2><p>Licences linked to this user's email address.</p></div></div>
|
||||
@forelse($licenses as $license)
|
||||
<div style="padding:18px 0;border-top:1px solid #eef2f6">
|
||||
<div style="display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap">
|
||||
<div><strong>{{ $license->edition }} · •••• {{ $license->license_key_last4 }}</strong><small style="display:block;color:#667085;margin-top:4px">{{ $license->subscription_status ?: 'No subscription status' }} · {{ $license->payment_provider ?: 'manual' }}</small></div>
|
||||
<div><a class="btn btn-secondary" href="{{ route('admin.license',$license) }}">Full licence</a></div>
|
||||
</div>
|
||||
<form method="post" action="{{ route('admin.users.licences.update',[$user,$license]) }}" style="display:flex;gap:10px;align-items:end;margin-top:14px;flex-wrap:wrap">@csrf @method('PATCH')<label>Activation limit<input type="number" min="1" max="1000" name="activation_limit" value="{{ $license->activation_limit }}" style="max-width:120px"></label><button>Update limit</button></form>
|
||||
@if($license->activations->isNotEmpty())
|
||||
<div class="table-scroll" style="margin-top:14px"><table class="pro-table"><thead><tr><th>Device</th><th>Platform</th><th>Version</th><th>Status</th><th></th></tr></thead><tbody>
|
||||
@foreach($license->activations as $activation)<tr><td>{{ $activation->installation_name ?: $activation->installation_id }}</td><td>{{ $activation->platform ?: '—' }}</td><td>{{ $activation->app_version ?: '—' }}</td><td>{{ $activation->deactivated_at ? 'Released' : 'Active' }}</td><td>@if(!$activation->deactivated_at)<form method="post" action="{{ route('admin.users.activations.destroy',[$user,$license,$activation]) }}">@csrf @method('DELETE')<button class="btn btn-secondary">Release</button></form>@endif</td></tr>@endforeach
|
||||
</tbody></table></div>
|
||||
@endif
|
||||
</div>
|
||||
@empty<p>No licences are linked to this email address.</p>@endforelse
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="editor-main">
|
||||
<div class="editor-card sticky-card">
|
||||
<div class="editor-card-head"><span>03</span><div><h2>Account actions</h2><p>Security and access controls.</p></div></div>
|
||||
<p><strong>Email</strong><br><span class="status-chip {{ $user->hasVerifiedEmail() ? 'success' : 'muted' }}">{{ $user->hasVerifiedEmail() ? 'Verified' : 'Not verified' }}</span></p>
|
||||
@if(!$user->hasVerifiedEmail())
|
||||
<form method="post" action="{{ route('admin.users.verification-email',$user) }}" style="margin-bottom:10px">@csrf<button class="btn btn-secondary" style="width:100%">Send verification email</button></form>
|
||||
<form method="post" action="{{ route('admin.users.verify',$user) }}" style="margin-bottom:10px">@csrf<button class="btn btn-secondary" style="width:100%">Mark verified</button></form>
|
||||
@endif
|
||||
<form method="post" action="{{ route('admin.users.password-reset',$user) }}" style="margin-bottom:10px">@csrf<button class="btn btn-secondary" style="width:100%">Send password reset</button></form>
|
||||
@unless(auth()->user()->is($user))
|
||||
@if($user->disabled_at)<form method="post" action="{{ route('admin.users.restore',$user) }}" style="margin-bottom:10px">@csrf<button style="width:100%">Restore account</button></form>@else<form method="post" action="{{ route('admin.users.suspend',$user) }}" style="margin-bottom:10px">@csrf<button class="btn btn-secondary" style="width:100%">Suspend account</button></form>@endif
|
||||
<form method="post" action="{{ route('admin.users.destroy',$user) }}" onsubmit="return confirm('Delete this portal user? Their licences will be retained.')">@csrf @method('DELETE')<button class="danger" style="width:100%">Delete user</button></form>
|
||||
@endunless
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Reset password</h1><form method="post" action="{{ route('password.email') }}">@csrf<label>Email</label><input name="email" type="email" required><button>Send reset link</button></form></div>@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Sign in</h1><form method="post" action="{{ route('login') }}">@csrf<label>Email</label><input name="email" type="email" value="{{ old('email') }}" required autofocus><label>Password</label><input name="password" type="password" required><label><input style="width:auto" type="checkbox" name="remember"> Remember me</label><br><button>Sign in</button> <a href="{{ route('password.request') }}">Forgot password?</a></form></div>@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Create account</h1><p class="muted">Use the same email address used to purchase SonarDesk.</p><form method="post" action="{{ route('register') }}">@csrf<label>Name</label><input name="name" value="{{ old('name') }}" required><label>Email</label><input name="email" type="email" value="{{ old('email') }}" required><label>Password</label><input name="password" type="password" required><label>Confirm password</label><input name="password_confirmation" type="password" required><button>Create account</button></form></div>@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Choose a new password</h1><form method="post" action="{{ route('password.update') }}">@csrf<input type="hidden" name="token" value="{{ $token }}"><label>Email</label><input name="email" type="email" value="{{ $email }}" required><label>New password</label><input name="password" type="password" required><label>Confirm password</label><input name="password_confirmation" type="password" required><button>Reset password</button></form></div>@endsection
|
||||
@@ -1 +0,0 @@
|
||||
@extends('layouts.app') @section('content')<div class="card"><h1>Verify your email</h1><p>For security, licence ownership is only shown after the account email has been verified.</p><form method="post" action="{{ route('verification.send') }}">@csrf<button>Send another verification email</button></form></div>@endsection
|
||||
@@ -1,301 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="hero-card">
|
||||
<span class="eyebrow">SonarDesk</span>
|
||||
<h1>Choose your licence</h1>
|
||||
<p class="lead">
|
||||
Every subscription starts with a 7-day free trial.
|
||||
Cancel before the trial ends and you won't be charged.
|
||||
</p>
|
||||
|
||||
@if($paddleEnvironment === 'sandbox')
|
||||
<div class="sandbox-badge">
|
||||
Sandbox checkout — no real payment is taken
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="pricing-grid">
|
||||
@foreach($plans as $code => $plan)
|
||||
<article class="price-card {{ str_starts_with($code, 'professional') ? 'featured' : '' }}">
|
||||
<div>
|
||||
<div class="plan-heading">
|
||||
<div>
|
||||
<h2>{{ $plan['name'] }}</h2>
|
||||
<div class="muted">{{ $plan['billing'] }} billing</div>
|
||||
</div>
|
||||
|
||||
@if($plan['billing'] === 'Annual')
|
||||
<span class="pill">Best value</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="price"
|
||||
data-price-display="{{ $plan['price_id'] }}"
|
||||
data-billing="{{ strtolower($plan['billing']) }}"
|
||||
>
|
||||
{{ $plan['display_price'] }}
|
||||
</div>
|
||||
|
||||
<ul class="features">
|
||||
<li>7-day free trial</li>
|
||||
<li>
|
||||
{{ $plan['activation_limit'] }}
|
||||
active Mac{{ $plan['activation_limit'] === 1 ? '' : 's' }}
|
||||
</li>
|
||||
<li>Secure online activation</li>
|
||||
<li>Licence management portal</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="checkout-button"
|
||||
type="button"
|
||||
data-price-id="{{ $plan['price_id'] }}"
|
||||
data-plan="{{ $plan['name'] }} {{ $plan['billing'] }}"
|
||||
>
|
||||
Start free trial
|
||||
</button>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="card checkout-status" id="checkout-status" hidden>
|
||||
<h2>Purchase received</h2>
|
||||
|
||||
<p>
|
||||
Paddle is confirming your subscription. Your SonarDesk licence key
|
||||
is generated by our licensing server and emailed to the address used
|
||||
at checkout. You can then activate it in
|
||||
<strong>SonarDesk → Settings → Licence</strong>.
|
||||
</p>
|
||||
|
||||
<p class="muted">
|
||||
Delivery normally follows the subscription webhook within moments.
|
||||
Check your junk folder if needed. You can also create a portal account
|
||||
with the same email address and resend the key from My licences.
|
||||
</p>
|
||||
|
||||
<a class="btn" href="{{ route('register') }}">
|
||||
Create portal account
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card" id="checkout-error" hidden>
|
||||
<h2>Checkout unavailable</h2>
|
||||
|
||||
<p>
|
||||
Paddle could not start the checkout. Please try again.
|
||||
</p>
|
||||
|
||||
<p class="muted" id="checkout-error-detail"></p>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const token = @json($paddleToken);
|
||||
const environment = @json($paddleEnvironment);
|
||||
|
||||
const errorPanel = document.getElementById('checkout-error');
|
||||
const errorDetail = document.getElementById('checkout-error-detail');
|
||||
|
||||
function showCheckoutError(message) {
|
||||
console.error('SONARDESK CHECKOUT ERROR:', message);
|
||||
|
||||
if (errorDetail) {
|
||||
errorDetail.textContent = message || 'Unknown checkout error.';
|
||||
}
|
||||
|
||||
if (errorPanel) {
|
||||
errorPanel.hidden = false;
|
||||
|
||||
errorPanel.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function logPaddleEvent(event) {
|
||||
console.log('PADDLE EVENT:', event);
|
||||
|
||||
if (
|
||||
event?.name === 'checkout.error' ||
|
||||
event?.name === 'checkout.warning' ||
|
||||
event?.name === 'checkout.payment.error'
|
||||
) {
|
||||
console.error('PADDLE ERROR:', {
|
||||
name: event?.name,
|
||||
type: event?.type,
|
||||
code: event?.code,
|
||||
detail: event?.detail,
|
||||
documentation_url: event?.documentation_url,
|
||||
errors: event?.errors,
|
||||
event: event
|
||||
});
|
||||
|
||||
const code = event?.code
|
||||
? `Code: ${event.code}`
|
||||
: 'Paddle checkout error';
|
||||
|
||||
const detail = event?.detail
|
||||
? ` — ${event.detail}`
|
||||
: '';
|
||||
|
||||
showCheckoutError(code + detail);
|
||||
}
|
||||
|
||||
if (event?.name === 'checkout.completed') {
|
||||
const status = document.getElementById('checkout-status');
|
||||
|
||||
if (status) {
|
||||
status.hidden = false;
|
||||
|
||||
status.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof Paddle === 'undefined') {
|
||||
showCheckoutError('Paddle.js failed to load.');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('SONARDESK PADDLE CONFIG:', {
|
||||
environment: environment,
|
||||
tokenPresent: Boolean(token),
|
||||
tokenPrefix: token ? token.substring(0, 5) : null
|
||||
});
|
||||
|
||||
try {
|
||||
if (environment === 'sandbox') {
|
||||
Paddle.Environment.set('sandbox');
|
||||
}
|
||||
|
||||
Paddle.Initialize({
|
||||
token: token,
|
||||
eventCallback: logPaddleEvent
|
||||
});
|
||||
|
||||
console.log('Paddle initialized successfully.');
|
||||
} catch (error) {
|
||||
console.error('PADDLE INITIALIZATION ERROR:', error);
|
||||
|
||||
showCheckoutError(
|
||||
error?.message || 'Paddle could not be initialized.'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const priceElements = new Map(
|
||||
Array.from(
|
||||
document.querySelectorAll('[data-price-display]')
|
||||
).map(element => [
|
||||
element.dataset.priceDisplay,
|
||||
element
|
||||
])
|
||||
);
|
||||
|
||||
const previewItems = Array.from(priceElements.keys()).map(priceId => ({
|
||||
priceId: priceId,
|
||||
quantity: 1
|
||||
}));
|
||||
|
||||
console.log('PADDLE PRICE PREVIEW ITEMS:', previewItems);
|
||||
|
||||
Paddle.PricePreview({
|
||||
items: previewItems
|
||||
})
|
||||
.then(result => {
|
||||
console.log('PADDLE PRICE PREVIEW RESULT:', result);
|
||||
|
||||
for (const item of (result?.data?.details?.lineItems ?? [])) {
|
||||
const element = priceElements.get(item?.price?.id);
|
||||
|
||||
const formatted =
|
||||
item?.formattedTotals?.total ??
|
||||
item?.formattedUnitTotals?.total;
|
||||
|
||||
if (!element || !formatted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const suffix =
|
||||
element.dataset.billing === 'annual'
|
||||
? ' / year'
|
||||
: ' / month';
|
||||
|
||||
element.textContent = formatted + suffix;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('PADDLE PRICE PREVIEW ERROR:', error);
|
||||
|
||||
// Server-rendered GBP price remains visible.
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll('.checkout-button')
|
||||
.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const priceId = button.dataset.priceId;
|
||||
const plan = button.dataset.plan;
|
||||
|
||||
console.log('OPENING PADDLE CHECKOUT:', {
|
||||
environment: environment,
|
||||
priceId: priceId,
|
||||
plan: plan
|
||||
});
|
||||
|
||||
errorPanel.hidden = true;
|
||||
|
||||
try {
|
||||
Paddle.Checkout.open({
|
||||
items: [
|
||||
{
|
||||
priceId: priceId,
|
||||
quantity: 1
|
||||
}
|
||||
],
|
||||
|
||||
settings: {
|
||||
displayMode: 'overlay',
|
||||
theme: window.matchMedia(
|
||||
'(prefers-color-scheme: dark)'
|
||||
).matches
|
||||
? 'dark'
|
||||
: 'light',
|
||||
allowLogout: false
|
||||
},
|
||||
|
||||
customData: {
|
||||
source: 'sonardesk-buy-page',
|
||||
plan: plan
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'PADDLE CHECKOUT OPEN ERROR:',
|
||||
error
|
||||
);
|
||||
|
||||
showCheckoutError(
|
||||
error?.message ||
|
||||
'Paddle could not open the checkout.'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endsection
|
||||
@@ -1,14 +0,0 @@
|
||||
@extends('layouts.app', [
|
||||
'title' => $page->meta_title ?: $page->title.' — '.\App\Models\SiteSetting::valueFor('seo_site_name', 'SonarDesk'),
|
||||
'description' => $page->meta_description ?: $page->excerpt,
|
||||
'keywords' => $page->meta_keywords,
|
||||
'canonical' => $page->canonical_url ?: null,
|
||||
'ogTitle' => $page->og_title ?: null,
|
||||
'ogDescription' => $page->og_description ?: null,
|
||||
'ogImage' => $page->og_image ?: null,
|
||||
'robotsIndex' => $page->robots_index,
|
||||
'robotsFollow' => $page->robots_follow,
|
||||
])
|
||||
@section('content')
|
||||
<article class="legal-page"><header class="legal-head"><span class="eyebrow">SonarDesk</span><h1>{{ $page->title }}</h1>@if($page->excerpt)<p>{{ $page->excerpt }}</p>@endif</header><div class="cms-body">{!! nl2br(e($page->body)) !!}</div></article>
|
||||
@endsection
|
||||
@@ -1,2 +0,0 @@
|
||||
@php echo '<?xml version="1.0" encoding="utf-8"?>'; @endphp
|
||||
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"><channel><title>SonarDesk Updates</title><link>{{ route('updates.appcast') }}</link><description>SonarDesk stable release feed</description>@foreach($releases as $release)<item><title>SonarDesk {{ $release->version }}</title><pubDate>{{ $release->published_at?->toRfc2822String() }}</pubDate><sparkle:version>{{ $release->build_number }}</sparkle:version><sparkle:shortVersionString>{{ $release->version }}</sparkle:shortVersionString>@if($release->minimum_macos)<sparkle:minimumSystemVersion>{{ $release->minimum_macos }}</sparkle:minimumSystemVersion>@endif<description><![CDATA[{!! nl2br(e($release->release_notes ?: 'SonarDesk update')) !!}]]></description><enclosure url="{{ route('download.release',$release) }}" length="{{ $release->file_size }}" type="application/octet-stream"@if($release->sparkle_signature) sparkle:edSignature="{{ $release->sparkle_signature }}"@endif /></item>@endforeach</channel></rss>
|
||||
@@ -1,18 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Downloads — SonarDesk and developer tools', 'description' => 'Browse SonarDesk and other macOS developer utilities. View release details or download installers.'])
|
||||
@section('fullbleed')
|
||||
<main class="download-page catalogue-page">
|
||||
<section class="catalogue-header"><div class="download-shell"><span class="eyebrow">Software downloads</span><h1>Downloads</h1><p>Find the latest releases of SonarDesk and our developer tools. View details, check requirements or download directly.</p></div></section>
|
||||
<section class="distribution-catalogue"><div class="download-shell"><div class="section-heading"><span class="eyebrow">Available software</span><h2>Product catalogue</h2><p>All downloads in one place.</p></div>
|
||||
<div class="distribution-product-grid">
|
||||
@if($latest)
|
||||
<article class="distribution-product-card"><div class="distribution-product-top"><span class="distribution-platform">macOS</span><span class="distribution-version">v{{ $latest->version }}</span></div><h3><a href="{{ route('download.sonardesk.details') }}">SonarDesk</a></h3><p class="distribution-product-description">{{ \Illuminate\Support\Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags(\Illuminate\Support\Str::markdown($latest->release_notes ?: $settings['intro'], ['html_input'=>'strip','allow_unsafe_links'=>false])))), 175) }}</p><div class="distribution-product-footer"><span>{{ $latest->formatted_size }}</span><div class="distribution-product-links"><a class="product-details-link" href="{{ route('download.sonardesk.details') }}">View details →</a><a class="btn btn-primary" href="{{ route('download.latest') }}">Download ↓</a></div></div></article>
|
||||
@endif
|
||||
@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><h3><a href="{{ route('download.product.details', $product) }}">{{ $product->name }}</a></h3><p class="distribution-product-description">{{ \Illuminate\Support\Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags(\Illuminate\Support\Str::markdown($product->description ?: 'Download the latest available package.', ['html_input'=>'strip','allow_unsafe_links'=>false])))), 175) }}</p><div class="distribution-product-footer"><span>{{ $product->formatted_size }}</span><div class="distribution-product-links"><a class="product-details-link" href="{{ route('download.product.details', $product) }}">View details →</a><a class="btn btn-primary" href="{{ route('download.product', $product) }}">Download ↓</a></div></div></article>
|
||||
@endforeach
|
||||
</div>
|
||||
@if(!$latest && $products->isEmpty())<p>No published downloads are available yet.</p>@endif
|
||||
</div></section>
|
||||
@if($releases->isNotEmpty())<section class="release-history"><div class="download-shell"><div class="section-heading"><span class="eyebrow">SonarDesk archive</span><h2>Previous SonarDesk versions</h2><p>Older published installers, grouped separately from current products.</p></div><div class="release-history-list">@foreach($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>@endforeach</div>{{ $releases->links() }}</div></section>@endif
|
||||
</main>
|
||||
@endsection
|
||||
@@ -1,13 +0,0 @@
|
||||
@extends('layouts.app', ['title' => $product->name.' — Downloads', 'description' => 'Download '.$product->name.' and read product information and release notes.'])
|
||||
@section('fullbleed')
|
||||
<main class="download-page"><div class="download-shell">
|
||||
<article class="product-detail-panel">
|
||||
<a href="{{ route('release') }}" style="color:#175cd3;text-decoration:none;font-weight:700">← All downloads</a>
|
||||
<div class="distribution-product-top" style="margin-top:26px"><span class="distribution-platform">{{ $product->platform ?: 'Software' }}</span><span class="distribution-version">{{ $product->version ? 'v'.$product->version : 'Latest' }}</span></div>
|
||||
<h1>{{ $product->name }}</h1>
|
||||
<p style="color:#667085">{{ $product->formatted_size }} · {{ $product->platform ?: 'Software' }}</p>
|
||||
<a class="btn btn-primary" href="{{ route('download.product', $product) }}" style="display:inline-flex;margin-top:18px">Download {{ $product->name }} ↓</a>
|
||||
<div class="distribution-product-description">{!! \Illuminate\Support\Str::markdown($product->description ?: 'Download the latest available package.', ['html_input' => 'strip', 'allow_unsafe_links' => false]) !!}</div>
|
||||
<a class="btn btn-primary" href="{{ route('download.product', $product) }}">Download ↓</a>
|
||||
</article></div></main>
|
||||
@endsection
|
||||
@@ -1,4 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'SonarDesk — Details and download', 'description' => 'SonarDesk release notes, system requirements and macOS installer.'])
|
||||
@section('fullbleed')
|
||||
<main class="download-page"><div class="download-shell"><article class="product-detail-panel"><a class="product-details-link" href="{{ route('release') }}">← All downloads</a><div class="distribution-product-top" style="margin-top:26px"><span class="distribution-platform">macOS</span><span class="distribution-version">v{{ $latest->version }}</span></div><h1>SonarDesk</h1><p>{{ $settings['intro'] }}</p><p class="detail-meta">Version {{ $latest->version }} · Build {{ $latest->build_number }} · {{ $latest->formatted_size }} · Released {{ $latest->published_at?->format('j F Y') }}</p><a class="btn btn-primary" href="{{ route('download.latest') }}">Download SonarDesk ↓</a><section class="detail-section"><h2>Release notes</h2><div class="distribution-product-description">{!! \Illuminate\Support\Str::markdown($latest->release_notes ?: 'Latest maintenance and improvements.', ['html_input'=>'strip','allow_unsafe_links'=>false]) !!}</div></section><section class="detail-section"><h2>System requirements</h2><p>{{ $settings['requirements'] }}</p>@if($latest->minimum_macos)<p><strong>Minimum macOS version:</strong> {{ $latest->minimum_macos }}</p>@endif</section><section class="detail-section"><h2>Installation</h2><ol><li>Download the signed installer.</li><li>Open the DMG and move SonarDesk to Applications.</li><li>Launch SonarDesk and activate with your licence.</li></ol></section>@if($latest->sha256)<details class="checksum-box"><summary>SHA-256 checksum</summary><code>{{ $latest->sha256 }}</code></details>@endif</article></div></main>
|
||||
@endsection
|
||||
@@ -1,15 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Features — SonarDesk', 'description' => 'Explore SonarDesk features for monitoring SonarQube projects, quality gates, issues and measures from macOS.'])
|
||||
@section('content')
|
||||
<section class="hero-card marketing-hero compact-hero"><span class="eyebrow">Features</span><h1>A focused desktop companion for SonarQube.</h1><p class="lead">Built around the checks developers make every day: project status, quality gates, issues, security findings and measures.</p><div class="support-actions marketing-actions"><a class="btn btn-primary" href="{{ route('buy') }}">Start free trial</a><a class="btn btn-secondary" href="{{ route('release') }}">Read the release notes</a></div></section>
|
||||
<div class="feature-detail-grid">
|
||||
<article class="feature-detail"><span>Projects</span><h2>Project dashboard</h2><p>Keep configured SonarQube projects together and scan project health without opening a collection of browser tabs.</p></article>
|
||||
<article class="feature-detail"><span>Quality</span><h2>Quality gates and measures</h2><p>Surface the status and metrics that tell you whether a project needs attention, in a layout designed for quick review.</p></article>
|
||||
<article class="feature-detail"><span>Issues</span><h2>Bugs, vulnerabilities and code smells</h2><p>Browse the major SonarQube issue categories, filter the list and open an issue to inspect its details and context.</p></article>
|
||||
<article class="feature-detail"><span>Scale</span><h2>Server-side pagination</h2><p>Issue browsing is designed to remain practical on projects with larger result sets by paging data from the server.</p></article>
|
||||
<article class="feature-detail"><span>Connections</span><h2>Multiple SonarQube servers</h2><p>Configure the environments you work with and keep them available from the same SonarDesk workspace.</p></article>
|
||||
<article class="feature-detail"><span>Security</span><h2>macOS Keychain</h2><p>SonarQube tokens are stored using macOS Keychain rather than being treated as ordinary application preferences.</p></article>
|
||||
<article class="feature-detail"><span>Licensing</span><h2>Email delivery and self-service portal</h2><p>After checkout, the licence key is emailed to the customer. The SonarDesk portal can resend the key and manage licence information and Mac activations.</p></article>
|
||||
<article class="feature-detail"><span>Distribution</span><h2>Built for direct Mac distribution</h2><p>SonarDesk is distributed directly for macOS, giving the product a focused release and support channel outside the Mac App Store.</p></article>
|
||||
</div>
|
||||
<section class="cta-panel"><div><span class="eyebrow">7-day free trial</span><h2>Try the complete app.</h2><p>Choose Personal for one active Mac or Professional for up to three.</p></div><a class="btn btn-primary" href="{{ route('buy') }}">View pricing</a></section>
|
||||
@endsection
|
||||
@@ -1,16 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Cookie Policy — SonarDesk'])
|
||||
|
||||
@section('content')
|
||||
<article class="legal-page">
|
||||
<header class="legal-head"><span class="eyebrow">Privacy controls</span><h1>Cookie Policy</h1><p>Last updated 9 September 2026</p></header>
|
||||
<div class="legal-intro">SonarDesk uses a small number of cookies or equivalent browser storage items. Optional categories are off by default until you consent.</div>
|
||||
|
||||
<section><h2>Necessary</h2><p>These are required for security and core site features and cannot be disabled through Cookie Settings.</p><div class="cookie-table"><div><strong>Session cookie</strong><span>Keeps you signed in and maintains the secure server session. Session or configured server lifetime.</span></div><div><strong>XSRF / CSRF protection</strong><span>Helps protect forms and authenticated actions against cross-site request forgery. Session or configured framework lifetime.</span></div><div><strong>Cookie consent preference</strong><span>Stores the categories you selected so we do not ask on every page. Up to 12 months.</span></div></div></section>
|
||||
|
||||
<section><h2>Preferences</h2><p>Preference storage is optional and disabled unless you enable it. It may be used for non-essential interface choices in future versions of the site.</p></section>
|
||||
<section><h2>Analytics</h2><p>Analytics is currently disabled by default. If analytics tooling is added, it must only load after you enable Analytics in Cookie Settings and this policy must be updated to identify the provider and retention period.</p></section>
|
||||
<section><h2>Marketing</h2><p>Marketing storage is currently disabled by default. Marketing or advertising technology must not load before you expressly enable this category.</p></section>
|
||||
<section><h2>Paddle checkout</h2><p>When you choose to start checkout, Paddle's checkout is loaded to provide payment and subscription functionality. Paddle may use storage that is necessary to provide checkout, prevent fraud and comply with payment obligations. Paddle provides its own privacy and cookie information for its service.</p></section>
|
||||
<section><h2>Change your choice</h2><p>You can revisit your choice at any time.</p><button type="button" class="btn btn-secondary" data-cookie-settings>Open Cookie Settings</button></section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -1,39 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Privacy Policy — SonarDesk'])
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$legal = config('sonardesk.legal');
|
||||
$retention = config('sonardesk.privacy.support_retention_days', 730);
|
||||
@endphp
|
||||
<article class="legal-page">
|
||||
<header class="legal-head">
|
||||
<span class="eyebrow">Privacy</span>
|
||||
<h1>Privacy Policy</h1>
|
||||
<p>Last updated 9 September 2026 · Policy version {{ config('sonardesk.privacy.policy_version') }}</p>
|
||||
</header>
|
||||
|
||||
<div class="legal-intro">{{ $legal['controller_name'] }} is the data controller for personal data processed through the SonarDesk website, licence portal and support service. We process only the information needed to sell, license, secure and support SonarDesk.</div>
|
||||
|
||||
<section><h2>Information we process</h2><p>We may process your name, email address, account credentials in hashed form, licence and activation identifiers, subscription and billing status, support requests, and technical server logs required to operate and secure the service. SonarDesk does not send the files you sync, SSH passwords or private SSH keys to the licensing service.</p></section>
|
||||
|
||||
<section><h2>Payments</h2><p>Payments and subscription checkout are handled by Paddle. Payment-card details are entered into Paddle's checkout and are not stored by SonarDesk. We receive identifiers and subscription information needed to provision and manage your licence. Paddle acts independently for parts of its payment and tax processing and provides its own privacy information.</p></section>
|
||||
|
||||
<section><h2>Why we use your information</h2><div class="legal-table"><div><strong>Contract</strong><span>Creating your account, providing licences, validating activations, managing subscriptions and delivering support you request.</span></div><div><strong>Legitimate interests</strong><span>Securing the service, preventing abuse, troubleshooting faults and maintaining reliable operational records where those interests do not override your rights.</span></div><div><strong>Legal obligations</strong><span>Records we must keep for accounting, tax, fraud prevention or other applicable legal requirements.</span></div><div><strong>Consent</strong><span>Any optional analytics or marketing storage, if introduced, is disabled until you choose to enable it through Cookie Settings.</span></div></div></section>
|
||||
|
||||
<section><h2>Support requests</h2><p>Support requests are stored in our database and emailed to the configured support team. We retain resolved support tickets for up to {{ $retention }} days, unless a longer period is necessary for an unresolved dispute, security matter or legal obligation. A scheduled retention process removes eligible resolved tickets.</p></section>
|
||||
|
||||
<section><h2>Licence and account records</h2><p>Licence, activation and subscription records are kept while your subscription or licence relationship is active and for a reasonable period afterwards where required for licence recovery, fraud prevention, accounting, dispute handling and legal obligations. We minimise retained data and do not use licence validation to inspect your synced files.</p></section>
|
||||
|
||||
<section><h2>Who receives data</h2><p>Information may be processed by service providers that are necessary to run SonarDesk, such as Paddle for checkout and subscription processing, our hosting/infrastructure providers, and email delivery infrastructure. We limit disclosure to what is necessary for the relevant service and require appropriate safeguards where applicable.</p></section>
|
||||
|
||||
<section><h2>International transfers</h2><p>Some service providers may process information outside the UK or EEA. Where data-protection law requires it, appropriate transfer mechanisms and safeguards must be used by the relevant controller or processor.</p></section>
|
||||
|
||||
<section><h2>Cookies and local storage</h2><p>Necessary cookies are used for security, authentication, sessions and your privacy preferences. Optional analytics and marketing categories default to off and must not be loaded before consent. See our <a href="{{ route('cookies') }}">Cookie Policy</a> or use <button type="button" class="link-button" data-cookie-settings>Cookie Settings</button>.</p></section>
|
||||
|
||||
<section><h2>Your rights</h2><p>Depending on the circumstances, you may have rights to access, correct, erase, restrict or object to processing of your personal data, request portability, and withdraw consent at any time where processing relies on consent. You also have the right to complain to the UK Information Commissioner's Office or your applicable supervisory authority.</p><p>Use the <a href="{{ route('support') }}">support form</a> and choose “Privacy / data request”. We may need to verify your identity before acting on a request.</p></section>
|
||||
|
||||
<section><h2>Security</h2><p>We use access controls, TLS, password hashing, CSRF protection, rate limiting and restricted administrative access. No internet service can guarantee absolute security, so we also minimise the personal data we collect.</p></section>
|
||||
|
||||
<section><h2>Contact</h2><p>Controller: <strong>{{ $legal['controller_name'] }}</strong><br>Country: {{ $legal['country'] }}<br>Privacy contact: <a href="mailto:{{ $legal['privacy_email'] }}">{{ $legal['privacy_email'] }}</a></p></section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -1,102 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Refund Policy — SonarDesk'])
|
||||
|
||||
@section('content')
|
||||
@php($legal = config('sonardesk.legal'))
|
||||
<article class="legal-page">
|
||||
<header class="legal-head">
|
||||
<span class="eyebrow">Legal</span>
|
||||
<h1>Refund Policy</h1>
|
||||
<p>Last updated 1 October 2026</p>
|
||||
</header>
|
||||
|
||||
<div class="legal-intro">
|
||||
This policy explains cancellation, refund and statutory consumer rights for SonarDesk subscriptions.
|
||||
SonarDesk payments are processed by Paddle, our Merchant of Record. Nothing in this policy limits any
|
||||
rights or remedies that cannot lawfully be limited.
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>1. Seven-day free trial</h2>
|
||||
<p>Where shown at checkout, SonarDesk includes a seven-day free trial. You can cancel before the trial
|
||||
ends to prevent the subscription converting to a paid subscription. If you cancel during the trial
|
||||
before a charge is made, there is no subscription payment to refund.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Cancelling a paid subscription</h2>
|
||||
<p>You may cancel your subscription through the SonarDesk licence portal or the subscription-management
|
||||
facilities provided through Paddle. Cancellation stops future automatic renewals. Unless stated
|
||||
otherwise during cancellation, your licence normally remains available until the end of the billing
|
||||
period for which you have already paid.</p>
|
||||
<p>Cancellation does not itself create a refund for an earlier payment.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Refund requests</h2>
|
||||
<p>If you believe a payment should be refunded, contact
|
||||
<a href="{{ route('support') }}">SonarDesk Support</a> as soon as possible. Please provide the email
|
||||
address used for the purchase and enough information for us to identify the transaction. Do not send
|
||||
full payment-card details.</p>
|
||||
<p>Because Paddle is the Merchant of Record, a refund may be processed by Paddle and may also be
|
||||
requested through Paddle's buyer-support facilities. Approved refunds are returned to the original
|
||||
payment method in accordance with Paddle's processing arrangements and the payment provider's timing.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. UK and EEA consumer withdrawal rights</h2>
|
||||
<p>If you purchase as a consumer, applicable law may provide a cooling-off or withdrawal period for
|
||||
distance contracts. For UK consumers this is generally 14 days, subject to the statutory rules and
|
||||
exceptions that apply to digital content and services.</p>
|
||||
<p>If you expressly request or consent to digital supply or performance beginning during a statutory
|
||||
withdrawal period, the law may affect the right to withdraw once supply or performance has begun.
|
||||
Nothing on this page removes a statutory right that applies to your purchase.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Faulty or misdescribed digital content</h2>
|
||||
<p>This refund policy does not replace your statutory remedies. If SonarDesk is faulty, materially not
|
||||
as described, or otherwise fails to meet rights that apply to digital content or services under
|
||||
consumer law, you may be entitled to a repair, replacement, price reduction, refund or another remedy
|
||||
depending on the circumstances.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Duplicate or incorrect charges</h2>
|
||||
<p>If you believe you have been charged twice, charged after a valid cancellation, charged an incorrect
|
||||
amount, or charged for the wrong plan, contact us promptly. We will investigate the subscription and
|
||||
transaction records and, where appropriate, arrange correction or refund through Paddle.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Renewal payments</h2>
|
||||
<p>Subscriptions renew automatically until cancelled. A renewal charge is not automatically refundable
|
||||
solely because the subscription was not cancelled before renewal. We will nevertheless consider refund
|
||||
requests fairly, taking account of the circumstances, applicable law, the timing of the request and
|
||||
whether the renewed service has been used.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. Chargebacks</h2>
|
||||
<p>Please contact SonarDesk Support or Paddle about a disputed payment before initiating a chargeback
|
||||
where practical. This gives us an opportunity to investigate duplicate, fraudulent or incorrect charges
|
||||
promptly. This does not restrict any right you have to dispute a payment with your payment provider.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. How refunds affect your licence</h2>
|
||||
<p>A full refund or reversal of a subscription payment may cause the associated subscription and
|
||||
SonarDesk licence to be cancelled or revoked. Where only part of a payment is refunded, any effect on
|
||||
the subscription will depend on the reason for the adjustment and the resulting Paddle subscription
|
||||
status.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Contact</h2>
|
||||
<p>For refund or billing enquiries, contact
|
||||
<a href="mailto:{{ $legal['support_email'] }}">{{ $legal['support_email'] }}</a> or use
|
||||
<a href="{{ route('support') }}">SonarDesk Support</a>. Include your purchase email address and relevant
|
||||
transaction details, but never send complete card details.</p>
|
||||
<p>See also our <a href="{{ route('terms') }}">Terms of Service</a>.</p>
|
||||
</section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -1,146 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'Terms of Service — SonarDesk'])
|
||||
|
||||
@section('content')
|
||||
@php($legal = config('sonardesk.legal'))
|
||||
<article class="legal-page">
|
||||
<header class="legal-head">
|
||||
<span class="eyebrow">Legal</span>
|
||||
<h1>Terms of Service</h1>
|
||||
<p>Last updated 1 October 2026</p>
|
||||
</header>
|
||||
|
||||
<div class="legal-intro">
|
||||
These Terms of Service govern your use of the SonarDesk website, licence portal,
|
||||
subscription service and macOS application. By purchasing, activating or using SonarDesk,
|
||||
you agree to these terms. Your statutory rights are not affected.
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2>1. SonarDesk licences</h2>
|
||||
<p>SonarDesk is licensed, not sold. A Personal subscription permits the number of active Mac
|
||||
installations stated for that plan at the time of purchase. A Professional subscription permits
|
||||
the number stated for the Professional plan. Licences are for the subscriber's use and must not
|
||||
be shared, resold, sublicensed or used to circumvent activation limits.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Free trials</h2>
|
||||
<p>Where a free trial is offered, its duration and the price that will apply afterwards are shown
|
||||
before checkout. Unless you cancel before the trial ends, the subscription will automatically
|
||||
convert to the selected paid plan and Paddle will charge the payment method provided at checkout.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Subscriptions, billing and renewal</h2>
|
||||
<p>SonarDesk subscriptions renew automatically at the billing interval selected at checkout until
|
||||
cancelled. The renewal price, billing period, applicable taxes and other checkout information are
|
||||
presented before you complete the transaction. Payment processing, tax collection and subscription
|
||||
billing are handled by Paddle, our Merchant of Record.</p>
|
||||
<p>Changes between Personal and Professional plans may result in a prorated charge or credit as
|
||||
calculated by Paddle and displayed as part of the subscription change.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Cancellation</h2>
|
||||
<p>You may cancel a subscription through the SonarDesk licence portal or the subscription-management
|
||||
facilities made available through Paddle. Cancellation prevents future renewals. Unless the
|
||||
cancellation flow expressly states otherwise, access normally continues until the end of the
|
||||
already-paid billing period.</p>
|
||||
<p>Cancellation and refund requests are separate. Cancelling a subscription does not automatically
|
||||
refund charges already paid. See our <a href="{{ route('refund') }}">Refund Policy</a>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Activation and licence checks</h2>
|
||||
<p>SonarDesk uses a licence service to activate permitted Macs and periodically confirm that an
|
||||
activation remains valid. A reasonable offline grace period may be provided. Continued use after a
|
||||
licence or subscription has expired or been revoked is not authorised.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Your SonarQube connections</h2>
|
||||
<p>You are responsible for the SonarQube servers, credentials, tokens and projects you connect to
|
||||
SonarDesk. SonarDesk accesses information made available by the permissions of the credentials you
|
||||
provide. It does not grant additional permissions on your SonarQube server.</p>
|
||||
<p>You are responsible for ensuring that you are authorised to access and process the projects and
|
||||
information you connect to SonarDesk.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>7. Acceptable use</h2>
|
||||
<p>You must not attempt to defeat or circumvent licensing controls, interfere with the SonarDesk
|
||||
service, gain unauthorised access to accounts or systems, distribute malware through the service,
|
||||
reverse engineer the service except where applicable law expressly permits it, or use SonarDesk in
|
||||
violation of applicable law or third-party rights.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>8. Software updates and service availability</h2>
|
||||
<p>We may update SonarDesk, the website and licensing service to improve security, compatibility,
|
||||
reliability or functionality. We may also discontinue obsolete versions where reasonably necessary.
|
||||
We aim to provide a reliable service but do not guarantee uninterrupted or error-free availability.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>9. Third-party services</h2>
|
||||
<p>SonarDesk works with third-party services including SonarQube and Paddle. Those services are
|
||||
operated independently and may have their own terms and policies. We are not responsible for changes,
|
||||
outages or restrictions imposed by third-party services outside our reasonable control.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>10. Consumer rights</h2>
|
||||
<p>Nothing in these terms excludes or restricts rights that cannot lawfully be excluded or restricted.
|
||||
If you are a consumer, this includes applicable rights relating to digital content and services under
|
||||
UK consumer law. Where mandatory law gives you stronger protection than these terms, the mandatory
|
||||
protection applies.</p>
|
||||
<p>Information about refunds, statutory remedies and withdrawal rights is set out in our
|
||||
<a href="{{ route('refund') }}">Refund Policy</a>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>11. Liability</h2>
|
||||
<p>Nothing in these terms excludes or limits liability where doing so would be unlawful, including
|
||||
liability for death or personal injury caused by negligence, fraud or fraudulent misrepresentation.</p>
|
||||
<p>Subject to those protections, we are not responsible for indirect or consequential losses, loss
|
||||
caused by unauthorised or incorrect configuration, unavailable third-party systems, or events outside
|
||||
our reasonable control. If you use SonarDesk for business purposes, you are responsible for maintaining
|
||||
appropriate backups and operational safeguards.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>12. Suspension and termination</h2>
|
||||
<p>We may suspend or terminate access where reasonably necessary to address fraud, security threats,
|
||||
material breach of these terms, chargebacks, or an invalid or expired subscription. Where appropriate,
|
||||
we will give reasonable notice and an opportunity to resolve the issue.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>13. Changes to these terms</h2>
|
||||
<p>We may update these terms when the service, law or our commercial arrangements change. The current
|
||||
version will be published on this page with its revision date. Material changes affecting an existing
|
||||
subscription will be communicated where required by law.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>14. Privacy</h2>
|
||||
<p>Our processing of personal data is described in the
|
||||
<a href="{{ route('privacy') }}">Privacy Policy</a> and
|
||||
<a href="{{ route('cookies') }}">Cookie Policy</a>.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>15. Governing law</h2>
|
||||
<p>These terms are governed by the laws applicable in {{ $legal['country'] }}, subject to any mandatory
|
||||
consumer protections and rights you have under the law of the country in which you live.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>16. Contact</h2>
|
||||
<p>These terms are provided by <strong>{{ $legal['controller_name'] }}</strong> in
|
||||
{{ $legal['country'] }}. For licensing, billing or contractual questions, contact
|
||||
<a href="mailto:{{ $legal['support_email'] }}">{{ $legal['support_email'] }}</a> or use
|
||||
<a href="{{ route('support') }}">SonarDesk Support</a>.</p>
|
||||
</section>
|
||||
</article>
|
||||
@endsection
|
||||
@@ -1,114 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="portal-download-banner"><div><span class="eyebrow">Latest SonarDesk</span><strong>Need the Mac app?</strong><small>Download the latest published, signed release from the SonarDesk distribution centre.</small></div><a class="btn btn-primary" href="{{ route('release') }}">Download SonarDesk</a></div>
|
||||
<section class="portal-header">
|
||||
<div>
|
||||
<span class="eyebrow">Account</span>
|
||||
<h1>My licences</h1>
|
||||
<p class="lead compact">Manage subscriptions, licence keys and activated Macs.</p>
|
||||
</div>
|
||||
<a class="btn btn-secondary" href="{{ route('buy') }}">Buy another licence</a>
|
||||
</section>
|
||||
|
||||
@php
|
||||
$activeLicences = $licenses->filter(fn ($license) => $license->is_usable);
|
||||
$historyLicences = $licenses->reject(fn ($license) => $license->is_usable);
|
||||
@endphp
|
||||
|
||||
@if($activeLicences->isEmpty())
|
||||
<div class="empty-state">
|
||||
<h2>No active licences</h2>
|
||||
<p>No active SonarDesk licence is associated with {{ auth()->user()->email }}.</p>
|
||||
<a class="btn" href="{{ route('buy') }}">Choose a plan</a>
|
||||
</div>
|
||||
@else
|
||||
<div class="licence-list">
|
||||
@foreach($activeLicences as $license)
|
||||
@php
|
||||
$activeCount = $license->activations->whereNull('deactivated_at')->count();
|
||||
$isDuplicatePersonal = $hasProfessional && $license->edition === 'Personal';
|
||||
$billing = str_contains(strtolower((string) optional($license->product)->code), 'annual') ? 'Annual' : 'Monthly';
|
||||
@endphp
|
||||
<article class="licence-card {{ $license->edition === 'Professional' ? 'licence-card-featured' : '' }}">
|
||||
<div class="licence-main">
|
||||
<div class="licence-title-row">
|
||||
<div>
|
||||
<div class="badge-row">
|
||||
<span class="status-badge status-active">Active</span>
|
||||
<span class="plan-badge">{{ $license->edition }}</span>
|
||||
<span class="billing-badge">{{ $billing }}</span>
|
||||
</div>
|
||||
<h2>SonarDesk {{ $license->edition }}</h2>
|
||||
<p class="muted licence-key">Licence ending <strong>{{ $license->license_key_last4 }}</strong></p>
|
||||
</div>
|
||||
<a class="btn btn-secondary" href="{{ route('portal.show', $license) }}">Manage licence</a>
|
||||
</div>
|
||||
|
||||
<div class="licence-stats">
|
||||
<div>
|
||||
<span class="stat-label">Macs activated</span>
|
||||
<strong>{{ $activeCount }} of {{ $license->activation_limit }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="stat-label">Subscription</span>
|
||||
<strong>{{ ucfirst($license->subscription_status ?: 'active') }}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="stat-label">{{ $license->subscription_status === 'trialing' ? 'Trial ends' : 'Renews' }}</span>
|
||||
<strong>{{ optional($license->current_period_ends_at)->format('j M Y') ?: '—' }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activation-meter" aria-label="{{ $activeCount }} of {{ $license->activation_limit }} Macs activated">
|
||||
<span style="width: {{ min(100, ($activeCount / max(1, $license->activation_limit)) * 100) }}%"></span>
|
||||
</div>
|
||||
|
||||
@if($isDuplicatePersonal)
|
||||
<div class="subscription-warning">
|
||||
<div>
|
||||
<strong>You already have an active Professional subscription.</strong>
|
||||
<p>This Personal plan is a separate Paddle subscription and could be billed separately. Cancel it if it was only created while upgrading.</p>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.cancel', $license) }}" onsubmit="return confirm('Cancel this Personal subscription immediately in Paddle? This cannot be undone.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger">Cancel Personal</button>
|
||||
</form>
|
||||
</div>
|
||||
@elseif($license->edition === 'Personal')
|
||||
<div class="upgrade-strip">
|
||||
<div>
|
||||
<strong>Need more Macs?</strong>
|
||||
<span>Upgrade this subscription to Professional for up to 3 Macs.</span>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.upgrade', $license) }}" onsubmit="return confirm('Upgrade this existing Paddle subscription to Professional now? Paddle will prorate the change.');">
|
||||
@csrf
|
||||
<button>Upgrade to Professional</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($historyLicences->isNotEmpty())
|
||||
<details class="history-panel">
|
||||
<summary>Licence history <span>{{ $historyLicences->count() }}</span></summary>
|
||||
<div class="history-list">
|
||||
@foreach($historyLicences as $license)
|
||||
<div class="history-row">
|
||||
<div>
|
||||
<strong>SonarDesk {{ $license->edition }}</strong>
|
||||
<span>Licence ending {{ $license->license_key_last4 }}</span>
|
||||
</div>
|
||||
<div class="history-status">{{ ucfirst($license->subscription_status ?: ($license->revoked_at ? 'revoked' : 'unavailable')) }}</div>
|
||||
<a href="{{ route('portal.show', $license) }}">Details</a>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</details>
|
||||
@endif
|
||||
@endsection
|
||||
@@ -1,81 +0,0 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
<div class="portal-download-banner"><div><span class="eyebrow">Latest SonarDesk</span><strong>Need the Mac app?</strong><small>Download the latest published, signed release from the SonarDesk distribution centre.</small></div><a class="btn btn-primary" href="{{ route('release') }}">Download SonarDesk</a></div>
|
||||
@php
|
||||
$activeActivations = $license->activations->whereNull('deactivated_at');
|
||||
$billing = str_contains(strtolower((string) optional($license->product)->code), 'annual') ? 'Annual' : 'Monthly';
|
||||
@endphp
|
||||
|
||||
<a class="back-link" href="{{ route('portal.index') }}">← My licences</a>
|
||||
|
||||
<section class="licence-detail-head">
|
||||
<div>
|
||||
<div class="badge-row">
|
||||
<span class="status-badge {{ $license->is_usable ? 'status-active' : 'status-inactive' }}">{{ $license->is_usable ? 'Active' : 'Unavailable' }}</span>
|
||||
<span class="plan-badge">{{ $license->edition }}</span>
|
||||
<span class="billing-badge">{{ $billing }}</span>
|
||||
</div>
|
||||
<h1>SonarDesk {{ $license->edition }}</h1>
|
||||
<p class="muted">Licence ending <strong>{{ $license->license_key_last4 }}</strong></p>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.resend', $license) }}">
|
||||
@csrf
|
||||
<button class="btn-secondary">Email licence key</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="detail-grid">
|
||||
<section class="panel">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
<h2>Activated Macs</h2>
|
||||
<p>{{ $activeActivations->count() }} of {{ $license->activation_limit }} activation slots in use.</p>
|
||||
</div>
|
||||
<span class="activation-count">{{ $activeActivations->count() }}/{{ $license->activation_limit }}</span>
|
||||
</div>
|
||||
|
||||
@forelse($activeActivations as $activation)
|
||||
<div class="device-row">
|
||||
<div class="device-icon">⌘</div>
|
||||
<div class="device-copy">
|
||||
<strong>{{ $activation->installation_name ?: 'Mac' }}</strong>
|
||||
<span>{{ $activation->platform ?: 'macOS' }}@if($activation->app_version) · SonarDesk {{ $activation->app_version }}@endif</span>
|
||||
<small>Last seen {{ optional($activation->last_seen_at)->diffForHumans() ?: 'not yet recorded' }}</small>
|
||||
</div>
|
||||
<form method="post" action="{{ route('portal.deactivate', [$license, $activation->id]) }}" onsubmit="return confirm('Deactivate this Mac? SonarDesk on that Mac will lose access when it next validates.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger-outline">Deactivate</button>
|
||||
</form>
|
||||
</div>
|
||||
@empty
|
||||
<div class="empty-inline">No Macs are currently activated with this licence.</div>
|
||||
@endforelse
|
||||
</section>
|
||||
|
||||
<aside class="panel subscription-panel">
|
||||
<h2>Subscription</h2>
|
||||
<dl class="subscription-facts">
|
||||
<div><dt>Plan</dt><dd>{{ $license->edition }} {{ $billing }}</dd></div>
|
||||
<div><dt>Status</dt><dd>{{ ucfirst($license->subscription_status ?: 'active') }}</dd></div>
|
||||
<div><dt>{{ $license->subscription_status === 'trialing' ? 'Trial ends' : 'Current period ends' }}</dt><dd>{{ optional($license->current_period_ends_at)->format('j M Y') ?: '—' }}</dd></div>
|
||||
</dl>
|
||||
|
||||
@if($license->edition === 'Personal' && $license->is_usable)
|
||||
<form method="post" action="{{ route('portal.upgrade', $license) }}" onsubmit="return confirm('Upgrade this existing Paddle subscription to Professional now? Paddle will prorate the change.');">
|
||||
@csrf
|
||||
<button class="full-width">Upgrade to Professional</button>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if($license->payment_provider === 'paddle' && $license->external_subscription_id && $license->subscription_status !== 'canceled')
|
||||
<form method="post" action="{{ route('portal.cancel', $license) }}" onsubmit="return confirm('Cancel this Paddle subscription immediately? This cannot be undone.');">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button class="danger-outline full-width">Cancel subscription</button>
|
||||
</form>
|
||||
@endif
|
||||
</aside>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -1,16 +0,0 @@
|
||||
@extends('layouts.app', ['title' => 'SonarDesk 1.0 — Release', 'description' => 'SonarDesk 1.0 for macOS: a native desktop companion for SonarQube project health, quality gates and issues.'])
|
||||
@section('content')
|
||||
<section class="hero-card marketing-hero compact-hero"><span class="eyebrow">Introducing SonarDesk 1.0</span><h1>SonarQube monitoring, built for the Mac.</h1><p class="lead">SonarDesk brings project health, quality gates, bugs, vulnerabilities, code smells and measures into a dedicated native macOS workspace.</p><div class="support-actions marketing-actions"><a class="btn btn-primary" href="{{ route('buy') }}">Start free trial</a><a class="btn btn-secondary" href="{{ route('features') }}">Explore features</a></div></section>
|
||||
<section class="release-body">
|
||||
<div class="release-main">
|
||||
<span class="eyebrow">Version 1.0</span><h2>Why SonarDesk</h2>
|
||||
<p>SonarQube is an essential part of many development workflows, but checking it throughout the day often means returning to another browser tab, navigating back to a project and rebuilding context. SonarDesk is designed to make that recurring check faster and more focused on macOS.</p>
|
||||
<p>It does not replace SonarQube. Your SonarQube server remains the source of truth. SonarDesk gives you a dedicated desktop view of the information you monitor most often.</p>
|
||||
<h2>What is included</h2>
|
||||
<ul class="release-list"><li>Project dashboards and project health views</li><li>Quality gate status and important measures</li><li>Issue browsing for bugs, vulnerabilities and code smells</li><li>Detailed issue views and practical filtering</li><li>Server-side pagination for larger issue sets</li><li>Support for multiple SonarQube server configurations</li><li>SonarQube token storage using macOS Keychain</li><li>Direct licensing with a self-service customer portal</li></ul>
|
||||
<h2>Who it is for</h2><p>SonarDesk is for developers, technical leads and engineering teams who already use SonarQube and want a faster, dedicated way to keep project quality visible while working on a Mac.</p>
|
||||
<h2>Launch pricing</h2><p>Personal is £24.99/year or £2.99/month for one active Mac. Professional is £44.99/year or £4.99/month for up to three active Macs. Both include a 7-day free trial and the complete SonarDesk feature set.</p>
|
||||
</div>
|
||||
<aside class="release-aside"><div class="card"><span class="eyebrow">At a glance</span><h3>SonarDesk 1.0</h3><p>Native macOS application</p><p>Direct distribution</p><p>7-day free trial</p><p>Personal + Professional plans</p><a class="btn btn-primary full-width" href="{{ route('buy') }}">Try SonarDesk</a></div></aside>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -1,100 +0,0 @@
|
||||
@extends('layouts.app', [
|
||||
'title' => 'Support — SonarDesk',
|
||||
'description' => 'Contact SonarDesk support for technical, licence, billing and privacy enquiries.'
|
||||
])
|
||||
|
||||
@section('content')
|
||||
<section class="support-hero">
|
||||
<span class="eyebrow">Support</span>
|
||||
<h1>How can we help?</h1>
|
||||
<p class="lead">Send us the details below. Your request is saved securely, assigned a reference and emailed to the support team.</p>
|
||||
</section>
|
||||
|
||||
<div class="support-layout">
|
||||
<section class="support-form-card">
|
||||
<div class="panel-heading-simple">
|
||||
<div>
|
||||
<h2>Contact support</h2>
|
||||
<p>We will reply to the email address you provide.</p>
|
||||
</div>
|
||||
<span class="response-badge">Support ticket</span>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ route('support.store') }}" class="support-form">
|
||||
@csrf
|
||||
<div class="honeypot" aria-hidden="true">
|
||||
<label for="company_website">Company website</label>
|
||||
<input id="company_website" name="company_website" type="text" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-grid two">
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="name" value="{{ old('name', auth()->user()?->name) }}" autocomplete="name" required maxlength="120">
|
||||
@error('name')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email</label>
|
||||
<input id="email" type="email" name="email" value="{{ old('email', auth()->user()?->email) }}" autocomplete="email" required maxlength="254">
|
||||
@error('email')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid two">
|
||||
<div>
|
||||
<label for="category">Category</label>
|
||||
<select id="category" name="category" required>
|
||||
@foreach($categories as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('category') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('category')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="subject">Subject</label>
|
||||
<input id="subject" name="subject" value="{{ old('subject') }}" required maxlength="180" placeholder="Briefly describe the issue">
|
||||
@error('subject')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message">Message</label>
|
||||
<textarea id="message" name="message" rows="8" required maxlength="10000" placeholder="Tell us what happened, what you expected, your macOS version and SonarDesk version where relevant.">{{ old('message') }}</textarea>
|
||||
<div class="form-help">Do not include passwords, private SSH keys or your full licence token.</div>
|
||||
@error('message')<div class="field-error">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
|
||||
<label class="consent-check">
|
||||
<input type="checkbox" name="privacy_notice" value="1" @checked(old('privacy_notice')) required>
|
||||
<span>I have read the <a href="{{ route('privacy') }}" target="_blank" rel="noopener">Privacy Policy</a> and understand that my details will be used to respond to this request.</span>
|
||||
</label>
|
||||
@error('privacy_notice')<div class="field-error">{{ $message }}</div>@enderror
|
||||
|
||||
<div class="support-actions">
|
||||
<button type="submit" class="btn btn-primary">Send support request</button>
|
||||
@auth
|
||||
@if(auth()->user()->hasVerifiedEmail())<a class="btn btn-secondary" href="{{ route('portal.index') }}">Open licence portal</a>@endif
|
||||
@endauth
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<aside class="support-side">
|
||||
<article class="support-info-card blue">
|
||||
<div class="support-info-icon">◎</div>
|
||||
<h3>Licence & activation</h3>
|
||||
<p>Include the last four characters of your licence key and the exact activation message.</p>
|
||||
</article>
|
||||
<article class="support-info-card green">
|
||||
<div class="support-info-icon">✓</div>
|
||||
<h3>Useful diagnostics</h3>
|
||||
<p>Include the SonarDesk version, macOS version and the relevant History error output.</p>
|
||||
</article>
|
||||
<article class="support-info-card violet">
|
||||
<div class="support-info-icon">⌁</div>
|
||||
<h3>Privacy requests</h3>
|
||||
<p>Choose “Privacy / data request” for access, correction, deletion, restriction or objection requests.</p>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
@endsection
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user