wite
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
@import 'tailwindcss';
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
@theme { --font-sans: ui-sans-serif, system-ui, sans-serif; }
|
||||
@layer base { body { @apply bg-slate-950 text-slate-200 antialiased; } h1 { @apply text-4xl md:text-6xl font-semibold tracking-tight text-white; } h2 { @apply text-2xl font-semibold text-white mb-4; } h3 { @apply text-lg font-semibold text-white; } input:not([type=checkbox]), textarea, select { @apply w-full rounded-xl border border-slate-700 bg-slate-900 p-3 text-white; } label { @apply block text-sm text-slate-300 mb-2; } button, a { @apply focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-cyan-300; } }
|
||||
@layer components { .button { @apply inline-flex items-center justify-center rounded-xl bg-cyan-300 px-5 py-3 font-semibold text-slate-950 hover:bg-cyan-200 transition cursor-pointer; } .secondary { @apply inline-flex items-center justify-center rounded-xl border border-slate-700 px-5 py-3 text-slate-100 hover:bg-slate-800 cursor-pointer; } .card { @apply rounded-2xl border border-slate-800 bg-slate-900/70 p-6; } .muted { @apply text-slate-400; } .eyebrow { @apply text-xs uppercase tracking-[.25em] text-cyan-300 font-semibold; } .prose-copy { @apply whitespace-pre-wrap leading-8 text-slate-300; } }
|
||||
[x-cloak] {display:none!important}
|
||||
@@ -0,0 +1 @@
|
||||
//
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout title="Your account · Tarnax"><p class="eyebrow mb-4">Your Tarnax</p><h1 class="mb-8">Welcome, {{ auth()->user()->name }}.</h1><div class="flex flex-wrap gap-3 mb-8"><a class="secondary" href="{{ route('security') }}">Account security</a><a class="secondary" href="{{ route('support') }}">Get support</a><a class="secondary" href="{{ route('privacy.export') }}">Export personal data</a></div><div class="grid md:grid-cols-2 gap-6"><section class="card"><h2>Direct licenses</h2>@forelse($licenses as $license)<div class="border-t border-slate-700 py-4"><p class="text-cyan-200 mb-2">{{ ucfirst($license->status) }} · {{ $license->device_limit }} devices</p><code class="text-sm break-all">{{ \Illuminate\Support\Facades\Crypt::decryptString($license->key_encrypted) }}</code><p class="text-xs muted mt-2">Keep your license key private.</p></div>@empty<p class="muted">Your license appears here after a verified direct purchase.</p>@endforelse</section><section class="card"><h2>Your downloads</h2>@forelse($releases as $release)<a class="block border-t border-slate-700 py-4" href="{{ route('download.link',$release->id) }}">{{ $release->version }} · {{ $release->platform }} ↗</a>@empty<p class="muted">No direct releases yet.</p>@endforelse</section><section class="card"><h2>Purchases</h2>@forelse($purchases as $purchase)<p class="py-3 border-t border-slate-700">{{ ucfirst($purchase->provider) }} · {{ $purchase->status }} · {{ $purchase->currency }} {{ number_format($purchase->total/100,2) }}</p>@empty<p class="muted">No purchases yet.</p>@endforelse</section><section class="card"><h2>App Store verification</h2><p class="muted text-sm mb-4">Your app can associate its StoreKit purchase using this account token. Apple purchases do not grant a direct license.</p><code class="text-xs break-all">{{ auth()->user()->account_token }}</code><details class="mt-4"><summary>Verify a signed StoreKit transaction</summary><form method="post" action="{{ route('apple.verify') }}" class="space-y-3 mt-3">@csrf<label>Signed transaction<textarea name="signed_transaction" required></textarea></label><button class="secondary">Verify purchase</button></form></details></section></div><section class="card mt-8"><h2>Privacy requests</h2><p class="muted mb-4">Request account erasure. Billing records required for legal retention may be retained; support reviews each request.</p><form method="post" action="{{ route('privacy.delete') }}">@csrf<button class="secondary">Request account deletion</button></form></section></x-layout>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout title="Admin · Tarnax"><p class="eyebrow mb-4">Tarnax / admin</p><h1 class="mb-8">Your control room.</h1><livewire:admin.cms /></x-layout>
|
||||
@@ -0,0 +1,9 @@
|
||||
<x-layout title="Account · Tarnax"><div class="max-w-md mx-auto card"><p class="eyebrow mb-4">Your Tarnax account</p><h2>{{ ucwords(str_replace('-',' ',$mode)) }}</h2><form method="post" action="{{ ['login'=>'/login','register'=>'/register','forgot-password'=>'/forgot-password','reset-password'=>'/reset-password','confirm-password'=>'/user/confirm-password','two-factor-challenge'=>'/two-factor-challenge'][$mode] }}" class="space-y-4">@csrf
|
||||
@if($mode==='register')<label>Name<input name="name" value="{{ old('name') }}" required autocomplete="name"></label>@endif
|
||||
@if(in_array($mode,['login','register','forgot-password','reset-password']))<label>Email<input type="email" name="email" value="{{ old('email',$request->email??'') }}" required autocomplete="email"></label>@endif
|
||||
@if(in_array($mode,['login','register','reset-password','confirm-password']))<label>Password<input type="password" name="password" required autocomplete="{{ in_array($mode,['register','reset-password'])?'new-password':'current-password' }}"></label>@endif
|
||||
@if(in_array($mode,['register','reset-password']))<label>Confirm password<input type="password" name="password_confirmation" required autocomplete="new-password"></label>@endif
|
||||
@if($mode==='reset-password')<input type="hidden" name="token" value="{{ $request->route('token') }}">@endif
|
||||
@if($mode==='two-factor-challenge')<label>Authenticator code<input name="code" inputmode="numeric" autocomplete="one-time-code"></label><label>Or recovery code<input name="recovery_code"></label>@endif
|
||||
@if($mode==='register')<p class="muted text-sm">By registering, you agree to the <a class="underline" href="{{ route('page','terms') }}">terms</a>. Read our <a class="underline" href="{{ route('page','privacy') }}">privacy notice</a>.</p>@endif
|
||||
<button class="button w-full">Continue →</button></form><div class="flex gap-4 text-sm mt-5"><a href="/login">Sign in</a><a href="/register">Register</a><a href="/forgot-password">Reset password</a></div></div></x-layout>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout><div class="card max-w-xl mx-auto"><h2>Verify your email</h2><p class="muted mb-5">Open the link we sent you to activate your account.</p><form method="post" action="/email/verification-notification">@csrf<button class="button">Resend verification email</button></form></div></x-layout>
|
||||
@@ -0,0 +1,7 @@
|
||||
@props(['title'=>'Tarnax — Your server. Your control.','description'=>'A native remote for your qBittorrent server.'])
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="csrf-token" content="{{ csrf_token() }}"><meta name="description" content="{{ $description }}"><title>{{ $title }}</title><link rel="icon" href="/brand/favicon.png">@vite(['resources/css/app.css','resources/js/app.js']) @livewireStyles</head>
|
||||
<body><header class="border-b border-slate-800"><div class="max-w-7xl mx-auto px-6 py-5 flex flex-wrap items-center justify-between gap-5"><a href="{{ route('home') }}" class="flex items-center gap-3 text-xl font-bold text-white"><img src="/brand/icon.png" width="36" height="36" class="rounded-xl" alt="">tarnax<span class="text-xs font-normal text-slate-500">/ software, considered.</span></a><nav aria-label="Main navigation" class="flex flex-wrap gap-6 text-sm"><a href="{{ route('releases') }}">Downloads</a><a href="{{ route('page','features') }}">Features</a><a href="{{ route('support') }}">Support</a>@auth<a href="{{ route('account') }}">My account</a>@if(auth()->user()->is_admin)<a href="{{ route('admin') }}">Admin</a>@endif<form method="post" action="/logout">@csrf<button>Sign out</button></form>@else<a href="/login">Sign in ↗</a>@endauth</nav></div></header>
|
||||
<main class="max-w-7xl mx-auto px-6 py-12 min-h-[70vh]">@if(session('status'))<div role="status" class="card mb-6 text-cyan-200">{{ session('status') }}</div>@endif @if($errors->any())<div role="alert" class="card mb-6 text-rose-300">{{ $errors->first() }}</div>@endif {{ $slot }}</main>
|
||||
<footer class="border-t border-slate-800"><div class="max-w-7xl mx-auto p-6 flex flex-wrap gap-5 text-sm text-slate-400"><span class="mr-auto">© {{ date('Y') }} Tarnax · Independent software</span>@foreach(['privacy','terms','refunds','cookies'] as $slug)<a href="{{ route('page',$slug) }}">{{ ucfirst($slug) }}</a>@endforeach<button onclick="window.dispatchEvent(new Event('cookie-preferences'))">Cookie preferences</button></div><p class="max-w-7xl mx-auto px-6 pb-6 text-xs text-slate-500">Requires your own qBittorrent server. Use only content you are authorised to access and share. Not affiliated with the qBittorrent project.</p></footer>
|
||||
<div x-data="{open: !document.cookie.includes('tarnax_consent='), analytics:false, marketing:false, error:'', async save(a,m){try{let r=await fetch('{{ route('consent') }}',{method:'POST',headers:{'Content-Type':'application/json','X-CSRF-TOKEN':document.querySelector('meta[name=csrf-token]').content,'Accept':'application/json'},body:JSON.stringify({analytics:a,marketing:m})});if(!r.ok)throw Error();this.open=false;window.dispatchEvent(new Event('consent-changed'));}catch(e){this.error='Could not save preferences. Please try again.'}}}" @cookie-preferences.window="open=true" x-show="open" x-cloak role="dialog" aria-label="Cookie preferences" class="fixed bottom-4 left-4 right-4 md:left-auto md:w-[520px] card shadow-2xl z-50"><h3>Your privacy, your choice</h3><p class="muted text-sm my-3">Essential cookies keep your account secure. Optional cookies stay off until you choose them. No optional trackers are installed in this build.</p><label><input type="checkbox" x-model="analytics"> Analytics</label><label><input type="checkbox" x-model="marketing"> Marketing</label><div class="flex flex-wrap gap-2 mt-4"><button class="secondary" @click="save(false,false)">Reject optional</button><button class="secondary" @click="save(analytics,marketing)">Save choices</button><button class="button" @click="save(true,true)">Accept all</button></div><p x-text="error" role="alert"></p></div>
|
||||
@livewireScripts</body></html>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-layout>
|
||||
@php($settings=\Illuminate\Support\Facades\DB::table('settings')->pluck('value','key'))
|
||||
<section class="grid lg:grid-cols-2 gap-16 items-center py-12 md:py-20"><div><div class="eyebrow mb-6">Built for your connection</div><h1 class="leading-[1.05]">{{ $settings['headline'] ?? 'Your server. Your control.' }}</h1><p class="text-lg text-slate-400 leading-8 my-7 max-w-xl">{{ $settings['tagline'] ?? 'A native remote for your qBittorrent server.' }}</p><div class="flex flex-wrap gap-3">@if(config('tarnax.apple.url'))<a class="button" href="{{ config('tarnax.apple.url') }}" rel="noopener">Download on the App Store ↗</a>@endif<a href="#direct" class="secondary">Explore direct purchase →</a></div><p class="text-xs text-slate-500 mt-5">Your server. Your files. A clearer view.</p></div><div class="relative rounded-3xl border border-slate-700 bg-gradient-to-br from-cyan-950 via-slate-900 to-indigo-950 p-8 shadow-2xl"><div class="flex items-center gap-3 mb-10"><img src="/brand/icon.png" class="rounded-2xl" width="56" height="56" alt="Tarnax icon"><div><h3>Tarnax</h3><p class="text-sm muted">A little closer to your server.</p></div><span class="ml-auto rounded-full bg-cyan-400/10 text-cyan-200 text-xs px-3 py-1">Interface preview</span></div><div class="grid grid-cols-2 gap-4"><div class="card"><span class="text-xs muted">DOWNLOAD</span><p class="text-3xl text-white mt-2">42.8 <span class="text-sm muted">MB/s</span></p></div><div class="card"><span class="text-xs muted">UPLOAD</span><p class="text-3xl text-white mt-2">8.2 <span class="text-sm muted">MB/s</span></p></div></div><svg viewBox="0 0 400 120" class="w-full my-6" role="img" aria-label="Illustrative transfer activity"><path d="M0 105L30 80L60 90L90 40L120 60L150 25L180 50L210 15L240 40L270 20L300 45L330 10L360 30L400 15" fill="none" stroke="#67e8f9" stroke-width="3"/><path d="M0 115L30 110L60 100L90 110L120 80L150 90L180 65L210 85L240 70L270 90L300 60L330 80L360 55L400 70" fill="none" stroke="#818cf8" stroke-width="2"/></svg><div class="flex justify-between text-sm muted border-t border-slate-700 pt-5"><span>Everything in view.</span><span>Designed with care ↗</span></div></div></section>
|
||||
<section class="grid md:grid-cols-3 gap-5 my-12">@foreach([['01 / SEE THE FLOW','A clearer picture','Live transfer activity, speeds and server space, thoughtfully presented.'],['02 / MANAGE THE DETAILS','Everything in its place','Search, filter and organise transfers. Fine-tune file priorities and server preferences.'],['03 / YOUR CONNECTION','Made for your server','Connect to your own qBittorrent Web UI over trusted HTTPS. Your content stays on your server.']] as [$label,$title,$body])<article class="card"><p class="eyebrow mb-8">{{ $label }}</p><h3>{{ $title }}</h3><p class="muted mt-3 leading-7">{{ $body }}</p></article>@endforeach</section>
|
||||
<section id="direct" class="card my-20 grid md:grid-cols-2 gap-10 p-10"><div><p class="eyebrow mb-4">Two ways to make it yours</p><h2>Choose your distribution.</h2><p class="muted leading-7">App Store purchases are managed by Apple. Direct purchases use Paddle and include a separate license for supported direct-distribution builds. Check release availability before purchasing.</p></div><div><h3>Direct license</h3><p class="text-3xl text-white my-4">{{ $settings['price_label'] ?? 'Pricing coming soon' }}</p><p class="muted text-sm mb-5">Two device activations · Account downloads · Personal support</p>@auth<button class="button" x-data @click="window.startTarnaxCheckout($el)">Buy securely with Paddle →</button>@else<a class="button" href="/register">Create an account to purchase →</a>@endauth<p id="checkout-status" role="status" class="mt-3 text-sm muted"></p></div></section>
|
||||
@if(config('tarnax.paddle.client_token'))<script src="https://cdn.paddle.com/paddle/v2/paddle.js"></script><script>window.addEventListener('load',()=>{ @if(config('tarnax.paddle.environment')!=='production') Paddle.Environment.set('sandbox'); @endif Paddle.Initialize({token:@json(config('tarnax.paddle.client_token'))}); });</script>@endif
|
||||
<script>window.startTarnaxCheckout=async function(button){button.disabled=true;let status=document.getElementById('checkout-status');try{let r=await fetch(@json(route('checkout')),{method:'POST',headers:{'X-CSRF-TOKEN':document.querySelector('meta[name=csrf-token]').content,'Accept':'application/json'}});let data=await r.json();if(!r.ok)throw Error(data.message||'Checkout unavailable');if(!window.Paddle)throw Error('Checkout is awaiting configuration.');Paddle.Checkout.open({transactionId:data.transaction_id});status.textContent='Complete payment in the secure checkout. Your account updates after confirmation.';}catch(e){status.textContent=e.message;}finally{button.disabled=false;}}</script>
|
||||
</x-layout>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div><div class="flex flex-wrap gap-4 mb-6"><label class="min-w-56">Manage<select wire:model.live="section">@foreach($sections as $name)<option value="{{ $name }}">{{ ucwords(str_replace('_',' ',$name)) }}</option>@endforeach</select></label><div class="card flex-1 text-sm muted">Protected by administrator access and confirmed 2FA. Showing the latest 100 records per section. Mutations are audited.</div></div>@if(session('status'))<p role="status" class="text-cyan-200 mb-4">{{ session('status') }}</p>@endif @if($errors->any())<p role="alert" class="text-rose-300 mb-4">{{ $errors->first() }}</p>@endif
|
||||
@if(in_array($section,['pages','releases']))<form wire:submit="save" class="card grid md:grid-cols-2 gap-4 mb-8"><h2>{{ $editing?'Edit':'Create' }} {{ rtrim($section,'s') }}</h2>@foreach($section==='pages'?['slug','title','description']:['version','channel','platform'] as $field)<label>{{ ucfirst($field) }}<input wire:model="form.{{ $field }}"></label>@endforeach<label class="md:col-span-2">{{ $section==='pages'?'Body (plain text, safe by default)':'Release notes' }}<textarea rows="6" wire:model="form.{{ $section==='pages'?'body':'notes' }}"></textarea></label>@if($section==='releases')<label>Private binary (ZIP, DMG or PKG)<input type="file" wire:model="upload" accept=".zip,.dmg,.pkg"></label>@endif<label><input type="checkbox" wire:model="form.published"> Published</label><button class="button" wire:loading.attr="disabled">Save</button><button type="button" class="secondary" wire:click="newRecord">New record</button></form>@endif
|
||||
@if($section==='settings')<form wire:submit="saveSettings" class="card mb-8 space-y-4"><h2>Storefront settings</h2>@foreach(['headline','tagline','price_label'] as $field)<label>{{ ucwords(str_replace('_',' ',$field)) }}<input wire:model="form.{{ $field }}" required></label>@endforeach<button class="button">Save settings</button></form>@endif
|
||||
<div class="card overflow-x-auto"><table class="w-full text-left text-sm"><thead><tr>@if($rows->isNotEmpty())@foreach(array_keys((array)$rows->first()) as $key)<th class="p-3 text-slate-400">{{ $key }}</th>@endforeach<th>Actions</th>@endif</tr></thead><tbody>@forelse($rows as $row)<tr class="border-t border-slate-800">@foreach((array)$row as $key=>$value)<td class="p-3 max-w-64 break-words">{{ \Illuminate\Support\Str::limit((string)$value,160) }}</td>@endforeach<td class="p-3">@if(in_array($section,['pages','releases']))<button class="text-cyan-200" wire:click="edit({{ $row->id }})">Edit</button>@elseif($section==='licenses')<button class="text-rose-200" wire:confirm="Revoke this license?" wire:click="revoke({{ $row->id }})">Revoke</button>@elseif($section==='privacy_requests' && $row->status==='pending')<button wire:confirm="Erase customer profile and support data, revoke licenses, and retain billing records?" wire:click="fulfillDeletion({{ $row->id }})">Fulfill deletion</button>@endif</td></tr>@empty<tr><td class="p-5 muted">No records yet.</td></tr>@endforelse</tbody></table></div></div>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="grid lg:grid-cols-3 gap-6"><section class="card"><h2>Start a conversation</h2><form wire:submit="create" class="space-y-4"><label>Subject<input wire:model="subject" required maxlength="200"></label><label>How can we help?<textarea wire:model="body" rows="5" required></textarea></label><button class="button" wire:loading.attr="disabled">Create ticket</button></form><h3 class="mt-8">Your tickets</h3>@foreach($tickets as $ticket)<button wire:click="select({{ $ticket->id }})" class="block text-left w-full py-4 border-b border-slate-700">#{{ $ticket->id }} {{ $ticket->subject }}<span class="block text-xs muted">{{ $ticket->status }}</span></button>@endforeach</section><section class="card lg:col-span-2">@if($errors->any())<p role="alert" class="text-rose-300 mb-4">{{ $errors->first() }}</p>@endif @if($ticketId)<h2>Ticket #{{ $ticketId }}</h2>@foreach($messages as $message)<article class="border-b border-slate-700 py-5"><p class="text-sm text-cyan-200">{{ $message->name }} · {{ $message->created_at }}</p><p class="prose-copy mt-2">{{ $message->body }}</p></article>@endforeach<form wire:submit="send" class="space-y-4 mt-6"><label>Reply<textarea wire:model="reply" required rows="4"></textarea></label><button class="button" wire:loading.attr="disabled">Send reply</button><button type="button" wire:click="close" class="secondary">Close ticket</button></form>@else<p class="muted">Choose a conversation or create a ticket. Please never send passwords or license keys.</p>@endif</section></div>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout :title="$page->title.' · Tarnax'" :description="$page->description ?? ''"><article class="max-w-3xl mx-auto"><p class="eyebrow mb-5">Tarnax / {{ $page->slug }}</p><h1 class="mb-10">{{ $page->title }}</h1><div class="prose-copy">{{ $page->body }}</div></article></x-layout>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout title="Releases · Tarnax"><p class="eyebrow mb-5">Always moving forward</p><h1 class="mb-5">Downloads & releases</h1><p class="muted mb-10">Direct builds require an active direct license. App Store builds update through Apple.</p><div class="space-y-5">@forelse($releases as $release)<article class="card"><div class="flex justify-between gap-4"><h2>Tarnax {{ $release->version }}</h2><span class="muted">{{ $release->platform }} · {{ $release->channel }}</span></div><p class="prose-copy mb-5">{{ $release->notes }}</p><a class="button" href="{{ route('download.link',$release->id) }}">Download →</a><p class="text-xs muted mt-4 break-all">SHA-256: {{ $release->sha256 }}</p></article>@empty<div class="card">No direct builds have been published yet.</div>@endforelse</div><div class="mt-6">{{ $releases->links() }}</div></x-layout>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout title="Security · Tarnax"><div class="max-w-2xl space-y-6"><h1 class="mb-8">Account security</h1><section class="card"><h2>Two-factor authentication</h2>@if(!auth()->user()->two_factor_secret)<form method="post" action="/user/two-factor-authentication">@csrf<button class="button">Enable authenticator</button></form>@else<div class="bg-white p-4 inline-block rounded-xl">{!! auth()->user()->twoFactorQrCodeSvg() !!}</div>@if(!auth()->user()->two_factor_confirmed_at)<form method="post" action="/user/confirmed-two-factor-authentication" class="space-y-3 mt-5">@csrf<label>Authenticator code<input name="code" required autocomplete="one-time-code"></label><button class="button">Confirm setup</button></form>@else<p class="mt-4 text-cyan-200">Two-factor authentication is active.</p>@endif<details class="mt-4"><summary>Recovery codes — store securely</summary><pre class="mt-3 text-sm">{{ implode("\n",auth()->user()->recoveryCodes()) }}</pre></details><form method="post" action="/user/two-factor-authentication" class="mt-5">@csrf @method('DELETE')<button class="secondary">Disable two-factor authentication</button></form>@endif</section><section class="card"><h2>Change password</h2><form method="post" action="/user/password" class="space-y-4">@csrf @method('PUT')<label>Current password<input type="password" name="current_password" required></label><label>New password<input type="password" name="password" required></label><label>Confirm new password<input type="password" name="password_confirmation" required></label><button class="button">Update password</button></form></section></div></x-layout>
|
||||
@@ -0,0 +1 @@
|
||||
<x-layout title="Support · Tarnax"><p class="eyebrow mb-4">Tarnax / support</p><h1 class="mb-8">Let’s get it sorted.</h1><livewire:support-desk /></x-layout>
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user