Files
SkinbaseNova/resources/views/_legacy/user.blade.php
Gregor Klevze d0aefc5ddc feat: Nova homepage, profile redesign, and legacy view system overhaul
Homepage
- Add HomepageService with hero, trending (award-weighted), fresh uploads,
  popular tags, creator spotlight (weekly uploads ranking), and news sections
- Add React components: HomePage, HomeHero, HomeTrending, HomeFresh,
  HomeTags, HomeCreators, HomeNews (lazy-loaded below the fold)
- Wire home.blade.php with JSON props, SEO meta, JSON-LD, and hero preload
- Add HomePage.jsx to vite.config.js inputs

Profile page
- Hero banner with random user artwork as background + dark gradient overlay
- Favourites section uses real Artwork models + <x-artwork-card> for CDN URLs
- Newest artworks grid: gallery-grid → grid grid-cols-2 gap-4

Edit Profile page (user.blade.php)
- Add hero banner (featured wallpaper/photography via artwork_features,
  content_type_id IN [2,3]) sourced in UserController
- Remove bg-deep from outer wrapper; card backgrounds: bg-panel → bg-nova-800
- Remove stray AI-generated tag fragment from template

Author profile links
- Fix all /@username routes in: HomepageService, MonthlyCommentatorsController,
  LatestCommentsController, MyBuddiesController and corresponding blade views

Legacy view namespace
- Register View::addNamespace('legacy', resource_path('views/_legacy'))
  in AppServiceProvider::boot()
- Convert all view('legacy.x') and @include('legacy.x') calls to legacy::x
- Migrate legacy views to resources/views/_legacy/ with namespace support
2026-02-26 10:25:35 +01:00

369 lines
16 KiB
PHP

