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
This commit is contained in:
368
resources/views/_legacy/user.blade.php
Normal file
368
resources/views/_legacy/user.blade.php
Normal file
@@ -0,0 +1,368 @@
|
||||
@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
|
||||
Reference in New Issue
Block a user