@extends('layouts.nova')
@section('content')
@php
$birthDay = $birthDay ?? null;
$birthMonth = $birthMonth ?? null;
$birthYear = $birthYear ?? null;
$avatarUserId = (int) ($user->id ?? auth()->id() ?? 0);
$avatarHash = $user->icon
?? optional(auth()->user())->profile->avatar_hash
?? null;
$currentAvatarUrl = !empty($avatarHash)
? \App\Support\AvatarUrl::forUser($avatarUserId, $avatarHash, 128)
: \App\Support\AvatarUrl::default();
@endphp
@push('styles')
<style>
.edit-profile-hero { position: relative; overflow: hidden; }
</style>
@endpush
{{-- ── Hero background ──────────────────────────────────────────────── --}}
<div class="edit-profile-hero border-b border-[--sb-line]"
@if(!empty($heroBgUrl))
style="background: url('{{ $heroBgUrl }}') center/cover no-repeat;"
@else
style="background: linear-gradient(135deg, rgba(15,23,36,1) 0%, rgba(21,30,46,1) 50%, rgba(9,16,26,1) 100%);"
@endif
>
@if(!empty($heroBgUrl))
<div class="absolute inset-0 bg-gradient-to-r from-[#0f1724]/95 via-[#0f1724]/80 to-[#0f1724]/50"></div>
@endif
<div class="relative z-10 max-w-5xl mx-auto px-4 py-10 flex items-end gap-5">
<img src="{{ $currentAvatarUrl }}"
alt="Your avatar"
class="w-20 h-20 rounded-full object-cover border-4 border-[--sb-line] shadow-lg shrink-0">
<div>
<h1 class="text-2xl font-bold text-white">Edit Profile</h1>
<p class="text-sm text-[--sb-muted] mt-1">Manage your account settings</p>
</div>
</div>
</div>
<div class="min-h-screen text-white py-12">
<!-- Container -->
<div class="max-w-5xl mx-auto px-4">
@if ($errors->any())
<div class="mb-4 rounded-lg bg-red-700/10 border border-red-700/20 p-3 text-sm text-red-300">
<div class="font-semibold mb-2">Please fix the following errors:</div>
<ul class="list-disc pl-5 space-y-1">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<!-- ================= Profile Card ================= -->
<div class="bg-nova-800 rounded-xl shadow-lg p-8 mb-10">
<form method="POST" action="{{ route('profile.update') }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- LEFT COLUMN -->
<div class="space-y-5">
<!-- Email -->
<div>
<label class="form-label">Email</label>
<input type="email" name="email"
class="form-input"
value="{{ old('email', auth()->user()->email) }}">
</div>
<!-- Username -->
<div>
<label class="form-label">Username</label>
<input type="text" name="username"
class="form-input"
value="{{ old('username', auth()->user()->username) }}"
readonly>
</div>
<!-- Real Name -->
<div>
<label class="form-label">Real Name</label>
<input type="text" name="name"
class="form-input"
value="{{ old('name', auth()->user()->name) }}">
</div>
<!-- Homepage -->
<div>
<label class="form-label">Homepage</label>
<input type="url" name="homepage"
class="form-input"
placeholder="https://"
value="{{ old('homepage', auth()->user()->homepage ?? auth()->user()->website ?? '') }}">
</div>
<!-- Birthday -->
<div>
<label class="form-label">Birthday</label>
<div class="grid grid-cols-3 gap-3">
@php
$currentYear = date('Y');
$startYear = $currentYear - 100;
$months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
@endphp
<select name="day" class="form-input" aria-label="Day">
<option value="">Day</option>
@for($d = 1; $d <= 31; $d++)
<option value="{{ $d }}" @if(intval(old('day', $birthDay)) == $d) selected @endif>{{ $d }}</option>
@endfor
</select>
<select name="month" class="form-input" aria-label="Month">
<option value="">Month</option>
@foreach($months as $idx => $m)
@php $val = $idx + 1; @endphp
<option value="{{ $val }}" @if(intval(old('month', $birthMonth)) == $val) selected @endif>{{ $m }}</option>
@endforeach
</select>
<select name="year" class="form-input" aria-label="Year">
<option value="">Year</option>
@for($y = $currentYear; $y >= $startYear; $y--)
<option value="{{ $y }}" @if(intval(old('year', $birthYear)) == $y) selected @endif>{{ $y }}</option>
@endfor
</select>
</div>
</div>
<!-- Gender -->
<div>
<label class="form-label">Gender</label>
<div class="flex gap-6 mt-2 text-soft">
<label><input type="radio" name="gender" value="m" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'm') checked @endif> Male</label>
<label><input type="radio" name="gender" value="f" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'f') checked @endif> Female</label>
<label><input type="radio" name="gender" value="x" @if(old('gender', strtolower(auth()->user()->gender ?? '')) == 'x') checked @endif> N/A</label>
</div>
</div>
<!-- Country -->
<div>
<label class="form-label">Country</label>
<input type="text" name="country"
class="form-input"
value="{{ old('country', auth()->user()->country ?? auth()->user()->country_code ?? '') }}">
</div>
<!-- Preferences -->
<div class="flex gap-6 text-soft">
<label>
<input type="checkbox" name="mailing" @if(old('mailing', auth()->user()->mlist ?? false)) checked @endif>
Mailing List
</label>
<label>
<input type="checkbox" name="notify" @if(old('notify', auth()->user()->friend_upload_notice ?? false)) checked @endif>
Upload Notifications
</label>
</div>
</div>
<!-- RIGHT COLUMN -->
<div class="space-y-5">
<!-- Avatar -->
<div>
<label class="form-label">Avatar</label>
<div class="rounded-xl border border-sb-line bg-black/10 p-4">
<div class="flex items-center gap-4">
<img
id="avatarPreviewImage"
src="{{ $currentAvatarUrl }}"
alt="{{ $user->name ?? $user->username ?? 'Avatar' }}"
class="w-24 h-24 rounded-full object-cover ring-1 ring-white/10"
loading="lazy"
decoding="async"
>
<div class="min-w-0 flex-1">
<button
type="button"
id="avatarDropzone"
class="w-full rounded-lg border border-dashed border-sb-line px-4 py-4 text-left hover:bg-white/5 focus:outline-none focus:ring-2 focus:ring-sb-blue/50"
>
<div class="text-sm text-white/90">Drag & drop a new avatar here</div>
<div class="text-xs text-soft mt-1">or click to browse (JPG, PNG, WEBP)</div>
<div id="avatarFileName" class="text-xs text-sb-muted mt-2 truncate">No file selected</div>
</button>
<input id="avatarInput" type="file" name="avatar" class="sr-only" accept="image/jpeg,image/png,image/webp">
</div>
</div>
</div>
</div>
<!-- Emoticon -->
<div>
<label class="form-label">Emoticon</label>
<input type="file" name="emoticon" class="form-file">
</div>
<!-- Personal Picture -->
<div>
<label class="form-label">Personal Picture</label>
<input type="file" name="photo" class="form-file">
</div>
<!-- About -->
<div>
<label class="form-label">About Me</label>
<textarea name="about" rows="4"
class="form-textarea">{{ old('about', auth()->user()->about ?? auth()->user()->about_me ?? '') }}</textarea>
</div>
<!-- Signature -->
<div>
<label class="form-label">Signature</label>
<textarea name="signature" rows="3"
class="form-textarea">{{ old('signature', auth()->user()->signature ?? '') }}</textarea>
</div>
<!-- Description -->
<div>
<label class="form-label">Description</label>
<textarea name="description" rows="4"
class="form-textarea">{{ old('description', auth()->user()->description ?? '') }}</textarea>
</div>
</div>
</div>
<!-- Save Button -->
<div class="mt-8 text-right">
<button type="submit"
class="btn-primary">
Update Profile
</button>
</div>
</form>
</div>
<!-- ================= PASSWORD CARD ================= -->
<div class="bg-nova-800 rounded-xl shadow-lg p-8">
<h2 class="text-xl font-semibold mb-6">
Change Password
</h2>
<form method="POST" action="{{ route('profile.password') }}">
@csrf
@method('PUT')
<div class="space-y-5 max-w-md">
<div>
<label class="form-label">Current Password</label>
<input type="password" name="current_password"
class="form-input">
</div>
<div>
<label class="form-label">New Password</label>
<input type="password" name="password"
class="form-input">
</div>
<div>
<label class="form-label">Repeat Password</label>
<input type="password" name="password_confirmation"
class="form-input">
</div>
<button class="btn-secondary">
Change Password
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
(() => {
const input = document.getElementById('avatarInput');
const dropzone = document.getElementById('avatarDropzone');
const preview = document.getElementById('avatarPreviewImage');
const fileName = document.getElementById('avatarFileName');
if (!input || !dropzone || !preview || !fileName) {
return;
}
const updatePreview = (file) => {
if (!file || !file.type || !file.type.startsWith('image/')) {
return;
}
const objectUrl = URL.createObjectURL(file);
preview.src = objectUrl;
fileName.textContent = file.name;
preview.onload = () => URL.revokeObjectURL(objectUrl);
};
dropzone.addEventListener('click', () => input.click());
input.addEventListener('change', () => {
const file = input.files && input.files[0] ? input.files[0] : null;
if (file) {
updatePreview(file);
}
});
['dragenter', 'dragover'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
dropzone.classList.add('ring-2', 'ring-sb-blue/50');
});
});
['dragleave', 'drop'].forEach((eventName) => {
dropzone.addEventListener(eventName, (event) => {
event.preventDefault();
event.stopPropagation();
dropzone.classList.remove('ring-2', 'ring-sb-blue/50');
});
});
dropzone.addEventListener('drop', (event) => {
const file = event.dataTransfer && event.dataTransfer.files ? event.dataTransfer.files[0] : null;
if (!file) {
return;
}
const dt = new DataTransfer();
dt.items.add(file);
input.files = dt.files;
updatePreview(file);
});
})();
</script>
@endpush