current state

This commit is contained in:
2026-02-08 10:42:01 +01:00
parent 0a4372c40d
commit e055af9248
70 changed files with 4882 additions and 330 deletions

View File

@@ -0,0 +1,181 @@
# 1⃣ Final SQL New Favorites Table (Artwork-only)
**Table name:** `user_favorites`
**Scope:** users ↔ artworks
**Engine:** InnoDB
**Charset:** utf8mb4
```sql
CREATE TABLE user_favorites (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
artwork_id BIGINT UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uniq_user_artwork (user_id, artwork_id),
KEY idx_artwork (artwork_id),
KEY idx_user_created (user_id, created_at),
CONSTRAINT fk_user_favorites_user
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE,
CONSTRAINT fk_user_favorites_artwork
FOREIGN KEY (artwork_id) REFERENCES artworks(id)
ON DELETE CASCADE
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
```
This is **production-ready** and matches everything we discussed:
* no legacy fields
* no duplication
* proper constraints
* fast queries
---
# 2⃣ VS Code Agent Markdown (Laravel 12 Migration Prompt)
Save this as for example:
```
.vscode/agents/laravel-user-favorites-migration.md
```
or
```
.cursor/rules/laravel-user-favorites.md
```
---
```markdown
# Laravel 12 User Favorites Migration & Model
## Context
We are migrating legacy "favourites" functionality into a clean, modern Laravel 12 system.
Each user can add artworks to their favorites list.
This is a many-to-many relationship between users and artworks.
Legacy table MUST NOT be reused.
---
## Goal
Create a Laravel 12 database migration and Eloquent model for a new table named:
```
user_favorites
````
This table stores **which user favorited which artwork**, with a timestamp.
---
## Database Requirements
### Table: user_favorites
| Column | Type | Notes |
|------|------|------|
| id | BIGINT UNSIGNED | Primary key |
| user_id | BIGINT UNSIGNED | FK → users.id |
| artwork_id | BIGINT UNSIGNED | FK → artworks.id |
| created_at | TIMESTAMP | When artwork was favorited |
### Constraints & Indexes
- UNIQUE (user_id, artwork_id)
→ prevents duplicate favorites
- INDEX artwork_id
→ fast favorite count per artwork
- INDEX (user_id, created_at)
→ fast "my favorites" queries
### Foreign Keys
- user_id → users.id (ON DELETE CASCADE)
- artwork_id → artworks.id (ON DELETE CASCADE)
### Engine & Charset
- Engine: InnoDB
- Charset: utf8mb4
- Collation: utf8mb4_unicode_ci
---
## Laravel Migration Requirements
- Use `Schema::create`
- Use `foreignId()->constrained()->cascadeOnDelete()`
- Use `timestamps()` **ONLY if created_at is needed**
(do NOT add updated_at)
- Add explicit indexes and unique constraints
---
## Laravel Model Requirements
### Model: UserFavorite
- Table: `user_favorites`
- `$timestamps = false` (created_at handled manually or via DB default)
- Fillable:
- user_id
- artwork_id
- created_at
### Relationships
```php
UserFavorite belongsTo User
UserFavorite belongsTo Artwork
````
---
## Additional Notes
* This table is interaction-based, NOT content-based
* Do NOT store favorite counts here
* Favorite counts will be aggregated separately (Redis or statistics table)
* This table must be lean and write-optimized
---
## Deliverables
* Migration file for Laravel 12
* Eloquent model `UserFavorite`
* Proper naming and clean schema
* No legacy fields, no polymorphic logic
Generate clean, production-quality code.
````
---
## 3⃣ How to Use This in VS Code (Quick Steps)
1. Paste markdown into `.vscode/agents/` or `.cursor/rules/`
2. Open VS Code
3. Ask your AI agent:
> “Create Laravel 12 migration and model based on this document”
4. Review generated migration
5. Run:
```bash
php artisan migrate
````

View File

@@ -0,0 +1,312 @@
# Skinbase User Schema Review & Upgrade Plan
**Database:** MySQL / Percona 8.x
**Project:** Skinbase (new system, no legacy dependencies)
**Reviewed tables:** users, user_profiles, user_social_links, user_statistics
---
## 1. Overview
The current user-related database schema is **well designed**, modern, and suitable
for long-term growth.
Key strengths include:
- Clear separation of concerns
- Proper use of foreign keys and cascading deletes
- BigInt primary keys
- Soft deletes on users
- Migration-friendly legacy password handling
This document summarizes:
- what is already good
- recommended optimizations
- future-proofing steps (non-breaking)
- performance considerations for scale
---
## 2. users Table
### 2.1 Whats Good
- Unique `username` and `email`
- `legacy_password_algo` allows smooth migration from old systems
- `needs_password_reset` improves security posture
- Role stored as string allows flexibility in early stages
- Soft deletes enabled
### 2.2 Recommended Indexes
Add indexes for common query patterns:
```sql
CREATE INDEX idx_users_active ON users (is_active);
CREATE INDEX idx_users_role ON users (role);
CREATE INDEX idx_users_last_visit ON users (last_visit_at);
````
These improve:
* active user filtering
* admin queries
* “last seen” or online user features
### 2.3 Future Role Normalization (Planned)
Current approach is fine short-term:
```text
role = 'user' | 'admin' | 'moderator'
```
Planned future upgrade:
* `roles` table
* `user_roles` pivot table (many-to-many)
This allows:
* multiple roles per user
* temporary or scoped roles
* better permission modeling
⚠️ No immediate action required — just avoid hard-coding role logic.
### 2.4 Optional Security Enhancements
Recommended additions (optional but advised):
```sql
last_password_change_at TIMESTAMP NULL,
failed_login_attempts INT UNSIGNED DEFAULT 0,
locked_until TIMESTAMP NULL
```
Enables:
* rate limiting
* temporary account locking
* better auditability
---
## 3. user_profiles Table
### 3.1 Strengths
* Clean one-to-one relationship with `users`
* Public profile data separated from auth data
* Nullable fields for progressive profile completion
* Inclusive gender enum with safe default
* Localization-ready (`language`, `country_code`)
### 3.2 Country Handling Recommendation
Prefer using only:
```text
country_code (ISO 3166-1 alpha-2)
```
Benefits:
* language-independent
* avoids inconsistent country naming
* easier frontend mapping
`country` text field can be deprecated later if needed.
### 3.3 Avatar & Media Metadata (Future)
Current:
```text
avatar VARCHAR(255)
```
Recommended future approach:
```text
avatar_hash CHAR(64)
avatar_ext VARCHAR(10)
avatar_updated_at TIMESTAMP
```
This aligns with:
* hash-based file storage
* CDN-friendly URLs
* cache invalidation control
---
## 4. user_social_links Table
### 4.1 Current Design
* One row per platform per user
* Unique constraint on `(user_id, platform)`
* Cascade delete enabled
This is solid.
### 4.2 Platform Normalization (Recommended)
Current:
```text
platform VARCHAR(32)
```
Risk:
* inconsistent values (`twitter`, `x`, `Twitter`, etc.)
Options:
**Option A Enum (simple):**
```sql
ENUM('twitter','x','instagram','deviantart','artstation','github','website')
```
**Option B Reference Table (best long-term):**
* `social_platforms`
* foreign key `platform_id`
Option B is preferred for Skinbase as platforms evolve.
---
## 5. user_statistics Table
### 5.1 Strengths
* Isolated counters
* One row per user
* Minimal row size
* Clean FK relationship
### 5.2 Performance Warning (Important)
Avoid frequent direct updates like:
```sql
UPDATE user_statistics SET downloads = downloads + 1;
```
At scale, this causes:
* row locking
* write contention
* degraded performance
### 5.3 Recommended Strategy
* Use **Redis** (or in-memory cache) for real-time increments
* Periodically flush aggregated values to MySQL
* Use jobs / cron for batch updates
This ensures:
* fast user interactions
* scalable statistics tracking
---
## 6. Charset & Collation
Current:
```text
utf8mb4_unicode_ci
```
This is correct and safe.
Optional upgrade (MySQL 8+):
```text
utf8mb4_0900_ai_ci
```
Benefits:
* newer Unicode rules
* slightly better performance
Not required immediately.
---
## 7. Tables Planned for Future Expansion
Not required now, but expected as Skinbase grows:
### 7.1 user_activity_log
* logins
* uploads
* profile edits
* moderation actions
### 7.2 user_followers
* artist-to-artist following
* social graph features
### 7.3 user_settings
* privacy preferences
* notification settings
* email subscriptions
These should remain **separate tables** to avoid bloating `users`.
---
## 8. Laravel Implementation Notes
Recommended model casting:
```php
protected $casts = [
'is_active' => 'boolean',
'needs_password_reset' => 'boolean',
'email_verified_at' => 'datetime',
'last_visit_at' => 'datetime',
];
```
Best practices:
* Eager load profiles (`with('profile')`)
* Cache public profile + statistics
* Keep statistics out of main user queries
---
## 9. Final Verdict
**Schema quality:** Excellent
**Scalability:** Very good
**Migration readiness:** Excellent
**Long-term Skinbase fit:** Excellent
The current schema is production-ready and significantly better than typical legacy
user databases. Most future improvements can be introduced **without breaking changes**.
Primary future wins:
* Redis-backed statistics
* normalized roles and social platforms
* hash-based media storage
---
**Status:** Approved for production
**Next steps:** API design, public profile queries, legacy user migration
```

View File

@@ -22,7 +22,7 @@ class Chat
return;
}
$last = DB::connection('legacy')->table('chat')
$last = DB::table('chat')
->select('message')
->where('user_id', $userId)
->orderByDesc('chat_id')
@@ -30,7 +30,7 @@ class Chat
->first();
if (!$last || ($last->message ?? '') !== $tekst) {
DB::connection('legacy')->table('chat')->insert([
DB::table('chat')->insert([
'time' => now(),
'sender' => $username,
'user_id' => $userId,
@@ -43,7 +43,7 @@ class Chat
{
$output = "<ul>";
$chats = DB::connection('legacy')->table('chat')
$chats = DB::table('chat')
->select('time', 'sender', 'message')
->orderByDesc('chat_id')
->limit((int)$num_rows ?: 8)

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class ImportLegacyFavourites extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'import:legacy-favourites
{--connection=legacy : Legacy DB connection name}
{--table=favourites : Legacy favourites table name}
{--id-column=id : ID column to use for chunking}
{--map-user=user_id : Column name for user id}
{--map-artwork=artwork_id : Column name for artwork id}
{--map-created=datum : Column name for created timestamp}
{--chunk=500 : Chunk size}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Copy legacy favourites (from another DB connection) into user_favorites';
public function handle(): int
{
$connection = $this->option('connection');
$table = $this->option('table');
$idColumn = $this->option('id-column');
$mapUser = $this->option('map-user');
$mapArtwork = $this->option('map-artwork');
$mapCreated = $this->option('map-created');
$chunk = (int) $this->option('chunk');
$this->info("Using connection='{$connection}', table='{$table}', idColumn='{$idColumn}'");
try {
$legacy = DB::connection($connection);
} catch (\Throwable $e) {
$this->error('Cannot connect to legacy connection: '.$e->getMessage());
return 1;
}
try {
$schema = $legacy->getSchemaBuilder();
} catch (\Throwable $e) {
$this->error('Failed to get schema builder for legacy connection: '.$e->getMessage());
return 1;
}
if (! $schema->hasTable($table)) {
$this->error("Table '{$table}' does not exist on connection '{$connection}'");
return 1;
}
$this->info('Starting import...');
$attempted = 0;
$inserted = 0;
// Try chunkById for efficient processing; fallback to cursor if id column missing
try {
$legacy->table($table)
->select([$idColumn, $mapUser, $mapArtwork, $mapCreated])
->orderBy($idColumn)
->chunkById($chunk, function ($rows) use (&$attempted, &$inserted, $mapUser, $mapArtwork, $mapCreated) {
$batch = [];
foreach ($rows as $r) {
$attempted++;
$batch[] = [
'user_id' => $r->{$mapUser},
'artwork_id' => $r->{$mapArtwork},
'created_at' => $r->{$mapCreated} ?? now(),
];
}
if (count($batch) > 0) {
$res = DB::table('user_favorites')->insertOrIgnore($batch);
// insertOrIgnore may return number inserted on some drivers; approximate otherwise
if (is_int($res)) {
$inserted += $res;
} else {
$inserted += count($batch);
}
}
$this->info("Processed {$attempted} rows, approx inserted {$inserted}");
});
} catch (\Throwable $e) {
$this->warn('chunkById failed, falling back to cursor: '.$e->getMessage());
$cursor = $legacy->table($table)
->select([$mapUser, $mapArtwork, $mapCreated])
->orderBy($mapCreated)
->cursor();
$batch = [];
foreach ($cursor as $r) {
$attempted++;
$batch[] = [
'user_id' => $r->{$mapUser},
'artwork_id' => $r->{$mapArtwork},
'created_at' => $r->{$mapCreated} ?? now(),
];
if (count($batch) >= $chunk) {
$res = DB::table('user_favorites')->insertOrIgnore($batch);
if (is_int($res)) {
$inserted += $res;
} else {
$inserted += count($batch);
}
$this->info("Processed {$attempted} rows, approx inserted {$inserted}");
$batch = [];
}
}
if (count($batch) > 0) {
$res = DB::table('user_favorites')->insertOrIgnore($batch);
if (is_int($res)) {
$inserted += $res;
} else {
$inserted += count($batch);
}
}
}
$this->info("Import complete. Attempted {$attempted}, approx inserted {$inserted}.");
return 0;
}
}

View File

@@ -25,17 +25,15 @@ class ImportLegacyUsers extends Command
$imported = 0;
$skipped = 0;
if (! DB::connection('legacy')->getPdo()) {
if (! DB::getPdo()) {
$this->error('Legacy DB connection "legacy" is not configured or reachable.');
return self::FAILURE;
}
DB::connection('legacy')
->table('users')
DB::table('users')
->chunkById($chunk, function ($rows) use (&$imported, &$skipped) {
$ids = $rows->pluck('user_id')->all();
$stats = DB::connection('legacy')
->table('users_statistics')
$stats = DB::table('users_statistics')
->whereIn('user_id', $ids)
->get()
->keyBy('user_id');

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class ImportWallzCategories extends Command
{
/**
* The name and signature of the console command.
*
* --connection: database connection name for legacy DB (default: legacy)
* --table: legacy table name (default: wallz)
* --chunk: rows per chunk (default: 500)
*/
protected $signature = 'import:wallz-categories {--connection=legacy} {--table=wallz} {--chunk=500}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import artwork -> category mappings from legacy wallz table into artwork_category';
public function handle()
{
$conn = $this->option('connection');
$table = $this->option('table');
$chunk = (int) $this->option('chunk');
$this->info("Importing from connection=[{$conn}] table=[{$table}]");
// Validate connection exists in config
if (! config()->has('database.connections.' . $conn)) {
$this->error("Database connection '{$conn}' not configured. Available connections: " . implode(', ', array_keys(config('database.connections'))));
return 1;
}
// Check legacy table exists
try {
if (! Schema::connection($conn)->hasTable($table)) {
$this->error("Table '{$table}' does not exist on connection '{$conn}'.");
return 1;
}
} catch (\Throwable $e) {
$this->error('Error checking legacy table: ' . $e->getMessage());
return 1;
}
$total = DB::connection($conn)->table($table)->count();
$this->info("Found {$total} rows in legacy table.");
$bar = $this->output->createProgressBar($total ?: 0);
$bar->start();
try {
DB::connection($conn)->table($table)
->select('id', 'category')
->orderBy('id')
->chunk($chunk, function ($rows) use ($bar) {
$inserts = [];
foreach ($rows as $r) {
// Skip empty category
if (empty($r->category)) continue;
$inserts[] = [
'artwork_id' => (int) $r->id,
'category_id' => (int) $r->category,
];
}
if (! empty($inserts)) {
// Use insertOrIgnore to avoid duplicates
DB::table('artwork_category')->insertOrIgnore($inserts);
}
$bar->advance(count($rows));
});
$bar->finish();
$this->newLine(2);
$this->info('Import complete.');
return 0;
} catch (\Throwable $e) {
$bar->finish();
$this->newLine(2);
$this->error('Import failed: ' . $e->getMessage());
return 1;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ArtworkController extends Controller
{
public function index(Request $request)
{
abort(404);
}
public function create(Request $request)
{
abort(404);
}
public function store(Request $request)
{
abort(404);
}
public function edit(Request $request, int $id)
{
abort(404);
}
public function update(Request $request, int $id)
{
abort(404);
}
public function destroy(Request $request, int $id)
{
abort(404);
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\ContentType;
use App\Models\Artwork;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -16,6 +17,7 @@ class CategoryPageController extends Controller
abort(404);
}
if ($categoryPath === null || $categoryPath === '') {
// No category path: show content-type landing page (e.g., /wallpapers)
$rootCategories = $contentType->rootCategories()->orderBy('sort_order')->orderBy('name')->get();
@@ -41,6 +43,7 @@ class CategoryPageController extends Controller
->where('slug', strtolower(array_shift($segments)))
->first();
if (! $current) {
abort(404);
}
@@ -56,12 +59,28 @@ class CategoryPageController extends Controller
$subcategories = $category->children()->orderBy('sort_order')->orderBy('name')->get();
$rootCategories = $contentType->rootCategories()->orderBy('sort_order')->orderBy('name')->get();
// Placeholder artworks paginator (until artwork data is wired).
$page = max(1, (int) $request->query('page', 1));
$artworks = new LengthAwarePaginator([], 0, 40, $page, [
'path' => $request->url(),
'query' => $request->query(),
]);
// Collect category ids for the category + all descendants recursively
$collected = [];
$gather = function (Category $cat) use (&$gather, &$collected) {
$collected[] = $cat->id;
foreach ($cat->children as $child) {
$gather($child);
}
};
// Ensure children relation is loaded to avoid N+1 recursion
$category->load('children');
$gather($category);
// Load artworks that are attached to any of these categories
$query = Artwork::whereHas('categories', function ($q) use ($collected) {
$q->whereIn('categories.id', $collected);
})->published()->public();
// Paginate results
$perPage = 40;
$artworks = $query->orderBy('published_at', 'desc')
->paginate($perPage)
->withQueryString();
$page_title = $category->name;
$page_meta_description = $category->description ?? ($contentType->name . ' artworks on Skinbase');

View File

@@ -2,7 +2,12 @@
namespace App\Http\Controllers;
abstract class Controller
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
abstract class Controller extends BaseController
{
//
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}

View File

@@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Dashboard;
use App\Http\Controllers\Controller;
use App\Http\Requests\Dashboard\UpdateArtworkRequest;
use App\Models\Artwork;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\View\View;
class ArtworkController extends Controller
{
public function index(Request $request): View
{
$artworks = $request->user()
->artworks()
->latest()
->paginate(20);
return view('artworks.index', [
'artworks' => $artworks,
'page_title' => 'My Artworks',
]);
}
public function edit(Request $request, int $id): View
{
$artwork = $request->user()->artworks()->whereKey($id)->firstOrFail();
$this->authorize('update', $artwork);
return view('artworks.edit', [
'artwork' => $artwork,
'page_title' => 'Edit Artwork',
]);
}
public function update(UpdateArtworkRequest $request, int $id): RedirectResponse
{
$artwork = $request->user()->artworks()->whereKey($id)->firstOrFail();
$this->authorize('update', $artwork);
$data = $request->validated();
$artwork->title = $data['title'];
$artwork->description = $data['description'] ?? null;
if ($request->hasFile('file')) {
$file = $request->file('file');
// Remove prior stored file if it's on the public disk.
if (! empty($artwork->file_path) && Storage::disk('public')->exists($artwork->file_path)) {
Storage::disk('public')->delete($artwork->file_path);
}
$size = $file->getSize() ?? 0;
$mime = $file->getMimeType() ?? 'application/octet-stream';
$dimensions = @getimagesize($file->getRealPath());
$width = is_array($dimensions) ? (int) ($dimensions[0] ?? 0) : 0;
$height = is_array($dimensions) ? (int) ($dimensions[1] ?? 0) : 0;
$path = $file->storePublicly('artworks', 'public');
$artwork->file_name = $file->getClientOriginalName();
$artwork->file_path = $path;
$artwork->file_size = (int) $size;
$artwork->mime_type = (string) $mime;
$artwork->width = max(1, $width);
$artwork->height = max(1, $height);
// If a file is replaced, clear CDN-derived fields unless a separate pipeline repopulates them.
$artwork->hash = null;
$artwork->thumb_ext = null;
$artwork->file_ext = $file->guessExtension() ?: $file->getClientOriginalExtension() ?: null;
}
$artwork->save();
return redirect()
->route('dashboard.artworks.edit', $artwork->id)
->with('status', 'Artwork updated.');
}
public function destroy(Request $request, int $id): RedirectResponse
{
$artwork = $request->user()->artworks()->whereKey($id)->firstOrFail();
$this->authorize('delete', $artwork);
// Best-effort remove stored file.
if (! empty($artwork->file_path) && Storage::disk('public')->exists($artwork->file_path)) {
Storage::disk('public')->delete($artwork->file_path);
}
$artwork->delete();
return redirect()
->route('dashboard.artworks.index')
->with('status', 'Artwork deleted.');
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Models\Artwork;
use App\Models\User;
use Illuminate\Support\Facades\Schema;
class GalleryController extends Controller
{
public function show(Request $request, $userId, $username = null)
{
$user = User::find((int)$userId);
if (! $user) {
abort(404);
}
$page = max(1, (int) $request->query('page', 1));
$hits = 20;
$query = Artwork::where('user_id', $user->id)
->approved()
->published()
->public()
->orderByDesc('published_at');
$total = (int) $query->count();
$artworks = $query->skip(($page - 1) * $hits)->take($hits)->get();
return view('legacy.gallery', [
'user' => $user,
'artworks' => $artworks,
'page' => $page,
'hits' => $hits,
'total' => $total,
]);
}
}

View File

@@ -21,7 +21,7 @@ class ArtController extends Controller
if ($request->isMethod('post') && $request->input('action') === 'store_comment') {
if (auth()->check()) {
try {
\Illuminate\Support\Facades\DB::connection('legacy')->table('artworks_comments')->insert([
DB::table('artwork_comments')->insert([
'artwork_id' => (int)$id,
'owner_user_id' => (int)($request->user()->id ?? 0),
'user_id' => (int)$request->user()->id,
@@ -44,7 +44,7 @@ class ArtController extends Controller
// load comments for artwork (legacy schema)
try {
$comments = \Illuminate\Support\Facades\DB::connection('legacy')->table('artworks_comments as t1')
$comments = DB::table('artwork_comments as t1')
->rightJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->select('t1.description', 't1.date', 't1.time', 't2.uname', 't2.signature', 't2.icon', 't2.user_id')
->where('t1.artwork_id', (int)$id)

View File

@@ -17,7 +17,7 @@ class AvatarController extends Controller
$defaultAvatar = public_path('gfx/avatar.jpg');
try {
$icon = DB::connection('legacy')->table('users')->where('user_id', $user_id)->value('icon');
$icon = DB::table('users')->where('user_id', $user_id)->value('icon');
} catch (\Throwable $e) {
$icon = null;
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class BuddiesController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (! $user) {
return redirect()->route('login');
}
$perPage = 50;
try {
$query = DB::table('friends_list as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.id')
->leftJoin('user_profiles as p', 'p.user_id', '=', 't2.id')
->where('t1.friend_id', $user->id)
->select('t1.id', 't1.user_id', 't1.friend_id', 't2.name as uname', 'p.avatar as icon', 't1.date_added')
->orderByDesc('t1.date_added');
$followers = $query->paginate($perPage)->withQueryString();
} catch (\Throwable $e) {
$followers = collect();
}
$page_title = ($user->name ?? $user->username ?? 'User') . ': Followers';
return view('legacy.buddies', compact('followers', 'page_title'));
}
}

View File

@@ -38,7 +38,7 @@ class ChatController extends Controller
// Load smileys from legacy DB
try {
$smileys = DB::connection('legacy')->table('smileys')->select('code', 'picture', 'emotion')->get();
$smileys = DB::table('smileys')->select('code', 'picture', 'emotion')->get();
} catch (\Throwable $e) {
$smileys = collect();
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use App\Models\UserFavorite;
class FavouritesController extends Controller
{
public function index(Request $request, $userId = null, $username = null)
{
$userId = $userId ? (int)$userId : ($request->user()->id ?? null);
$page = max(1, (int) $request->query('page', 1));
$hits = 20;
$start = ($page - 1) * $hits;
$total = 0;
$results = collect();
// Prefer Eloquent model if table exists
try {
$schema = DB::getSchemaBuilder();
} catch (\Throwable $e) {
$schema = null;
}
// Detect user id and name columns for compatibility with legacy schema
$userIdCol = Schema::hasColumn('users', 'user_id') ? 'user_id' : 'id';
$userNameCol = null;
foreach (['uname', 'username', 'name'] as $col) {
if (Schema::hasColumn('users', $col)) {
$userNameCol = $col;
break;
}
}
if ($schema && $schema->hasTable('user_favorites') && class_exists(UserFavorite::class)) {
try {
$query = UserFavorite::with(['artwork.user'])
->where('user_id', $userId)
->orderByDesc('created_at')
->orderByDesc('artwork_id');
$total = (int) $query->count();
$favorites = $query->skip($start)->take($hits)->get();
$results = $favorites->map(function ($fav) use ($userNameCol) {
$art = $fav->artwork;
if (! $art) {
return null;
}
$item = (object) $art->toArray();
$item->uname = ($userNameCol && isset($art->user)) ? ($art->user->{$userNameCol} ?? null) : null;
$item->datum = $fav->created_at;
return $item;
})->filter();
} catch (\Throwable $e) {
$total = 0;
$results = collect();
}
} else {
// Fallback to legacy tables
try {
if ($schema && $schema->hasTable('artworks_favourites')) {
$favTable = 'artworks_favourites';
} elseif ($schema && $schema->hasTable('favourites')) {
$favTable = 'favourites';
} else {
$favTable = null;
}
if ($schema && $schema->hasTable('artworks')) {
$artTable = 'artworks';
} elseif ($schema && $schema->hasTable('wallz')) {
$artTable = 'wallz';
} else {
$artTable = null;
}
} catch (\Throwable $e) {
$favTable = null;
$artTable = null;
}
if ($favTable && $artTable) {
try {
$total = (int) DB::table($favTable)->where('user_id', $userId)->count();
$t2JoinCol = 't2.' . $userIdCol;
$t2NameSelect = $userNameCol ? DB::raw("t2.{$userNameCol} as uname") : DB::raw("'' as uname");
$results = DB::table($favTable . ' as t1')
->rightJoin($artTable . ' as t3', 't1.artwork_id', '=', 't3.id')
->leftJoin('users as t2', 't3.user_id', '=', $t2JoinCol)
->where('t1.user_id', $userId)
->select('t3.*', $t2NameSelect, 't1.datum')
->orderByDesc('t1.datum')
->orderByDesc('t1.artwork_id')
->skip($start)
->take($hits)
->get();
} catch (\Throwable $e) {
$total = 0;
$results = collect();
}
}
}
$results = collect($results)->filter()->values()->transform(function ($row) {
$row->name = $row->name ?? '';
$row->slug = $row->slug ?? Str::slug($row->name);
$row->encoded = isset($row->id) ? app(\App\Helpers\Thumb::class)::encodeId((int)$row->id) : null;
return $row;
});
$page_title = ($username ?: ($userNameCol ? DB::table('users')->where($userIdCol, $userId)->value($userNameCol) : '')) . ' Favourites';
return view('legacy.favourites', [
'results' => $results,
'page_title' => $page_title,
'user_id' => $userId,
'page' => $page,
'hits' => $hits,
'total' => $total,
]);
}
public function destroy(Request $request, $userId, $artworkId)
{
$auth = $request->user();
if (! $auth || $auth->id != (int)$userId) {
abort(403);
}
$favTable = Schema::hasTable('user_favorites') ? 'user_favorites' : (Schema::hasTable('artworks_favourites') ? 'artworks_favourites' : 'favourites');
DB::table($favTable)->where('user_id', (int)$userId)->where('artwork_id', (int)$artworkId)->delete();
return redirect()->route('legacy.favourites', ['id' => $userId])->with('status', 'Removed from favourites');
}
}

View File

@@ -21,19 +21,19 @@ class InterviewController extends Controller
$interviewId = (int) $request->input('interview_id');
try {
DB::connection('legacy')->table('interviews_comment')->insert([
DB::table('interviews_comment')->insert([
'nid' => $interviewId,
'author' => $_SESSION['web_login']['username'] ?? 'Anonymous',
'datum' => DB::raw('CURRENT_TIMESTAMP'),
'tekst' => $tekst,
]);
$ar2 = DB::connection('legacy')->table('users')
$ar2 = DB::table('users')
->where('uname', $_SESSION['web_login']['username'])
->first();
if (!empty($ar2->user_id)) {
DB::connection('legacy')->table('users_statistics')
DB::table('users_statistics')
->where('user_id', $ar2->user_id)
->increment('newscomment');
}
@@ -44,7 +44,7 @@ class InterviewController extends Controller
}
try {
$ar = DB::connection('legacy')->table('interviews')->where('id', $id)->first();
$ar = DB::table('interviews')->where('id', $id)->first();
} catch (\Throwable $e) {
$ar = null;
}
@@ -54,7 +54,7 @@ class InterviewController extends Controller
}
try {
$artworks = DB::connection('legacy')->table('wallz')
$artworks = DB::table('wallz')
->where('uname', $ar->username)
->inRandomOrder()
->limit(2)
@@ -64,7 +64,7 @@ class InterviewController extends Controller
}
try {
$comments = DB::connection('legacy')->table('interviews_comment as c')
$comments = DB::table('interviews_comment as c')
->leftJoin('users as u', 'u.uname', '=', 'c.author')
->where('c.nid', $id)
->select('c.*', 'u.user_id', 'u.user_type', 'u.signature', 'u.icon')
@@ -79,7 +79,7 @@ class InterviewController extends Controller
$postCounts = [];
if (!empty($authors)) {
try {
$counts = DB::connection('legacy')->table('interviews_comment')
$counts = DB::table('interviews_comment')
->select('author', DB::raw('COUNT(*) as cnt'))
->whereIn('author', $authors)
->groupBy('author')

View File

@@ -11,7 +11,7 @@ class InterviewsController extends Controller
public function index(Request $request)
{
try {
$interviews = DB::connection('legacy')->table('interviews AS t1')
$interviews = DB::table('interviews AS t1')
->select('t1.id', 't1.headline', 't2.user_id', 't2.uname', 't2.icon')
->leftJoin('users AS t2', 't1.username', '=', 't2.uname')
->orderByDesc('t1.datum')

View File

@@ -13,7 +13,7 @@ class MonthlyCommentatorsController extends Controller
$hits = 30;
$page = max(1, (int) $request->query('page', 1));
$query = DB::connection('legacy')->table('artworks_comments as t1')
$query = DB::table('artwork_comments as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->leftJoin('country as c', 't2.country', '=', 'c.id')
->where('t1.user_id', '>', 0)

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
class MyBuddiesController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (! $user) {
return redirect()->route('login');
}
$perPage = 50;
try {
$query = DB::table('friends_list as t1')
->leftJoin('users as t2', 't1.friend_id', '=', 't2.id')
->leftJoin('user_profiles as p', 'p.user_id', '=', 't2.id')
->where('t1.user_id', $user->id)
->select('t1.id', 't1.friend_id', 't1.user_id', 't2.name as uname', 'p.avatar as icon', 't1.date_added')
->orderByDesc('t1.date_added');
$buddies = $query->paginate($perPage)->withQueryString();
} catch (\Throwable $e) {
$buddies = collect();
}
$page_title = ($user->name ?? $user->username ?? 'User') . ': Following List';
return view('legacy.mybuddies', compact('buddies', 'page_title'));
}
public function destroy(Request $request, $id)
{
$user = $request->user();
if (! $user) {
abort(403);
}
try {
$deleted = DB::table('friends_list')->where('id', $id)->where('user_id', $user->id)->delete();
if ($deleted) {
$request->session()->flash('status', 'Removed from following list.');
}
} catch (\Throwable $e) {
$request->session()->flash('error', 'Could not remove buddy.');
}
return redirect()->route('legacy.mybuddies');
}
}

View File

@@ -13,7 +13,7 @@ class NewsController extends Controller
$id = (int) $id;
try {
$news = DB::connection('legacy')->table('news as t1')
$news = DB::table('news as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->where('t1.news_id', $id)
->select('t1.*', 't2.uname', 't2.user_type', 't2.signature', 't2.icon')
@@ -27,7 +27,7 @@ class NewsController extends Controller
}
try {
$comments = DB::connection('legacy')->table('news_comment as c')
$comments = DB::table('news_comment as c')
->leftJoin('users as u', 'c.user_id', '=', 'u.user_id')
->where('c.news_id', $id)
->select('c.posted', 'c.message', 'c.user_id', 'u.user_type', 'u.signature', 'u.icon', 'u.uname')

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Services\ArtworkService;
use App\Models\ContentType;
class PhotographyController extends Controller
{
protected ArtworkService $artworks;
public function __construct(ArtworkService $artworks)
{
$this->artworks = $artworks;
}
public function index(Request $request)
{
// Legacy group mapping: Photography => id 3
$group = 'Photography';
$id = 3;
// Fetch legacy category info if available
$category = null;
try {
if (Schema::hasTable('artworks_categories')) {
$category = DB::table('artworks_categories')
->select('category_name', 'rootid', 'section_id', 'description', 'category_id')
->where('category_id', $id)
->first();
}
} catch (\Throwable $e) {
$category = null;
}
$page_title = $category->category_name ?? 'Photography';
$tidy = $category->description ?? null;
$perPage = 40;
// Use ArtworkService to get artworks for the content type 'photography'
try {
$artworks = $this->artworks->getArtworksByContentType('photography', $perPage);
} catch (\Throwable $e) {
$artworks = collect();
}
// Load subcategories (legacy) if available
$subcategories = collect();
try {
if (Schema::hasTable('artworks_categories')) {
$subcategories = DB::table('artworks_categories')->select('category_id','category_name')->where('rootid', $id)->orderBy('category_name')->get();
if ($subcategories->count() == 0 && !empty($category->rootid)) {
$subcategories = DB::table('artworks_categories')->select('category_id','category_name')->where('rootid', $category->rootid)->orderBy('category_name')->get();
}
}
} catch (\Throwable $e) {
$subcategories = collect();
}
// Fallback to authoritative categories table when legacy table is missing/empty
if (! $subcategories || $subcategories->count() === 0) {
$ct = ContentType::where('slug', 'photography')->first();
if ($ct) {
$subcategories = $ct->rootCategories()
->orderBy('sort_order')
->orderBy('name')
->get()
->map(fn ($c) => (object) ['category_id' => $c->id, 'category_name' => $c->name]);
} else {
$subcategories = collect();
}
}
return view('legacy.photography', compact('page_title','tidy','group','artworks','subcategories','id'));
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\ArtworkComment;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Carbon;
class ReceivedCommentsController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (! $user) {
abort(403);
}
$hits = 33;
$page = max(1, (int) $request->query('page', 1));
$base = ArtworkComment::with(['user', 'artwork'])
->whereHas('artwork', function ($q) use ($user) {
$q->where('user_id', $user->id)->where('is_approved', true);
})
->orderByDesc('created_at');
$comments = $base->paginate($hits);
return view('legacy.received-comments', [
'comments' => $comments,
'page' => $page,
'hits' => $hits,
'total' => $comments->total(),
]);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use App\Services\ThumbnailPresenter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
class StatisticsController extends Controller
{
public function index(Request $request): View
{
$userId = $request->user()->id;
$sort = (string) $request->query('sort', 'date');
$allowed = ['date', 'name', 'dls', 'category', 'comments'];
if (! in_array($sort, $allowed, true)) {
$sort = 'date';
}
$categorySub = DB::table('artwork_category as ac')
->join('categories as c', 'ac.category_id', '=', 'c.id')
->select('ac.artwork_id', DB::raw('MIN(c.name) as category_name'))
->groupBy('ac.artwork_id');
$query = DB::table('artworks as a')
->leftJoinSub($categorySub, 'cat', function ($join) {
$join->on('a.id', '=', 'cat.artwork_id');
})
->where('a.user_id', $userId)
->select([
'a.*',
DB::raw('cat.category_name as category_name'),
])
->selectRaw('(SELECT COUNT(*) FROM artwork_comments WHERE artwork_id = a.id) AS num_comments');
if ($sort === 'name') {
$query->orderBy('a.name', 'asc');
} elseif ($sort === 'dls') {
$query->orderByDesc('a.dls');
} elseif ($sort === 'category') {
$query->orderBy('cat.category_name', 'asc');
} elseif ($sort === 'comments') {
$query->orderByDesc('num_comments');
} else {
$query->orderByDesc('a.published_at')->orderByDesc('a.id');
}
$artworks = $query->paginate(20)->appends(['sort' => $sort]);
$artworks->getCollection()->transform(function ($row) {
$thumb = ThumbnailPresenter::present($row, 'sm');
$row->thumb_url = $thumb['url'] ?? '';
$row->thumb_srcset = $thumb['srcset'] ?? null;
return $row;
});
return view('legacy.statistics', [
'artworks' => $artworks,
'sort' => $sort,
'page_title' => 'Artwork Statistics',
]);
}
}

View File

@@ -13,13 +13,13 @@ class TodayInHistoryController extends Controller
$hits = 39;
try {
$base = DB::connection('legacy')->table('featured_works as t0')
$base = DB::table('featured_works as t0')
->leftJoin('artworks as t1', 't0.artwork_id', '=', 't1.id')
->join('artworks_categories as t2', 't1.category', '=', 't2.category_id')
->join('categories as t2', 't1.category', '=', 't2.id')
->where('t1.approved', 1)
->whereRaw('MONTH(t0.post_date) = MONTH(CURRENT_DATE())')
->whereRaw('DAY(t0.post_date) = DAY(CURRENT_DATE())')
->select('t1.id', 't1.name', 't1.picture', 't1.uname', 't1.category', 't2.category_name');
->select('t1.id', 't1.name', 't1.picture', 't1.uname', 't1.category', DB::raw('t2.name as category_name'));
$artworks = $base->orderBy('t0.post_date','desc')->paginate($hits);
} catch (\Throwable $e) {

View File

@@ -58,72 +58,3 @@ class TopAuthorsController extends Controller
return view('legacy.top-authors', compact('page_title', 'authors', 'metric'));
}
}
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TopAuthorsController extends Controller
{
public function index(Request $request)
{
// Top users (most active)
try {
$topUsers = DB::connection('legacy')->table('wallz as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->select('t2.user_id', 't2.uname', 't2.icon', DB::raw('SUM(t1.dls) AS total_downloads'), DB::raw('COUNT(*) AS uploads'))
->groupBy('t1.user_id')
->orderByDesc('total_downloads')
->limit(23)
->get();
} catch (\Throwable $e) {
$topUsers = collect();
}
// Top followers
try {
$topFollowers = DB::connection('legacy')->table('friends_list as t1')
->rightJoin('users as t2', 't1.friend_id', '=', 't2.user_id')
->where('t1.friend_id', '>', 0)
->select('t2.uname', 't2.user_id', DB::raw('COUNT(*) as num'))
->groupBy('t1.friend_id')
->orderByDesc('num')
->limit(10)
->get();
} catch (\Throwable $e) {
$topFollowers = collect();
}
// Top commentators
try {
$topCommentators = DB::connection('legacy')->table('artworks_comments as t1')
->join('users as t2', 't1.user_id', '=', 't2.user_id')
->where('t1.user_id', '>', 0)
->select('t2.user_id','t2.uname','t2.user_type','t2.country', DB::raw('COUNT(*) as num_comments'))
->groupBy('t1.user_id')
->orderByDesc('num_comments')
->limit(10)
->get();
// enrich with country info if available
$topCommentators->transform(function ($c) {
if (!empty($c->country)) {
$cn = DB::connection('legacy')->table('country')->select('name','flag')->where('id', $c->country)->first();
$c->country_name = $cn->name ?? null;
$c->country_flag = $cn->flag ?? null;
} else {
$c->country_name = null;
$c->country_flag = null;
}
return $c;
});
} catch (\Throwable $e) {
$topCommentators = collect();
}
return view('legacy.top-authors', compact('topUsers', 'topFollowers', 'topCommentators'));
}
}

View File

@@ -15,7 +15,7 @@ class TopFavouritesController extends Controller
$hits = 21;
$page = max(1, (int) $request->query('page', 1));
$base = DB::connection('legacy')->table('artworks_favourites as t1')
$base = DB::table('artworks_favourites as t1')
->rightJoin('wallz as t2', 't1.artwork_id', '=', 't2.id')
->where('t2.approved', 1)
->select('t2.id', 't2.name', 't2.picture', 't2.category', DB::raw('COUNT(*) as num'))

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers\Legacy;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use App\Models\User;
use Carbon\Carbon;
class UserController extends Controller
{
public function index(Request $request)
{
$user = $request->user();
if (! $user) {
abort(403);
}
// Handle profile update
if ($request->isMethod('post')) {
$action = $request->input('confirm');
if ($action === 'true_password') {
$old = $request->input('oldpass');
$new = $request->input('newpass');
$new2 = $request->input('newpass2');
if ($new && $new === $new2 && Hash::check($old, $user->password)) {
$user->password = Hash::make($new);
$user->save();
$request->session()->flash('status', 'Password changed.');
} else {
$request->session()->flash('error', 'Password not changed.');
}
} else {
$data = $request->only(['real_name','web','country_code','signature','description','about_me']);
$user->real_name = $data['real_name'] ?? $user->real_name;
$user->web = $data['web'] ?? $user->web;
$user->country_code = $data['country_code'] ?? $user->country_code;
$user->signature = $data['signature'] ?? $user->signature;
$user->description = $data['description'] ?? $user->description;
$user->about_me = $data['about_me'] ?? $user->about_me;
$d1 = $request->input('date1');
$d2 = $request->input('date2');
$d3 = $request->input('date3');
if ($d1 && $d2 && $d3) {
$user->birth = sprintf('%04d-%02d-%02d', (int)$d3, (int)$d2, (int)$d1);
}
$user->gender = $request->input('gender', $user->gender);
$user->mlist = $request->has('newsletter') ? 1 : 0;
$user->friend_upload_notice = $request->has('friend_upload_notice') ? 1 : 0;
if ($request->hasFile('avatar')) {
$f = $request->file('avatar');
$name = $user->id . '.' . $f->getClientOriginalExtension();
$f->move(public_path('avatar'), $name);
$user->icon = $name;
}
if ($request->hasFile('personal_picture')) {
$f = $request->file('personal_picture');
$name = $user->id . '.' . $f->getClientOriginalExtension();
$f->move(public_path('user-picture'), $name);
$user->picture = $name;
}
if ($request->hasFile('emotion_icon')) {
$f = $request->file('emotion_icon');
$name = $user->id . '.' . $f->getClientOriginalExtension();
$f->move(public_path('emotion'), $name);
$user->eicon = $name;
}
$user->save();
$request->session()->flash('status', 'Profile updated.');
}
}
// Prepare birth date parts for the legacy form
$birthDay = null;
$birthMonth = null;
$birthYear = null;
if (! empty($user->birth)) {
try {
$dt = Carbon::parse($user->birth);
$birthDay = $dt->format('d');
$birthMonth = $dt->format('m');
$birthYear = $dt->format('Y');
} catch (\Throwable $e) {
// ignore parse errors
}
}
// Load country list if available (legacy table names)
$countries = collect();
try {
if (Schema::hasTable('country_list')) {
$countries = DB::table('country_list')->orderBy('country_name')->get();
} elseif (Schema::hasTable('countries')) {
$countries = DB::table('countries')->orderBy('name')->get();
}
} catch (\Throwable $e) {
$countries = collect();
}
return view('legacy.user', [
'user' => $user,
'birthDay' => $birthDay,
'birthMonth' => $birthMonth,
'birthYear' => $birthYear,
'countries' => $countries,
]);
}
}

View File

@@ -44,10 +44,10 @@ class LegacyController extends Controller
$perPage = 50;
try {
$artworks = DB::connection('legacy')->table('wallz as w')
->leftJoin('artworks_categories as c', 'w.category', '=', 'c.category_id')
$artworks = DB::table('wallz as w')
->leftJoin('categories as c', 'w.category', '=', 'c.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select('w.id', 'w.name', 'w.picture', 'w.category', 'w.datum', 'c.category_name', 'u.uname')
->select('w.id', 'w.name', 'w.picture', 'w.category', 'w.datum', DB::raw('c.name as category_name'), 'u.uname')
->where('w.approved', 1)
->where('w.public', 'Y')
->orderByDesc('w.datum')
@@ -101,10 +101,10 @@ class LegacyController extends Controller
$page_meta_description = $group . ' artworks on Skinbase';
$page_meta_keywords = strtolower($group) . ', skinbase, artworks, wallpapers, photography, skins';
try {
$category = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'description', 'rootid', 'section_id')
->where('category_id', $id)
try {
$category = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'description', DB::raw('parent_id as rootid'), DB::raw('content_type_id as section_id'))
->where('id', $id)
->first();
} catch (\Throwable $e) {
$category = null;
@@ -117,9 +117,9 @@ class LegacyController extends Controller
$perPage = 40;
try {
$base = DB::connection('legacy')->table('wallz as t1')
->select('t1.id', 't1.name', 't1.picture', 't3.uname', 't1.category', 't2.category_name')
->join('artworks_categories as t2', 't1.category', '=', 't2.category_id')
$base = DB::table('wallz as t1')
->select('t1.id', 't1.name', 't1.picture', 't3.uname', 't1.category', DB::raw('t2.name as category_name'))
->join('categories as t2', 't1.category', '=', 't2.id')
->leftJoin('users as t3', 't1.user_id', '=', 't3.user_id')
->where('t1.approved', 1)
->where(function ($q) use ($id, $category) {
@@ -139,22 +139,22 @@ class LegacyController extends Controller
}
try {
$subcategories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name')
->where('rootid', $id)
$subcategories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'))
->where('parent_id', $id)
->orderBy('category_name')
->get();
if ($subcategories->isEmpty() && $category->rootid) {
$subcategories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name')
->where('rootid', $category->rootid)
$subcategories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'))
->where('parent_id', $category->rootid)
->orderBy('category_name')
->get();
}
if ($subcategories->isEmpty()) {
$subcategories = DB::connection('legacy')->table('skupine')
$subcategories = DB::table('skupine')
->select('category_id', 'category_name')
->where('rootid', $id)
->orderBy('category_name')
@@ -162,7 +162,7 @@ class LegacyController extends Controller
}
} catch (\Throwable $e) {
try {
$subcategories = DB::connection('legacy')->table('skupine')
$subcategories = DB::table('skupine')
->select('category_id', 'category_name')
->where('rootid', $id)
->orderBy('category_name')
@@ -191,16 +191,16 @@ class LegacyController extends Controller
// Load top-level categories (section_id = 0 AND rootid = 0) like the legacy page
try {
$categories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
->orderBy('category_id')
$categories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'description')
->where('content_type_id', 0)
->where(DB::raw('parent_id'), 0)
->orderBy('id')
->get();
// Fallback to legacy table name if empty
if ($categories->isEmpty()) {
$categories = DB::connection('legacy')->table('skupine')
$categories = DB::table('skupine')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
@@ -209,7 +209,7 @@ class LegacyController extends Controller
}
} catch (\Throwable $e) {
try {
$categories = DB::connection('legacy')->table('skupine')
$categories = DB::table('skupine')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
@@ -225,15 +225,15 @@ class LegacyController extends Controller
if ($categories->isNotEmpty()) {
$ids = $categories->pluck('category_id')->unique()->values()->all();
try {
$subs = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'picture', 'section_id')
->whereIn('section_id', $ids)
$subs = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'image as picture', DB::raw('content_type_id as section_id'))
->whereIn('content_type_id', $ids)
->orderBy('category_name')
->get();
if ($subs->isEmpty()) {
// fallback to skupine table naming
$subs = DB::connection('legacy')->table('skupine')
$subs = DB::table('skupine')
->select('category_id', 'category_name', 'picture', 'section_id')
->whereIn('section_id', $ids)
->orderBy('category_name')
@@ -262,7 +262,7 @@ class LegacyController extends Controller
$page_meta_keywords = 'forum, discussions, topics, skinbase';
try {
$topics = DB::connection('legacy')->table('forum_topics as t')
$topics = DB::table('forum_topics as t')
->select(
't.topic_id',
't.topic',
@@ -292,7 +292,7 @@ class LegacyController extends Controller
public function forumTopic(Request $request, int $topic_id)
{
try {
$topic = DB::connection('legacy')->table('forum_topics')->where('topic_id', $topic_id)->first();
$topic = DB::table('forum_topics')->where('topic_id', $topic_id)->first();
} catch (\Throwable $e) {
$topic = null;
}
@@ -307,7 +307,7 @@ class LegacyController extends Controller
// Fetch subtopics; if none exist, fall back to posts (matches legacy behavior where some topics hold posts directly)
try {
$subtopics = DB::connection('legacy')->table('forum_topics as t')
$subtopics = DB::table('forum_topics as t')
->leftJoin('users as u', 't.user_id', '=', 'u.user_id')
->select(
't.topic_id',
@@ -348,7 +348,7 @@ class LegacyController extends Controller
]);
try {
$posts = DB::connection('legacy')->table('forum_posts as p')
$posts = DB::table('forum_posts as p')
->leftJoin('users as u', 'p.user_id', '=', 'u.user_id')
->select('p.id', 'p.message', 'p.post_date', 'u.uname', 'u.user_id', 'u.icon', 'u.eicon')
->where('p.topic_id', $topic->topic_id)
@@ -361,7 +361,7 @@ class LegacyController extends Controller
if ($posts->total() === 0) {
try {
$posts = DB::connection('legacy')->table('forum_posts as p')
$posts = DB::table('forum_posts as p')
->leftJoin('users as u', 'p.user_id', '=', 'u.user_id')
->select('p.id', 'p.message', 'p.post_date', 'u.uname', 'u.user_id', 'u.icon', 'u.eicon')
->where('p.tid', $topic->topic_id)
@@ -391,14 +391,14 @@ class LegacyController extends Controller
$memberFeatured = null;
try {
$featured = DB::connection('legacy')->table('featured_works as fw')
$featured = DB::table('featured_works as fw')
->leftJoin('wallz as w', 'fw.artwork_id', '=', 'w.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select('w.id', 'w.name', 'w.picture', 'u.uname', 'fw.post_date')
->orderByDesc('fw.post_date')
->first();
$memberFeatured = DB::connection('legacy')->table('users_opinions as o')
$memberFeatured = DB::table('users_opinions as o')
->leftJoin('wallz as w', 'o.artwork_id', '=', 'w.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select(DB::raw('COUNT(*) AS votes'), 'w.id', 'w.name', 'w.picture', 'u.uname')
@@ -437,7 +437,7 @@ class LegacyController extends Controller
private function forumNews(): array
{
try {
return DB::connection('legacy')->table('forum_topics as t1')
return DB::table('forum_topics as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->select(
't1.topic_id',
@@ -461,7 +461,7 @@ class LegacyController extends Controller
private function ourNews(): array
{
try {
return DB::connection('legacy')->table('news as t1')
return DB::table('news as t1')
->join('news_categories as t2', 't1.category_id', '=', 't2.category_id')
->join('users as t3', 't1.user_id', '=', 't3.user_id')
->select(
@@ -487,7 +487,7 @@ class LegacyController extends Controller
private function latestForumActivity(): array
{
try {
return DB::connection('legacy')->table('forum_topics as t1')
return DB::table('forum_topics as t1')
->select(
't1.topic_id',
't1.topic',
@@ -523,9 +523,9 @@ class LegacyController extends Controller
// Fallback to DB if cache missing
if (empty($uploads)) {
try {
$uploads = DB::connection('legacy')->table('wallz as w')
$uploads = DB::table('wallz as w')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->leftJoin('artworks_categories as c', 'w.category', '=', 'c.category_id')
->leftJoin('categories as c', 'w.category', '=', 'c.id')
->where('w.approved', 1)
->orderByDesc('w.datum')
->limit(20)
@@ -536,7 +536,7 @@ class LegacyController extends Controller
'name' => $row->name,
'picture' => $row->picture,
'uname' => $row->uname,
'category_name' => $row->category_name ?? '',
'category_name' => $row->category_name ?? $row->name ?? '',
];
})
->toArray();

View File

@@ -15,12 +15,19 @@ class ManageController extends Controller
$userId = $request->user()->id;
$perPage = 50;
// Use legacy connection query builder and join category name to avoid Eloquent model issues
$query = DB::connection('legacy')->table('artworks as a')
->leftJoin('artworks_categories as c', 'a.category', '=', 'c.category_id')
// Use default connection query builder and join category name to avoid Eloquent model issues
$categorySub = DB::table('artwork_category as ac')
->join('categories as c', 'ac.category_id', '=', 'c.id')
->select('ac.artwork_id', DB::raw('MIN(c.name) as category_name'))
->groupBy('ac.artwork_id');
$query = DB::table('artworks as a')
->leftJoinSub($categorySub, 'cat', function ($join) {
$join->on('a.id', '=', 'cat.artwork_id');
})
->where('a.user_id', $userId)
->select('a.*', 'c.category_name')
->orderByDesc('a.datum')
->select('a.*', DB::raw('cat.category_name as category_name'))
->orderByDesc('a.published_at')
->orderByDesc('a.id');
$artworks = $query->paginate($perPage);
@@ -34,12 +41,20 @@ class ManageController extends Controller
public function edit(Request $request, $id)
{
$userId = $request->user()->id;
$artwork = DB::connection('legacy')->table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
$artwork = DB::table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
if (! $artwork) {
abort(404);
}
$categories = DB::connection('legacy')->table('artworks_categories')->where('section_id', 0)->orderBy('category_id')->get();
// If artworks no longer have a single `category` column, fetch pivot selection
$selectedCategory = DB::table('artwork_category')->where('artwork_id', (int)$id)->value('category_id');
$artwork->category = $selectedCategory;
$categories = DB::table('categories')
->where('content_type_id', 0)
->orderBy('id')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'))
->get();
return view('manage.edit', [
'artwork' => $artwork,
@@ -51,7 +66,7 @@ class ManageController extends Controller
public function update(Request $request, $id)
{
$userId = $request->user()->id;
$existing = DB::connection('legacy')->table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
$existing = DB::table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
if (! $existing) {
abort(404);
@@ -66,7 +81,6 @@ class ManageController extends Controller
]);
$update = [
'name' => $data['name'],
'category' => $data['section'] ?? $existing->category,
'description' => $data['description'] ?? $existing->description,
'updated' => now(),
];
@@ -86,7 +100,16 @@ class ManageController extends Controller
$update['fname'] = basename($attPath);
}
DB::connection('legacy')->table('artworks')->where('id', (int)$id)->where('user_id', $userId)->update($update);
DB::table('artworks')->where('id', (int)$id)->where('user_id', $userId)->update($update);
// Update pivot: set single category selection for this artwork
if (isset($data['section'])) {
DB::table('artwork_category')->where('artwork_id', (int)$id)->delete();
DB::table('artwork_category')->insert([
'artwork_id' => (int)$id,
'category_id' => (int)$data['section'],
]);
}
return redirect()->route('manage')->with('status', 'Artwork was successfully updated.');
}
@@ -94,7 +117,7 @@ class ManageController extends Controller
public function destroy(Request $request, $id)
{
$userId = $request->user()->id;
$artwork = DB::connection('legacy')->table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
$artwork = DB::table('artworks')->where('id', (int)$id)->where('user_id', $userId)->first();
if (! $artwork) {
abort(404);
}
@@ -107,7 +130,7 @@ class ManageController extends Controller
Storage::delete('public/uploads/artworks/' . $artwork->picture);
}
DB::connection('legacy')->table('artworks')->where('id', (int)$id)->where('user_id', $userId)->delete();
DB::table('artworks')->where('id', (int)$id)->where('user_id', $userId)->delete();
return redirect()->route('manage')->with('status', 'Artwork deleted.');
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests\Dashboard;
use Illuminate\Foundation\Http\FormRequest;
class UpdateArtworkRequest extends FormRequest
{
public function authorize(): bool
{
// Authorization is enforced in the controller via ArtworkPolicy.
return true;
}
public function rules(): array
{
return [
'title' => 'required|string|max:150',
'description' => 'nullable|string',
// 100MB max (Laravel uses kilobytes)
'file' => 'nullable|image|max:102400',
];
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@@ -47,4 +48,9 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
public function artworks(): HasMany
{
return $this->hasMany(Artwork::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserFavorite extends Model
{
protected $table = 'user_favorites';
public $timestamps = false;
protected $fillable = [
'user_id',
'artwork_id',
'created_at',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function artwork(): BelongsTo
{
return $this->belongsTo(Artwork::class);
}
}

View File

@@ -24,15 +24,15 @@ class LegacyService
$featured = null;
$memberFeatured = null;
try {
$featured = DB::connection('legacy')->table('featured_works as fw')
try {
$featured = DB::table('featured_works as fw')
->leftJoin('wallz as w', 'fw.artwork_id', '=', 'w.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select('w.id', 'w.name', 'w.picture', 'u.uname', 'fw.post_date')
->orderByDesc('fw.post_date')
->first();
$memberFeatured = DB::connection('legacy')->table('users_opinions as o')
$memberFeatured = DB::table('users_opinions as o')
->leftJoin('wallz as w', 'o.artwork_id', '=', 'w.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select(DB::raw('COUNT(*) AS votes'), 'w.id', 'w.name', 'w.picture', 'u.uname')
@@ -80,9 +80,9 @@ class LegacyService
if (empty($uploads)) {
try {
$uploads = DB::connection('legacy')->table('wallz as w')
$uploads = DB::table('wallz as w')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->leftJoin('artworks_categories as c', 'w.category', '=', 'c.category_id')
->leftJoin('categories as c', 'w.category', '=', 'c.id')
->where('w.approved', 1)
->orderByDesc('w.datum')
->limit(20)
@@ -93,7 +93,7 @@ class LegacyService
'name' => $row->name,
'picture' => $row->picture,
'uname' => $row->uname,
'category_name' => $row->category_name ?? '',
'category_name' => $row->category_name ?? $row->name ?? '',
];
})
->toArray();
@@ -119,8 +119,8 @@ class LegacyService
public function forumNews(): array
{
try {
return DB::connection('legacy')->table('forum_topics as t1')
try {
return DB::table('forum_topics as t1')
->leftJoin('users as t2', 't1.user_id', '=', 't2.user_id')
->select(
't1.topic_id',
@@ -144,7 +144,7 @@ class LegacyService
public function ourNews(): array
{
try {
return DB::connection('legacy')->table('news as t1')
return DB::table('news as t1')
->join('news_categories as t2', 't1.category_id', '=', 't2.category_id')
->join('users as t3', 't1.user_id', '=', 't3.user_id')
->select(
@@ -170,7 +170,7 @@ class LegacyService
public function latestForumActivity(): array
{
try {
return DB::connection('legacy')->table('forum_topics as t1')
return DB::table('forum_topics as t1')
->select(
't1.topic_id',
't1.topic',
@@ -191,11 +191,11 @@ class LegacyService
public function browseGallery(int $perPage = 50)
{
try {
return DB::connection('legacy')->table('wallz as w')
->leftJoin('artworks_categories as c', 'w.category', '=', 'c.category_id')
try {
return DB::table('wallz as w')
->leftJoin('categories as c', 'w.category', '=', 'c.id')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->select('w.id', 'w.name', 'w.picture', 'w.category', 'w.datum', 'c.category_name', 'u.uname')
->select('w.id', 'w.name', 'w.picture', 'w.category', 'w.datum', DB::raw('c.name as category_name'), 'u.uname')
->where('w.approved', 1)
->where('w.public', 'Y')
->orderByDesc('w.datum')
@@ -226,9 +226,9 @@ class LegacyService
}
try {
$category = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'description', 'rootid', 'section_id')
->where('category_id', $id)
$category = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'description', DB::raw('parent_id as rootid'), DB::raw('content_type_id as section_id'))
->where('id', $id)
->first();
} catch (\Throwable $e) {
$category = null;
@@ -240,10 +240,10 @@ class LegacyService
$perPage = 40;
try {
$base = DB::connection('legacy')->table('wallz as t1')
->select('t1.id', 't1.name', 't1.picture', 't3.uname', 't1.category', 't2.category_name')
->join('artworks_categories as t2', 't1.category', '=', 't2.category_id')
try {
$base = DB::table('wallz as t1')
->select('t1.id', 't1.name', 't1.picture', 't3.uname', 't1.category', DB::raw('t2.name as category_name'))
->join('categories as t2', 't1.category', '=', 't2.id')
->leftJoin('users as t3', 't1.user_id', '=', 't3.user_id')
->where('t1.approved', 1)
->where(function ($q) use ($id, $category) {
@@ -289,23 +289,23 @@ class LegacyService
$artworks = null;
}
try {
$subcategories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name')
->where('rootid', $id)
try {
$subcategories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'))
->where('parent_id', $id)
->orderBy('category_name')
->get();
if ($subcategories->isEmpty() && $category->rootid) {
$subcategories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name')
->where('rootid', $category->rootid)
if ($subcategories->isEmpty() && $category->rootid) {
$subcategories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'))
->where('parent_id', $category->rootid)
->orderBy('category_name')
->get();
}
if ($subcategories->isEmpty()) {
$subcategories = DB::connection('legacy')->table('skupine')
if ($subcategories->isEmpty()) {
$subcategories = DB::table('skupine')
->select('category_id', 'category_name')
->where('rootid', $id)
->orderBy('category_name')
@@ -313,7 +313,7 @@ class LegacyService
}
} catch (\Throwable $e) {
try {
$subcategories = DB::connection('legacy')->table('skupine')
$subcategories = DB::table('skupine')
->select('category_id', 'category_name')
->where('rootid', $id)
->orderBy('category_name')
@@ -341,15 +341,15 @@ class LegacyService
public function browseCategories()
{
try {
$categories = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
->orderBy('category_id')
$categories = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'description')
->where('content_type_id', 0)
->where(DB::raw('parent_id'), 0)
->orderBy('id')
->get();
if ($categories->isEmpty()) {
$categories = DB::connection('legacy')->table('skupine')
$categories = DB::table('skupine')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
@@ -358,7 +358,7 @@ class LegacyService
}
} catch (\Throwable $e) {
try {
$categories = DB::connection('legacy')->table('skupine')
$categories = DB::table('skupine')
->select('category_id', 'category_name', 'description')
->where('section_id', 0)
->where('rootid', 0)
@@ -373,14 +373,14 @@ class LegacyService
if ($categories->isNotEmpty()) {
$ids = $categories->pluck('category_id')->unique()->values()->all();
try {
$subs = DB::connection('legacy')->table('artworks_categories')
->select('category_id', 'category_name', 'picture', 'section_id')
->whereIn('section_id', $ids)
$subs = DB::table('categories')
->select(DB::raw('id as category_id'), DB::raw('name as category_name'), 'image as picture', DB::raw('content_type_id as section_id'))
->whereIn('content_type_id', $ids)
->orderBy('category_name')
->get();
if ($subs->isEmpty()) {
$subs = DB::connection('legacy')->table('skupine')
$subs = DB::table('skupine')
->select('category_id', 'category_name', 'picture', 'section_id')
->whereIn('section_id', $ids)
->orderBy('category_name')
@@ -405,7 +405,7 @@ class LegacyService
public function forumIndex()
{
try {
$topics = DB::connection('legacy')->table('forum_topics as t')
$topics = DB::table('forum_topics as t')
->select(
't.topic_id',
't.topic',
@@ -435,7 +435,7 @@ class LegacyService
public function forumTopic(int $topic_id, int $page = 1)
{
try {
$topic = DB::connection('legacy')->table('forum_topics')->where('topic_id', $topic_id)->first();
$topic = DB::table('forum_topics')->where('topic_id', $topic_id)->first();
} catch (\Throwable $e) {
$topic = null;
}
@@ -445,7 +445,7 @@ class LegacyService
}
try {
$subtopics = DB::connection('legacy')->table('forum_topics as t')
$subtopics = DB::table('forum_topics as t')
->leftJoin('users as u', 't.user_id', '=', 'u.user_id')
->select(
't.topic_id',
@@ -478,7 +478,7 @@ class LegacyService
$sort = strtolower(request()->query('sort', 'desc')) === 'asc' ? 'asc' : 'desc';
try {
$posts = DB::connection('legacy')->table('forum_posts as p')
$posts = DB::table('forum_posts as p')
->leftJoin('users as u', 'p.user_id', '=', 'u.user_id')
->select('p.id', 'p.message', 'p.post_date', 'u.uname', 'u.user_id', 'u.icon', 'u.eicon')
->where('p.topic_id', $topic->topic_id)
@@ -491,7 +491,7 @@ class LegacyService
if (! $posts || $posts->total() === 0) {
try {
$posts = DB::connection('legacy')->table('forum_posts as p')
$posts = DB::table('forum_posts as p')
->leftJoin('users as u', 'p.user_id', '=', 'u.user_id')
->select('p.id', 'p.message', 'p.post_date', 'u.uname', 'u.user_id', 'u.icon', 'u.eicon')
->where('p.tid', $topic->topic_id)
@@ -529,10 +529,10 @@ class LegacyService
public function getArtwork(int $id)
{
try {
$row = DB::connection('legacy')->table('wallz as w')
$row = DB::table('wallz as w')
->leftJoin('users as u', 'w.user_id', '=', 'u.user_id')
->leftJoin('artworks_categories as c', 'w.category', '=', 'c.category_id')
->select('w.*', 'u.uname', 'c.category_name')
->leftJoin('categories as c', 'w.category', '=', 'c.id')
->select('w.*', 'u.uname', DB::raw('c.name as category_name'))
->where('w.id', $id)
->first();
} catch (\Throwable $e) {
@@ -567,7 +567,7 @@ class LegacyService
// additional stats (best-effort)
try {
$num_downloads = DB::connection('legacy')->table('artworks_downloads')
$num_downloads = DB::table('artworks_downloads')
->where('date', DB::raw('CURRENT_DATE'))
->where('artwork_id', $row->id)
->count();
@@ -576,7 +576,7 @@ class LegacyService
}
try {
$monthly_downloads = DB::connection('legacy')->table('monthly_downloads')
$monthly_downloads = DB::table('monthly_downloads')
->where('fname', $row->id)
->count();
} catch (\Throwable $e) {
@@ -584,7 +584,7 @@ class LegacyService
}
try {
$num_comments = DB::connection('legacy')->table('artworks_comments')
$num_comments = DB::table('artwork_comments')
->where('name', $row->id)
->where('author', '<>', '')
->count();
@@ -593,7 +593,7 @@ class LegacyService
}
try {
$num_favourites = DB::connection('legacy')->table('favourites')
$num_favourites = DB::table('favourites')
->where('artwork_id', $row->id)
->count();
} catch (\Throwable $e) {
@@ -601,7 +601,7 @@ class LegacyService
}
try {
$featured = DB::connection('legacy')->table('featured_works')
$featured = DB::table('featured_works')
->where('rootid', $row->rootid ?? 0)
->where('artwork_id', $row->id)
->orderByDesc('type')

View File

@@ -15,6 +15,7 @@
},
"require-dev": {
"fakerphp/faker": "^1.23",
"fruitcake/laravel-debugbar": "^4.0",
"laravel/boost": "^2.0",
"laravel/breeze": "*",
"laravel/pail": "^1.2.2",

272
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8ce5046202c37dce6cb659af6837b01b",
"content-hash": "77e9cb13ddd5d3584f5743ca484b7d65",
"packages": [
{
"name": "brick/math",
@@ -6333,6 +6333,108 @@
],
"time": "2025-08-08T12:00:00+00:00"
},
{
"name": "fruitcake/laravel-debugbar",
"version": "v4.0.7",
"source": {
"type": "git",
"url": "https://github.com/fruitcake/laravel-debugbar.git",
"reference": "a9cc62c81cd0bda4ca7410229487638d7df786be"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/a9cc62c81cd0bda4ca7410229487638d7df786be",
"reference": "a9cc62c81cd0bda4ca7410229487638d7df786be",
"shasum": ""
},
"require": {
"illuminate/routing": "^11|^12",
"illuminate/session": "^11|^12",
"illuminate/support": "^11|^12",
"php": "^8.2",
"php-debugbar/php-debugbar": "^3.1",
"php-debugbar/symfony-bridge": "^1.1"
},
"replace": {
"barryvdh/laravel-debugbar": "self.version"
},
"require-dev": {
"larastan/larastan": "^3",
"laravel/octane": "^2",
"laravel/pennant": "^1",
"laravel/pint": "^1",
"laravel/telescope": "^5.16",
"livewire/livewire": "^3.7|^4",
"mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^9|^10",
"php-debugbar/twig-bridge": "^2.0",
"phpstan/phpstan-phpunit": "^2",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11",
"shipmonk/phpstan-rules": "^4.3"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Debugbar": "Fruitcake\\LaravelDebugbar\\Facades\\Debugbar"
},
"providers": [
"Fruitcake\\LaravelDebugbar\\ServiceProvider"
]
},
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Fruitcake\\LaravelDebugbar\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fruitcake",
"homepage": "https://fruitcake.nl"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"barryvdh",
"debug",
"debugbar",
"dev",
"laravel",
"profiler",
"webprofiler"
],
"support": {
"issues": "https://github.com/fruitcake/laravel-debugbar/issues",
"source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.0.7"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-02-06T20:53:50+00:00"
},
{
"name": "hamcrest/hamcrest-php",
"version": "v2.1.1",
@@ -7736,6 +7838,174 @@
},
"time": "2022-02-21T01:04:05+00:00"
},
{
"name": "php-debugbar/php-debugbar",
"version": "v3.3.1",
"source": {
"type": "git",
"url": "https://github.com/php-debugbar/php-debugbar.git",
"reference": "afdaa2e56aca9d56b5bb2bad041bd2f6002017cf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/afdaa2e56aca9d56b5bb2bad041bd2f6002017cf",
"reference": "afdaa2e56aca9d56b5bb2bad041bd2f6002017cf",
"shasum": ""
},
"require": {
"php": "^8.2",
"psr/log": "^1|^2|^3",
"symfony/var-dumper": "^5.4|^6|^7|^8"
},
"replace": {
"maximebf/debugbar": "self.version"
},
"require-dev": {
"dbrekelmans/bdi": "^1.4",
"friendsofphp/php-cs-fixer": "^3.92",
"monolog/monolog": "^3.9",
"php-debugbar/doctrine-bridge": "^3@dev",
"php-debugbar/monolog-bridge": "^1@dev",
"php-debugbar/symfony-bridge": "^1@dev",
"php-debugbar/twig-bridge": "^2@dev",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-phpunit": "^2.0",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^10",
"predis/predis": "^3.3",
"shipmonk/phpstan-rules": "^4.3",
"symfony/browser-kit": "^6.4|7.0",
"symfony/dom-crawler": "^6.4|^7",
"symfony/event-dispatcher": "^5.4|^6.4|^7.3|^8.0",
"symfony/http-foundation": "^5.4|^6.4|^7.3|^8.0",
"symfony/mailer": "^5.4|^6.4|^7.3|^8.0",
"symfony/panther": "^1|^2.1",
"twig/twig": "^3.11.2"
},
"suggest": {
"php-debugbar/doctrine-bridge": "To integrate Doctrine with php-debugbar.",
"php-debugbar/monolog-bridge": "To integrate Monolog with php-debugbar.",
"php-debugbar/symfony-bridge": "To integrate Symfony with php-debugbar.",
"php-debugbar/twig-bridge": "To integrate Twig with php-debugbar."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/php-debugbar/php-debugbar",
"keywords": [
"debug",
"debug bar",
"debugbar",
"dev",
"profiler",
"toolbar"
],
"support": {
"issues": "https://github.com/php-debugbar/php-debugbar/issues",
"source": "https://github.com/php-debugbar/php-debugbar/tree/v3.3.1"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2026-02-06T21:09:38+00:00"
},
{
"name": "php-debugbar/symfony-bridge",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-debugbar/symfony-bridge.git",
"reference": "e37d2debe5d316408b00d0ab2688d9c2cf59b5ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-debugbar/symfony-bridge/zipball/e37d2debe5d316408b00d0ab2688d9c2cf59b5ad",
"reference": "e37d2debe5d316408b00d0ab2688d9c2cf59b5ad",
"shasum": ""
},
"require": {
"php": "^8.2",
"php-debugbar/php-debugbar": "^3.1",
"symfony/http-foundation": "^5.4|^6.4|^7.3|^8.0"
},
"require-dev": {
"dbrekelmans/bdi": "^1.4",
"phpunit/phpunit": "^10",
"symfony/browser-kit": "^6|^7",
"symfony/dom-crawler": "^6|^7",
"symfony/mailer": "^5.4|^6.4|^7.3|^8.0",
"symfony/panther": "^1|^2.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\Bridge\\Symfony\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Symfony bridge for PHP Debugbar",
"homepage": "https://github.com/php-debugbar/php-debugbar",
"keywords": [
"debugbar",
"dev",
"symfony"
],
"support": {
"issues": "https://github.com/php-debugbar/symfony-bridge/issues",
"source": "https://github.com/php-debugbar/symfony-bridge/tree/v1.1.0"
},
"time": "2026-01-15T14:47:34+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",

View File

@@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('user_favorites', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('user_id');
$table->unsignedBigInteger('artwork_id');
$table->timestamp('created_at')->useCurrent();
$table->unique(['user_id', 'artwork_id'], 'uniq_user_artwork');
$table->index('artwork_id', 'idx_artwork');
$table->index(['user_id', 'created_at'], 'idx_user_created');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('artwork_id')->references('id')->on('artworks')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('user_favorites');
}
};

398
package-lock.json generated
View File

@@ -10,10 +10,10 @@
"alpinejs": "^3.4.2",
"autoprefixer": "^10.4.2",
"axios": "^1.11.0",
"bootstrap": "^5.3.2",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.31",
"sass": "^1.70.0",
"tailwindcss": "^3.1.0",
"vite": "^7.0.7"
}
@@ -561,16 +561,314 @@
"node": ">= 8"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"node_modules/@parcel/watcher": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
"integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"optional": true,
"dependencies": {
"detect-libc": "^2.0.3",
"is-glob": "^4.0.3",
"node-addon-api": "^7.0.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.5.6",
"@parcel/watcher-darwin-arm64": "2.5.6",
"@parcel/watcher-darwin-x64": "2.5.6",
"@parcel/watcher-freebsd-x64": "2.5.6",
"@parcel/watcher-linux-arm-glibc": "2.5.6",
"@parcel/watcher-linux-arm-musl": "2.5.6",
"@parcel/watcher-linux-arm64-glibc": "2.5.6",
"@parcel/watcher-linux-arm64-musl": "2.5.6",
"@parcel/watcher-linux-x64-glibc": "2.5.6",
"@parcel/watcher-linux-x64-musl": "2.5.6",
"@parcel/watcher-win32-arm64": "2.5.6",
"@parcel/watcher-win32-ia32": "2.5.6",
"@parcel/watcher-win32-x64": "2.5.6"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
"integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
"integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
"integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
"integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
"integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
"integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
"integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
"integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
"integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
"integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
"integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
"integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.5.6",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
"integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
@@ -1402,26 +1700,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -2138,6 +2416,13 @@
"node": ">= 0.4"
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"dev": true,
"license": "MIT"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -2642,6 +2927,14 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -3074,6 +3367,57 @@
"tslib": "^2.1.0"
}
},
"node_modules/sass": {
"version": "1.97.3",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz",
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=14.0.0"
},
"optionalDependencies": {
"@parcel/watcher": "^2.4.1"
}
},
"node_modules/sass/node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/sass/node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",

View File

@@ -16,6 +16,7 @@
"laravel-vite-plugin": "^2.0.0",
"postcss": "^8.4.31",
"tailwindcss": "^3.1.0",
"vite": "^7.0.7"
"vite": "^7.0.7",
"sass": "^1.70.0"
}
}

187
public/css/nova.css Normal file
View File

@@ -0,0 +1,187 @@
/* RESET */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, sans-serif;
}
body {
background: #dfe5e9;
color: #e3e3e3;
min-height: 200vh;
}
/* TOPBAR */
.topbar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
background: #1c1c1f;
border-bottom: 1px solid #2a2a33;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
z-index: 9999;
}
.topbar a {
color: #eaeaea;
text-decoration: none;
font-size: 14px;
}
/* LEFT */
.topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
font-size: 20px;
font-weight: 600;
color: #4da3ff;
}
/* CENTER SEARCH */
.topbar-center {
flex: 1;
max-width: 520px;
margin: 0 20px;
}
.search-box {
position: relative;
width: 100%;
}
.search-box input {
width: 100%;
background: #1a1a22;
border: 1px solid #262631;
border-radius: 8px;
padding: 10px 40px 10px 14px;
color: #fff;
outline: none;
}
.search-box i {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
color: #777;
}
/* RIGHT */
.topbar-right {
display: flex;
align-items: center;
gap: 18px;
}
.icon-btn {
background: none;
border: none;
color: #ddd;
font-size: 16px;
cursor: pointer;
position: relative;
}
/* BADGE */
.badge {
position: absolute;
top: -4px;
right: -6px;
background: #ff5c5c;
color: #fff;
font-size: 10px;
padding: 2px 5px;
border-radius: 10px;
line-height: 1;
}
.icon-btn:hover {
color: #4da3ff;
}
/* DROPDOWN */
.dropdown {
position: relative;
}
.dropdown-menu {
position: absolute;
top: 120%;
right: 0;
background: #1a1a22;
border: 1px solid #262631;
border-radius: 8px;
min-width: 180px;
display: none;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0,0,0,.4);
}
.dropdown-menu a {
display: block;
padding: 10px 14px;
font-size: 13px;
}
.dropdown-menu a:hover {
background: #242430;
}
.dropdown.active .dropdown-menu {
display: block;
}
/* MOBILE */
.mobile-toggle {
display: none;
font-size: 18px;
}
.mobile-menu {
display: none;
position: fixed;
top: 64px;
left: 0;
right: 0;
background: #121217;
border-bottom: 1px solid #262631;
padding: 16px;
}
.mobile-menu a {
display: block;
padding: 10px 0;
border-bottom: 1px solid #1f1f26;
}
.mobile-menu.show {
display: block;
}
main {
padding-top: 80px;
}
/* RESPONSIVE */
@media (max-width: 768px) {
.topbar-center {
display: none;
}
.mobile-toggle {
display: block;
}
}

24
public/js/nova.js Normal file
View File

@@ -0,0 +1,24 @@
// Simulate login state (replace with Laravel later)
const isLoggedIn = true;
if (isLoggedIn) {
document.querySelector('.guest-only').style.display = 'none';
document.querySelector('.user-only').style.display = 'flex';
}
function toggleDropdown(btn) {
const drop = btn.closest('.dropdown');
drop.classList.toggle('active');
}
function toggleMobile() {
document.getElementById('mobileMenu').classList.toggle('show');
}
// Close dropdown on outside click
document.addEventListener('click', e => {
document.querySelectorAll('.dropdown').forEach(d => {
if (!d.contains(e.target)) d.classList.remove('active');
});
});

24
public/legacy/js/nova.js Normal file
View File

@@ -0,0 +1,24 @@
// Simulate login state (replace with Laravel later)
const isLoggedIn = true;
if (isLoggedIn) {
document.querySelector('.guest-only').style.display = 'none';
document.querySelector('.user-only').style.display = 'flex';
}
function toggleDropdown(btn) {
const drop = btn.closest('.dropdown');
drop.classList.toggle('active');
}
function toggleMobile() {
document.getElementById('mobileMenu').classList.toggle('show');
}
// Close dropdown on outside click
document.addEventListener('click', e => {
document.querySelectorAll('.dropdown').forEach(d => {
if (!d.contains(e.target)) d.classList.remove('active');
});
});

597
public/nova.html Normal file
View File

@@ -0,0 +1,597 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Skinbase Nova Preview</title>
<!-- Tailwind CDN (for template preview). In production use Vite + Tailwind build. -->
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Small extras Tailwind doesnt cover well in CDN mode */
.sb-scrollbar::-webkit-scrollbar { width: 10px; height: 10px; }
.sb-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,.08); border-radius: 999px; }
.sb-scrollbar::-webkit-scrollbar-track { background: rgba(0,0,0,.15); }
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
sb: {
bg: "#141416",
top: "#1c1c1f",
panel: "#191a1f",
panel2: "#15161b",
line: "#2a2a33",
text: "#e3e3e3",
muted: "#a6a6b0",
blue: "#4da3ff",
}
},
boxShadow: {
sb: "0 12px 30px rgba(0,0,0,.45)",
}
}
}
}
</script>
</head>
<body class="bg-sb-bg text-sb-text">
<!-- TOPBAR -->
<header class="fixed inset-x-0 top-0 z-50 h-16 bg-sb-top border-b border-sb-line">
<div class="mx-auto w-full h-full px-4 flex items-center gap-3">
<!-- Mobile hamburger -->
<button id="btnSidebar" class="md:hidden inline-flex items-center justify-center w-10 h-10 rounded-lg hover:bg-white/5">
<!-- bars -->
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<!-- Logo -->
<a href="#" class="flex items-center gap-2 pr-2">
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-orange-400 to-yellow-300 shadow-sm"></div>
<div class="text-lg font-semibold tracking-wide">
<span class="text-white">Skinbase</span><span class="text-sb-muted text-sm">.org</span>
</div>
</a>
<!-- Left nav -->
<nav class="hidden lg:flex items-center gap-4 text-sm text-sb-muted">
<a class="hover:text-white" href="#">Join</a>
<div class="relative">
<button class="hover:text-white inline-flex items-center gap-1" data-dd="signin">
Sign in
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<!-- optional dropdown -->
<div id="dd-signin" class="hidden absolute left-0 mt-2 w-56 rounded-lg bg-sb-panel border border-sb-line shadow-sb overflow-hidden">
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Sign in</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Forgot password</a>
</div>
</div>
<div class="relative">
<button class="hover:text-white inline-flex items-center gap-1" data-dd="browse">
Browse
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div id="dd-browse" class="hidden absolute left-0 mt-2 w-64 rounded-lg bg-sb-panel border border-sb-line shadow-sb overflow-visible">
<div class="rounded-lg overflow-hidden">
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">All Artworks</a>
<!-- Submenu: Types -->
<div class="relative group" data-submenu>
<button type="button" class="w-full px-4 py-2 text-sm hover:bg-white/5 inline-flex items-center justify-between gap-2" data-submenu-toggle aria-expanded="false">
<span>Types</span>
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 6l6 6-6 6"/>
</svg>
</button>
<div class="hidden group-hover:block absolute left-full top-0 ml-2 w-64 rounded-lg bg-sb-panel border border-sb-line shadow-sb overflow-hidden z-50" data-submenu-menu>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Photography</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Wallpapers</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Skins</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Other</a>
</div>
</div>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Featured Artwork</a>
</div>
</div>
</div>
<div class="relative">
<button class="hover:text-white inline-flex items-center gap-1" data-dd="cats">
Categories
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div id="dd-cats" class="hidden absolute left-0 mt-2 w-64 rounded-lg bg-sb-panel border border-sb-line shadow-sb overflow-hidden">
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Fantasy</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Sci-Fi</a>
<a class="block px-4 py-2 text-sm hover:bg-white/5" href="#">Nature</a>
</div>
</div>
</nav>
<!-- Search -->
<div class="flex-1 flex items-center justify-center">
<div class="w-full max-w-xl relative">
<input
class="w-full h-10 rounded-lg bg-black/20 border border-sb-line pl-4 pr-12 text-sm text-white placeholder:text-sb-muted/80 outline-none focus:border-sb-blue/60"
placeholder="Search tags, artworks, artists..."
/>
<button class="absolute right-2 top-1/2 -translate-y-1/2 w-9 h-9 rounded-md hover:bg-white/5 text-sb-muted hover:text-white">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="7"/>
<path d="M20 20l-3.5-3.5"/>
</svg>
</button>
</div>
</div>
<!-- Right icon counters -->
<div class="hidden md:flex items-center gap-3 text-sb-muted">
<button class="relative w-10 h-10 rounded-lg hover:bg-white/5">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
<button class="relative w-10 h-10 rounded-lg hover:bg-white/5">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 21s-7-4.4-9-9a5.5 5.5 0 0 1 9-6 5.5 5.5 0 0 1 9 6c-2 4.6-9 9-9 9z"/>
</svg>
<span class="absolute -bottom-1 right-0 text-[11px] tabular-nums px-1.5 py-0.5 rounded bg-black/30 border border-sb-line">37</span>
</button>
<button class="relative w-10 h-10 rounded-lg hover:bg-white/5">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16v14H5.2L4 19.2V4z"/>
<path d="M4 6l8 6 8-6"/>
</svg>
<span class="absolute -bottom-1 right-0 text-[11px] tabular-nums px-1.5 py-0.5 rounded bg-black/30 border border-sb-line">22</span>
</button>
<button class="relative w-10 h-10 rounded-lg hover:bg-white/5">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8a6 6 0 10-12 0c0 7-3 7-3 7h18s-3 0-3-7"/>
<path d="M13.7 21a2 2 0 01-3.4 0"/>
</svg>
<span class="absolute -bottom-1 right-0 text-[11px] tabular-nums px-1.5 py-0.5 rounded bg-black/30 border border-sb-line">5</span>
</button>
<!-- User dropdown -->
<div class="relative">
<button class="flex items-center gap-2 pl-2 pr-3 h-10 rounded-lg hover:bg-white/5" data-dd="user">
<img class="w-7 h-7 rounded-full object-cover ring-1 ring-white/10"
src="https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=64&h=64&fit=crop"
alt="User" />
<span class="text-sm text-white/90">Gregor</span>
<svg class="w-4 h-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 9l6 6 6-6"/>
</svg>
</button>
<div id="dd-user" class="hidden absolute right-0 mt-2 w-64 rounded-lg bg-sb-panel border border-sb-line shadow-sb overflow-hidden">
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">⬆️</span> Upload
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">✏️</span> Edit Artworks
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">📊</span> Statistics
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">👥</span> My Followers
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center"></span> Who I Follow
</a>
<div class="h-px bg-sb-line/80 my-1"></div>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">💬</span> Received Comments
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">❤️</span> My Favourites
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">🖼️</span> My Gallery
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">⚙️</span> Edit Profile
</a>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">👁️</span> View My Profile
</a>
<div class="h-px bg-sb-line/80 my-1"></div>
<a class="flex items-center gap-3 px-4 py-2 text-sm hover:bg-white/5" href="#">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center"></span> Logout
</a>
</div>
</div>
</div>
</div>
</header>
<!-- LAYOUT -->
<div class="pt-16">
<div class="mx-auto w-full">
<div class="flex min-h-[calc(100vh-64px)]">
<!-- SIDEBAR -->
<aside id="sidebar"
class="hidden md:block w-72 shrink-0 border-r border-sb-line bg-sb-panel2/60 backdrop-blur-sm">
<div class="p-4">
<button class="w-full h-12 rounded-xl bg-white/5 hover:bg-white/7 border border-white/5 flex items-center gap-3 px-4">
<span class="w-8 h-8 rounded-lg bg-white/5 inline-flex items-center justify-center">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</span>
<span class="text-sm text-white/90">Menu</span>
</button>
<div class="mt-6 text-sm text-sb-muted">
<div class="font-semibold text-white/80 mb-2">Main Categories:</div>
<ul class="space-y-2">
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">📷</span> Photography</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🖼️</span> Wallpapers</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🧩</span> Skins</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70"></span> Other</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Browse Subcategories:</div>
<ul class="space-y-2 sb-scrollbar max-h-56 overflow-auto pr-2">
<li><a class="hover:text-white" href="#">3D</a></li>
<li><a class="hover:text-white" href="#">Abstract</a></li>
<li><a class="hover:text-white" href="#">Animals</a></li>
<li><a class="hover:text-white" href="#">Anime</a></li>
<li><a class="hover:text-white" href="#">Art</a></li>
<li><a class="hover:text-white" href="#">Cars</a></li>
<li><a class="hover:text-white" href="#">Cartoon</a></li>
<li><a class="hover:text-white" href="#">Fantasy</a></li>
<li><a class="hover:text-white" href="#">Nature</a></li>
<li><a class="hover:text-white" href="#">Sci-Fi</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Daily Uploads <span class="text-sb-muted font-normal">(245)</span></div>
<div class="rounded-xl bg-white/5 border border-white/5 overflow-hidden">
<button class="w-full px-4 py-3 text-left hover:bg-white/5">All</button>
<button class="w-full px-4 py-3 text-left hover:bg-white/5">Hot</button>
</div>
<a class="mt-4 inline-flex items-center gap-2 text-sb-muted hover:text-white" href="#">
<span>Link, more</span>
<span class="opacity-60"></span>
</a>
</div>
</div>
</aside>
<!-- MAIN -->
<main class="flex-1">
<!-- Hero background -->
<div class="relative overflow-hidden">
<div class="absolute inset-0 opacity-35"
style="background-image: radial-gradient(circle at 20% 10%, rgba(77,163,255,.25), transparent 35%), radial-gradient(circle at 70% 30%, rgba(255,196,77,.18), transparent 40%), radial-gradient(circle at 30% 80%, rgba(180,77,255,.16), transparent 45%);">
</div>
<div class="relative px-6 py-8 md:px-10 md:py-10">
<div class="text-sm text-sb-muted">
<a class="hover:text-white" href="#">Wallpapers</a> <span class="opacity-50"></span> <span class="text-white/80">Fantasy</span>
</div>
<h1 class="mt-2 text-3xl md:text-4xl font-semibold tracking-tight text-white/95">Fantasy</h1>
<!-- Info card -->
<section class="mt-5 bg-white/5 border border-white/10 rounded-2xl shadow-sb">
<div class="p-5 md:p-6">
<div class="text-lg font-semibold text-white/90">Fantasy</div>
<p class="mt-2 text-sm leading-6 text-sb-muted">
Fantasy is a genre of art that uses magic and other supernatural forms as a primary element of plot, theme, or setting.
In its broadest sense, fantasy comprises works by authors, artists, filmmakers and musicians...
</p>
</div>
</section>
</div>
</div>
<!-- Grid -->
<section class="px-6 pb-10 md:px-10">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<!-- Card template -->
<!-- Repeat as needed -->
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-cyan-400/30 via-blue-500/20 to-purple-600/30"></div>
<!-- Ribbon -->
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">
FANTASY
</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">
Featured artwork
</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-emerald-400/25 via-teal-500/15 to-sky-500/25"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Island</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-fuchsia-400/20 via-rose-500/20 to-amber-500/20"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Sunset</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-indigo-400/20 via-slate-500/20 to-zinc-700/30"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Dragon</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-yellow-400/20 via-orange-500/15 to-red-600/25"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Explosion</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-lime-400/15 via-green-700/20 to-emerald-900/25"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Forest</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-sky-400/20 via-blue-800/25 to-slate-900/25"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Sea</div>
</a>
<a href="#" class="group relative rounded-2xl overflow-hidden bg-black/20 border border-white/10 shadow-sb">
<div class="aspect-[16/10] bg-gradient-to-br from-stone-400/10 via-zinc-600/20 to-neutral-900/35"></div>
<div class="absolute top-3 right-3">
<div class="origin-top-right rotate-45 translate-x-6 -translate-y-2">
<div class="px-10 py-1 text-[10px] tracking-widest font-bold bg-orange-600/90 text-white shadow">FANTASY</div>
</div>
</div>
<div class="p-3 text-xs text-sb-muted group-hover:text-white/80">Portrait</div>
</a>
</div>
<div class="flex justify-center mt-10">
<button class="px-10 py-3 rounded-xl bg-white/5 border border-white/10 hover:bg-white/10 text-white/90 shadow-sb">
Browse All
</button>
</div>
</section>
<!-- Footer -->
<footer class="border-t border-sb-line bg-sb-top">
<div class="px-6 md:px-10 py-8 flex flex-col md:flex-row md:items-center md:justify-between gap-6">
<div class="text-xl font-semibold tracking-wide">
<span class="text-white">Skinbase</span>
</div>
<div class="flex flex-wrap gap-x-6 gap-y-2 text-sm text-sb-muted">
<a class="hover:text-white" href="#">Bug Report</a>
<a class="hover:text-white" href="#">RSS Feeds</a>
<a class="hover:text-white" href="#">FAQ</a>
<a class="hover:text-white" href="#">Rules and Guidelines</a>
<a class="hover:text-white" href="#">Staff</a>
<a class="hover:text-white" href="#">Privacy Policy</a>
</div>
<div class="text-xs text-sb-muted">© 2026 Skinbase.org</div>
</div>
</footer>
</main>
</div>
</div>
</div>
<!-- Mobile sidebar drawer -->
<div id="sidebarOverlay" class="hidden fixed inset-0 z-40 bg-black/60"></div>
<div id="sidebarDrawer" class="hidden fixed top-16 left-0 bottom-0 z-50 w-80 max-w-[85vw] bg-sb-panel2 border-r border-sb-line shadow-sb">
<div class="p-4 sb-scrollbar overflow-auto h-full">
<div class="flex items-center justify-between">
<div class="text-sm font-semibold text-white/90">Menu</div>
<button id="btnCloseSidebar" class="w-10 h-10 rounded-lg hover:bg-white/5">
<svg class="w-5 h-5 mx-auto" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 6l12 12M18 6l-12 12"/>
</svg>
</button>
</div>
<div class="mt-5 text-sm text-sb-muted">
<div class="font-semibold text-white/80 mb-2">Main Categories:</div>
<ul class="space-y-2">
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">📷</span> Photography</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🖼️</span> Wallpapers</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70">🧩</span> Skins</a></li>
<li><a class="flex items-center gap-2 hover:text-white" href="#"><span class="opacity-70"></span> Other</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Browse Subcategories:</div>
<ul class="space-y-2">
<li><a class="hover:text-white" href="#">3D</a></li>
<li><a class="hover:text-white" href="#">Abstract</a></li>
<li><a class="hover:text-white" href="#">Animals</a></li>
<li><a class="hover:text-white" href="#">Anime</a></li>
<li><a class="hover:text-white" href="#">Art</a></li>
<li><a class="hover:text-white" href="#">Cars</a></li>
<li><a class="hover:text-white" href="#">Cartoon</a></li>
<li><a class="hover:text-white" href="#">Fantasy</a></li>
<li><a class="hover:text-white" href="#">Nature</a></li>
<li><a class="hover:text-white" href="#">Sci-Fi</a></li>
</ul>
<div class="mt-6 font-semibold text-white/80 mb-2">Daily Uploads <span class="text-sb-muted font-normal">(245)</span></div>
<div class="rounded-xl bg-white/5 border border-white/5 overflow-hidden">
<button class="w-full px-4 py-3 text-left hover:bg-white/5">All</button>
<button class="w-full px-4 py-3 text-left hover:bg-white/5">Hot</button>
</div>
</div>
</div>
</div>
<script>
// Dropdowns (hover on desktop, click fallback)
const ddButtons = document.querySelectorAll('[data-dd]');
const closeAllDropdowns = () => {
document.querySelectorAll('[id^="dd-"]').forEach(el => el.classList.add('hidden'));
};
const canHover = () => window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
const openDropdown = (key) => {
const menu = document.getElementById('dd-' + key);
if (!menu) return;
closeAllDropdowns();
menu.classList.remove('hidden');
};
const closeDropdown = (key) => {
const menu = document.getElementById('dd-' + key);
if (!menu) return;
menu.classList.add('hidden');
};
const closeTimers = new Map();
const scheduleClose = (key) => {
const t = closeTimers.get(key);
if (t) clearTimeout(t);
closeTimers.set(key, setTimeout(() => closeDropdown(key), 140));
};
const cancelClose = (key) => {
const t = closeTimers.get(key);
if (t) clearTimeout(t);
closeTimers.delete(key);
};
ddButtons.forEach(btn => {
const key = btn.getAttribute('data-dd');
const menu = document.getElementById('dd-' + key);
if (!key || !menu) return;
// Hover-to-open on desktop pointers
const wrap = btn.closest('.relative') || btn.parentElement;
if (wrap) {
wrap.addEventListener('mouseenter', () => {
if (!canHover()) return;
cancelClose(key);
openDropdown(key);
});
wrap.addEventListener('mouseleave', () => {
if (!canHover()) return;
scheduleClose(key);
});
}
// Click fallback (touch devices)
btn.addEventListener('click', (e) => {
if (canHover()) {
// On desktop, hover is primary; clicking the label shouldn't be required.
e.preventDefault();
return;
}
e.stopPropagation();
const isOpen = !menu.classList.contains('hidden');
closeAllDropdowns();
if (!isOpen) menu.classList.remove('hidden');
});
});
// Submenu hover handlers (ensure flyout appears on desktop)
document.querySelectorAll('[data-submenu]').forEach(function (group) {
var toggle = group.querySelector('[data-submenu-toggle]');
var menu = group.querySelector('[data-submenu-menu]');
if (!menu) return;
group.addEventListener('mouseenter', function () {
if (!canHover()) return;
menu.classList.remove('hidden');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
});
group.addEventListener('mouseleave', function () {
if (!canHover()) return;
menu.classList.add('hidden');
if (toggle) toggle.setAttribute('aria-expanded', 'false');
});
});
document.addEventListener('click', () => closeAllDropdowns());
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeAllDropdowns(); });
// Mobile sidebar
const btnSidebar = document.getElementById('btnSidebar');
const sidebarOverlay = document.getElementById('sidebarOverlay');
const sidebarDrawer = document.getElementById('sidebarDrawer');
const btnCloseSidebar = document.getElementById('btnCloseSidebar');
const openSidebar = () => {
sidebarOverlay.classList.remove('hidden');
sidebarDrawer.classList.remove('hidden');
document.body.style.overflow = 'hidden';
};
const closeSidebar = () => {
sidebarOverlay.classList.add('hidden');
sidebarDrawer.classList.add('hidden');
document.body.style.overflow = '';
};
btnSidebar?.addEventListener('click', openSidebar);
sidebarOverlay?.addEventListener('click', closeSidebar);
btnCloseSidebar?.addEventListener('click', closeSidebar);
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeSidebar();
});
</script>
</body>
</html>

223
resources/js/nova.js Normal file
View File

@@ -0,0 +1,223 @@
// Nova toolbar interactions
// - dropdown menus via [data-dropdown]
// - mobile menu toggle via [data-mobile-toggle] + #mobileMenu
(function () {
function closest(el, selector) {
while (el && el.nodeType === 1) {
if (el.matches(selector)) return el;
el = el.parentElement;
}
return null;
}
function canHover() {
return window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
}
function setExpanded(toggleEl, expanded) {
if (!toggleEl) return;
toggleEl.setAttribute('aria-expanded', expanded ? 'true' : 'false');
}
function closeAllDropdowns(except) {
var dropdowns = document.querySelectorAll('[data-dropdown]');
dropdowns.forEach(function (dropdown) {
if (except && dropdown === except) return;
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (menu) menu.classList.add('hidden');
setExpanded(toggle, false);
// Close any submenus
dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) {
sm.classList.add('hidden');
});
dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) {
setExpanded(st, false);
});
});
}
function openDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (!menu || !toggle) return;
closeAllDropdowns(dropdown);
menu.classList.remove('hidden');
setExpanded(toggle, true);
}
function closeDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (menu) menu.classList.add('hidden');
setExpanded(toggle, false);
}
function toggleDropdown(dropdown) {
var menu = dropdown.querySelector('[data-dropdown-menu]');
var toggle = dropdown.querySelector('[data-dropdown-toggle]');
if (!menu || !toggle) return;
var isOpen = !menu.classList.contains('hidden');
closeAllDropdowns(isOpen ? null : dropdown);
if (isOpen) {
menu.classList.add('hidden');
setExpanded(toggle, false);
} else {
menu.classList.remove('hidden');
setExpanded(toggle, true);
}
}
function getMobileMenu() {
return document.getElementById('mobileMenu');
}
function closeMobileMenu() {
var menu = getMobileMenu();
if (!menu) return;
menu.classList.add('hidden');
var toggle = document.querySelector('[data-mobile-toggle]');
setExpanded(toggle, false);
}
function toggleMobileMenu() {
var menu = getMobileMenu();
if (!menu) return;
var isOpen = !menu.classList.contains('hidden');
if (isOpen) {
closeMobileMenu();
} else {
menu.classList.remove('hidden');
var toggle = document.querySelector('[data-mobile-toggle]');
setExpanded(toggle, true);
closeAllDropdowns();
}
}
document.addEventListener('click', function (e) {
var dropdownToggle = closest(e.target, '[data-dropdown-toggle]');
if (dropdownToggle) {
e.preventDefault();
var dropdown = closest(dropdownToggle, '[data-dropdown]');
if (dropdown) toggleDropdown(dropdown);
return;
}
var mobileToggle = closest(e.target, '[data-mobile-toggle]');
if (mobileToggle) {
e.preventDefault();
toggleMobileMenu();
return;
}
// Submenu toggle (touch/click fallback)
var submenuToggle = closest(e.target, '[data-submenu-toggle]');
if (submenuToggle) {
if (canHover()) {
// On desktop, submenu opens on hover via CSS.
e.preventDefault();
return;
}
e.preventDefault();
var submenu = closest(submenuToggle, '[data-submenu]');
if (!submenu) return;
var menu = submenu.querySelector('[data-submenu-menu]');
if (!menu) return;
// Close other submenus within the same dropdown
var dropdown = closest(submenuToggle, '[data-dropdown]');
if (dropdown) {
dropdown.querySelectorAll('[data-submenu-menu]').forEach(function (sm) {
if (sm !== menu) sm.classList.add('hidden');
});
dropdown.querySelectorAll('[data-submenu-toggle]').forEach(function (st) {
if (st !== submenuToggle) setExpanded(st, false);
});
}
var isOpen = !menu.classList.contains('hidden');
if (isOpen) {
menu.classList.add('hidden');
setExpanded(submenuToggle, false);
} else {
menu.classList.remove('hidden');
setExpanded(submenuToggle, true);
}
return;
}
if (!closest(e.target, '[data-dropdown]')) {
closeAllDropdowns();
}
});
// Hover-to-open for desktop pointers
var hoverCloseTimers = new WeakMap();
function clearHoverTimer(dropdown) {
var t = hoverCloseTimers.get(dropdown);
if (t) window.clearTimeout(t);
hoverCloseTimers.delete(dropdown);
}
function scheduleClose(dropdown) {
clearHoverTimer(dropdown);
hoverCloseTimers.set(
dropdown,
window.setTimeout(function () {
closeDropdown(dropdown);
}, 140)
);
}
function bindHoverHandlers() {
if (!canHover()) return;
document.querySelectorAll('[data-dropdown]').forEach(function (dropdown) {
dropdown.addEventListener('mouseenter', function () {
clearHoverTimer(dropdown);
openDropdown(dropdown);
});
dropdown.addEventListener('mouseleave', function () {
scheduleClose(dropdown);
});
});
}
bindHoverHandlers();
// Submenu hover handlers: ensure flyouts open on pointer devices
if (canHover()) {
document.querySelectorAll('[data-submenu]').forEach(function (group) {
var toggle = group.querySelector('[data-submenu-toggle]');
var menu = group.querySelector('[data-submenu-menu]');
if (!menu) return;
group.addEventListener('mouseenter', function () {
menu.classList.remove('hidden');
if (toggle) setExpanded(toggle, true);
});
group.addEventListener('mouseleave', function () {
menu.classList.add('hidden');
if (toggle) setExpanded(toggle, false);
});
});
}
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
closeAllDropdowns();
closeMobileMenu();
});
window.addEventListener('resize', function () {
if (window.matchMedia('(min-width: 768px)').matches) {
closeMobileMenu();
}
});
})();

5
resources/scss/nova.scss Normal file
View File

@@ -0,0 +1,5 @@
/*
* Nova overrides (keep minimal).
* The Nova layout is styled primarily via Tailwind utilities from resources/css/app.css.
*/

View File

@@ -0,0 +1,85 @@
@extends('layouts.legacy')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h2 style="margin:0;display:inline-block">Edit Artwork</h2>
<a class="btn btn-default btn-xs pull-right" href="{{ route('dashboard.artworks.index') }}" style="margin-top:6px">
<i class="fa fa-arrow-left fa-fw"></i> Back to list
</a>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">
#{{ $artwork->id }} &middot; {{ $artwork->is_public ? 'Public' : 'Private' }}
</div>
</div>
@if(session('status'))
<div class="alert alert-success" role="status">{{ session('status') }}</div>
@endif
@if($errors->any())
<div class="alert alert-danger" role="alert">
<strong>Please fix the errors below.</strong>
</div>
@endif
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<form method="POST" action="{{ route('dashboard.artworks.update', $artwork->id) }}" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="row">
<div class="col-md-7">
<div class="form-group @error('title') has-error @enderror">
<label for="title" class="control-label">Title</label>
<input id="title" type="text" name="title" class="form-control" value="{{ old('title', $artwork->title) }}" required>
@error('title')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<div class="form-group @error('description') has-error @enderror">
<label for="description" class="control-label">Description</label>
<textarea id="description" name="description" class="form-control" rows="8">{{ old('description', $artwork->description) }}</textarea>
@error('description')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="btn btn-success">
<i class="fa fa-save fa-fw"></i> Save Changes
</button>
</div>
<div class="col-md-5">
<label class="control-label">Preview</label>
<div class="thumbnail" style="margin-top:6px">
<img
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
@if(!empty($artwork->thumb_srcset)) srcset="{{ $artwork->thumb_srcset }}" @endif
class="img-responsive"
alt="{{ $artwork->title }}"
>
</div>
<div class="form-group @error('file') has-error @enderror">
<label for="file" class="control-label">Replace image (optional)</label>
<input id="file" type="file" name="file" class="form-control" accept="image/*">
<p class="help-block">Max 100MB. Images only.</p>
@error('file')
<span class="help-block">{{ $message }}</span>
@enderror
</div>
<p class="text-muted" style="font-size:12px;margin-bottom:0">
Created: {{ optional($artwork->created_at)->format('d.m.Y') }}
</p>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,104 @@
@extends('layouts.legacy')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h2 style="margin:0;display:inline-block">My Artworks</h2>
<span class="text-muted pull-right" style="margin-top:6px">
{{ method_exists($artworks, 'total') ? $artworks->total() : $artworks->count() }} items
</span>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">Manage and edit your uploads.</div>
</div>
@if(session('status'))
<div class="alert alert-success" role="status">{{ session('status') }}</div>
@endif
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover table-advanced" style="margin-bottom:0">
<thead>
<tr>
<th style="width:92px">Preview</th>
<th>Title</th>
<th style="width:120px" class="text-center">Created</th>
<th style="width:90px" class="text-center">Public</th>
<th style="width:95px" class="text-center">Approved</th>
<th style="width:150px" class="text-center">Actions</th>
</tr>
</thead>
<tbody>
@forelse($artworks as $artwork)
<tr>
<td class="text-center">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}" title="Edit {{ $artwork->title }}">
<img
src="{{ $artwork->thumb_url ?? $artwork->thumb }}"
@if(!empty($artwork->thumb_srcset)) srcset="{{ $artwork->thumb_srcset }}" @endif
alt="{{ $artwork->title }}"
class="img-thumbnail"
style="width:70px;height:70px;object-fit:cover"
>
</a>
</td>
<td>
<div style="font-weight:700;line-height:1.2">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}">{{ $artwork->title }}</a>
</div>
<div class="text-muted" style="font-size:12px">#{{ $artwork->id }}</div>
@if(!empty($artwork->description))
<div class="text-muted" style="margin-top:4px;font-size:12px">
{{ str($artwork->description)->stripTags()->limit(120) }}
</div>
@endif
</td>
<td class="text-center">{{ optional($artwork->created_at)->format('d.m.Y') }}</td>
<td class="text-center">
@if($artwork->is_public)
<span class="label label-success">Yes</span>
@else
<span class="label label-default">No</span>
@endif
</td>
<td class="text-center">
@if($artwork->is_approved)
<span class="label label-primary">Yes</span>
@else
<span class="label label-warning">No</span>
@endif
</td>
<td class="text-center">
<div class="btn-group" role="group" aria-label="Actions">
<a href="{{ route('dashboard.artworks.edit', $artwork->id) }}" class="btn btn-xs btn-default">
<i class="fa fa-pencil fa-fw"></i> Edit
</a>
<form method="POST" action="{{ route('dashboard.artworks.destroy', $artwork->id) }}" style="display:inline-block" onsubmit="return confirm('Delete this artwork?');">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-xs btn-danger">
<i class="fa fa-trash fa-fw"></i> Delete
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6">No artworks found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="paginationMenu text-center">
{{ $artworks->links('pagination::bootstrap-3') }}
</div>
</div>
@endsection

View File

@@ -0,0 +1,18 @@
@extends('layouts.nova')
@section('content')
<div class="bg-slate-900 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-8">
<header class="mb-6">
<h1 class="text-3xl font-semibold text-white">Blank</h1>
</header>
<section class="bg-slate-800 rounded-lg p-6 shadow-sm">
<!-- Empty content area for layout testing -->
<p class="text-slate-300">Layout test area add components here.</p>
</section>
</div>
</div>
</div>
@endsection

View File

@@ -1,17 +1,45 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
@extends('layouts.legacy')
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
@section('content')
<div class="container">
@php($page_title = 'Dashboard')
<h2>Dashboard</h2>
@if(session('status'))
<div class="alert alert-success" role="status">{{ session('status') }}</div>
@endif
<div class="alert alert-info" role="status">
You're logged in.
</div>
<div class="row">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Your content</div>
<div class="panel-body">
<p>
<a class="btn btn-primary" href="{{ route('dashboard.artworks.index') }}">
<i class="fa fa-picture-o fa-fw"></i> Manage My Artworks
</a>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">Account</div>
<div class="panel-body">
<p>
<a class="btn btn-default" href="{{ route('profile.edit') }}">
<i class="fa fa-user fa-fw"></i> Edit Profile
</a>
</p>
</div>
</div>
</div>
</div>
</x-app-layout>
</div>
@endsection

View File

@@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ $page_title ?? 'Skinbase' }}</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ $page_meta_description ?? '' }}">
<meta name="keywords" content="{{ $page_meta_keywords ?? '' }}">
<!-- Icons (kept for now to preserve current visual output) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<link rel="shortcut icon" href="/favicon.ico">
@vite(['resources/css/app.css','resources/scss/nova.scss','resources/js/nova.js'])
@stack('head')
</head>
<body class="bg-neutral-950 text-neutral-200 min-h-screen">
<header class="fixed top-0 left-0 right-0 h-16 bg-neutral-900 border-b border-neutral-800 z-50">
<div class="h-full px-5 flex items-center justify-between gap-4">
<!-- LEFT -->
<div class="flex items-center gap-4">
<button type="button" class="md:hidden text-neutral-200 hover:text-sky-400" data-mobile-toggle aria-controls="mobileMenu" aria-expanded="false" aria-label="Toggle menu">
<i class="fas fa-bars text-lg"></i>
</button>
<a href="/" class="text-sky-400 font-semibold text-xl">Skinbase</a>
</div>
<!-- CENTER SEARCH (hidden on mobile) -->
<div class="hidden md:block flex-1 max-w-xl">
<form action="/search" method="get" class="relative">
<input type="search" name="q" value="{{ request('q') }}" placeholder="Search tags, artworks, artists…"
class="w-full bg-neutral-800 border border-neutral-700 rounded-lg py-2.5 pl-3.5 pr-10 text-white outline-none focus:border-sky-400">
<i class="fas fa-search absolute right-3.5 top-1/2 -translate-y-1/2 text-neutral-400"></i>
</form>
</div>
<!-- RIGHT -->
<div class="flex items-center gap-4 sm:gap-5">
@guest
<div class="hidden sm:flex items-center gap-4 text-sm">
<a href="/signup" class="hover:text-sky-400">Join</a>
<a href="/login" class="hover:text-sky-400">Sign in</a>
</div>
@endguest
@auth
@php
$userId = auth()->id();
$novaCounts = $novaCounts ?? [];
$favCount = $novaCounts['favourites'] ?? null;
$msgCount = $novaCounts['messages'] ?? null;
$noticeCount = $novaCounts['notifications'] ?? null;
@endphp
<a href="/upload" class="relative text-neutral-200 hover:text-sky-400" title="Upload">
<i class="fas fa-upload"></i>
</a>
<a href="/favourites/{{ $userId }}/{{ auth()->user()->username ?? '' }}" class="relative text-neutral-200 hover:text-sky-400" title="Favourites">
<i class="fas fa-heart"></i>
@if($favCount)
<span class="absolute -top-1 -right-2 bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full leading-none">{{ $favCount }}</span>
@endif
</a>
<a href="/messages" class="relative text-neutral-200 hover:text-sky-400" title="Messages">
<i class="fas fa-envelope"></i>
@if($msgCount)
<span class="absolute -top-1 -right-2 bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full leading-none">{{ $msgCount }}</span>
@endif
</a>
<a href="/notices" class="relative text-neutral-200 hover:text-sky-400" title="Notifications">
<i class="fas fa-bell"></i>
@if($noticeCount)
<span class="absolute -top-1 -right-2 bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full leading-none">{{ $noticeCount }}</span>
@endif
</a>
@endauth
<!-- BROWSE DROPDOWN -->
<div class="relative" data-dropdown>
<button type="button" class="text-neutral-200 hover:text-sky-400" data-dropdown-toggle aria-expanded="false" aria-label="Browse">
<i class="fas fa-layer-group"></i>
</button>
<div class="hidden absolute top-[120%] right-0 bg-neutral-800 border border-neutral-700 rounded-lg min-w-[200px] overflow-visible shadow-lg" data-dropdown-menu>
<div class="rounded-lg overflow-hidden">
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/browse">All Artworks</a>
<!-- Submenu: Types -->
<div class="relative group" data-submenu>
<button type="button" class="w-full text-left px-3.5 py-2.5 text-sm hover:bg-neutral-700 flex items-center justify-between gap-3" data-submenu-toggle aria-expanded="false">
<span>Types</span>
<i class="fas fa-chevron-right text-[11px] opacity-70"></i>
</button>
<div class="hidden group-hover:block absolute left-full top-0 ml-1 bg-neutral-800 border border-neutral-700 rounded-lg min-w-[200px] shadow-lg z-50" data-submenu-menu>
<div class="rounded-lg overflow-hidden">
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/photography">Photography</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/wallpapers">Wallpapers</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/skins">Skins</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/other">Other</a>
</div>
</div>
</div>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/featured-artworks">Featured</a>
</div>
</div>
</div>
<a href="/forum" class="hidden sm:inline text-sm hover:text-sky-400">Forum</a>
<!-- USER DROPDOWN -->
<div class="relative" data-dropdown>
<button type="button" class="text-neutral-200 hover:text-sky-400" data-dropdown-toggle aria-expanded="false" aria-label="User">
<i class="fas fa-user"></i>
</button>
<div class="hidden absolute top-[120%] right-0 bg-neutral-800 border border-neutral-700 rounded-lg min-w-[220px] overflow-hidden shadow-lg" data-dropdown-menu>
@auth
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/upload">Upload</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="{{ route('dashboard.artworks.index') }}">Edit Artworks</a>
<div class="h-px bg-neutral-700"></div>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/statistics">Statistics</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/mybuddies.php">My Followers</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/buddies.php">Who I Follow</a>
<div class="h-px bg-neutral-700"></div>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/recieved-comments">Received Comments</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/favourites/{{ $userId }}/{{ auth()->user()->username ?? '' }}">My Favourites</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/gallery/{{ $userId }}/{{ auth()->user()->username ?? '' }}">My Gallery</a>
<div class="h-px bg-neutral-700"></div>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/user">Edit Profile</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/profile/{{ $userId }}/{{ auth()->user()->username ?? '' }}">View Profile</a>
<form method="POST" action="/logout" class="m-0">
@csrf
<button type="submit" class="w-full text-left px-3.5 py-2.5 text-sm hover:bg-neutral-700">Logout</button>
</form>
@else
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/signup">Join</a>
<a class="block px-3.5 py-2.5 text-sm hover:bg-neutral-700" href="/login">Sign in</a>
@endauth
</div>
</div>
</div>
</div>
</header>
<!-- MOBILE MENU -->
<div class="hidden fixed top-16 left-0 right-0 bg-neutral-950 border-b border-neutral-800 p-4" id="mobileMenu">
<div class="space-y-2">
@guest
<a class="block py-2 border-b border-neutral-900" href="/signup">Join</a>
<a class="block py-2 border-b border-neutral-900" href="/login">Sign in</a>
@endguest
<a class="block py-2 border-b border-neutral-900" href="/browse">All Artworks</a>
<a class="block py-2 border-b border-neutral-900" href="/photography">Photography</a>
<a class="block py-2 border-b border-neutral-900" href="/wallpapers">Wallpapers</a>
<a class="block py-2 border-b border-neutral-900" href="/skins">Skins</a>
<a class="block py-2 border-b border-neutral-900" href="/other">Other</a>
<a class="block py-2 border-b border-neutral-900" href="/featured-artworks">Featured</a>
<a class="block py-2 border-b border-neutral-900" href="/forum">Forum</a>
<a class="block py-2 border-b border-neutral-900" href="/profile">Profile</a>
<a class="block py-2" href="/settings">Settings</a>
</div>
</div>
<main class="pt-20">
@yield('content')
</main>
</body>
</html>

View File

@@ -0,0 +1,11 @@
<footer id="mainFooter">
<p>&copy; 2000 - {{ date('Y') }} by SkinBase.org. All artwork copyrighted to its Author. (Dragon II Edition)</p>
<div class="footer_links">
<a href="/bug-report" title="Inform us about any bugs you found">Bug report</a> :
<a href="/rss-feeds" title="Skinbase RSS Feeds about new Artworks">RSS Feeds</a> :
<a href="/faq" title="Frequently Asked Questions">FAQ</a> :
<a href="/rules" title="Rules and Guidelines">Rules and Guidelines</a> :
<a href="/staff" title="Who is actually behind Skinbase">Staff</a> :
<a href="/privacy" title="Privacy Policy">Privacy Policy</a>
</div>
</footer>

View File

@@ -0,0 +1,214 @@
<nav class="nb-navbar" role="navigation">
<div class="nb-container">
<div class="nb-header">
<button type="button" class="nb-toggle" aria-expanded="false" aria-controls="nb-main-nav">
<span class="nb-sr">Toggle navigation</span>
<span class="nb-bar"></span>
<span class="nb-bar"></span>
<span class="nb-bar"></span>
</button>
<a href="/" class="nb-brand" title="SkinBase">SkinBase</a>
</div>
<div id="nb-main-nav" class="nb-collapse">
<form class="nb-search" action="/search" method="get" id="search_box">
<input type="text" name="q" value="{{ request('q') }}" placeholder="Search">
<input type="hidden" name="group" value="all">
<button type="submit" aria-label="Search"><svg class="icon icon-search" viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#icon-search"></use></svg></button>
</form>
<ul class="nb-nav">
<li class="nb-dropdown nb-mega">
<button class="nb-dropbtn"> <svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#icon-cloud"></use></svg> Browse <svg class="icon icon-caret" viewBox="0 0 24 24"><use xlink:href="#icon-caret"></use></svg></button>
<div class="nb-mega-panel" aria-hidden="true">
<div class="nb-mega-row">
<div class="nb-mega-col">
<div class="nb-mega-title">Browse Artworks</div>
<ul>
<li><a href="/browse">All Artworks</a></li>
<li><a href="/photography">Photography</a></li>
<li><a href="/wallpapers">Wallpapers</a></li>
<li><a href="/skins">Skins</a></li>
<li><a href="/other">Other</a></li>
<li><a href="/featured-artworks">Featured</a></li>
</ul>
</div>
<div class="nb-mega-col">
<div class="nb-mega-title">View</div>
<ul>
<li><a href="/forum">Forum</a></li>
<li><a href="/chat">Chat</a></li>
<li><a href="/browse-categories">Categories</a></li>
<li><a href="/latest-artworks">Latest Uploads</a></li>
<li><a href="/daily-uploads">Recent Uploads</a></li>
<li><a href="/today-in-history">Today in History</a></li>
</ul>
</div>
<div class="nb-mega-col">
<div class="nb-mega-title">Authors</div>
<ul>
<li><a href="/interviews">Interviews</a></li>
<li><a href="/Members/MembersPhotos/545">Members Photos</a></li>
<li><a href="/top-authors">Top Authors</a></li>
<li><a href="/latest-comments">Latest Comments</a></li>
<li><a href="/monthly-commentators">Monthly Top Comments</a></li>
</ul>
</div>
<div class="nb-mega-col">
<div class="nb-mega-title">Statistics</div>
<ul>
<li><a href="/today-downloads">Today Downloads</a></li>
<li><a href="/top-favourites">Top Favourites</a></li>
</ul>
</div>
</div>
</div>
</li>
<li class="nb-dropdown">
<button class="nb-dropbtn"> <svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><use xlink:href="#icon-list"></use></svg> Categories <svg class="icon icon-caret" viewBox="0 0 24 24"><use xlink:href="#icon-caret"></use></svg></button>
<div class="nb-dropdown-panel" aria-hidden="true">
<ul>
<li><a href="/photography">Photography</a></li>
<li><a href="/wallpapers">Wallpapers</a></li>
<li><a href="/skins">Skins</a></li>
<li><a href="/other">Others</a></li>
<li class="nb-divider" role="separator"></li>
<li><a href="/browse-categories" class="btn_category">Categories List</a></li>
</ul>
</div>
</li>
</ul>
<ul class="nb-nav nb-right">
@auth
@php
$userId = auth()->id();
try {
$uploadCount = \Illuminate\Support\Facades\DB::table('artworks')->where('user_id', $userId)->count();
} catch (\Throwable $e) {
$uploadCount = 0;
}
try {
$favCount = \Illuminate\Support\Facades\DB::table('favourites')->where('user_id', $userId)->count();
} catch (\Throwable $e) {
$favCount = 0;
}
try {
$msgCount = \Illuminate\Support\Facades\DB::table('messages')->where('reciever_id', $userId)->whereNull('read_at')->count();
} catch (\Throwable $e) {
$msgCount = 0;
}
try {
$noticeCount = \Illuminate\Support\Facades\DB::table('notification')->where('user_id', $userId)->where('new', 1)->count();
} catch (\Throwable $e) {
$noticeCount = 0;
}
try {
$profile = \Illuminate\Support\Facades\DB::table('user_profiles')->where('user_id', $userId)->first();
$avatar = $profile->avatar ?? null;
} catch (\Throwable $e) {
$avatar = null;
}
$displayName = auth()->user()->name ?: (auth()->user()->username ?? '');
@endphp
<li class="nb-notice">
<a href="/upload" title="Upload new Artwork"><svg class="icon" viewBox="0 0 24 24"><use xlink:href="#icon-upload"></use></svg><br> {{ $uploadCount }}</a>
</li>
<li class="nb-notice">
<a href="/favourites/{{ $userId }}/{{ auth()->user()->username ?? '' }}" title="Your Favourite Artworks"><svg class="icon" viewBox="0 0 24 24"><use xlink:href="#icon-heart"></use></svg><br> {{ $favCount }}</a>
</li>
<li class="nb-notice">
<a href="/messages" title="Messages"><svg class="icon" viewBox="0 0 24 24"><use xlink:href="#icon-mail"></use></svg><br> {{ $msgCount }}</a>
</li>
<li class="nb-notice">
<a href="/notices" title="Notices"><svg class="icon" viewBox="0 0 24 24"><use xlink:href="#icon-bell"></use></svg><br> {{ $noticeCount }}</a>
</li>
<li class="nb-dropdown nb-user">
<button class="nb-dropbtn">
@if($avatar)
<img src="/storage/{{ ltrim($avatar, '/') }}" alt="{{ $displayName }}" width="20">&nbsp;&nbsp;
@endif
<span class="username">{{ $displayName }}</span>
<svg class="icon icon-caret" viewBox="0 0 24 24"><use xlink:href="#icon-caret"></use></svg>
</button>
<div class="nb-dropdown-panel">
<ul>
<li><a href="/upload">Upload</a></li>
<li><a href="{{ route('dashboard.artworks.index') }}">Edit Artworks</a></li>
<li class="nb-divider" role="presentation"></li>
<li><a href="/statistics">Statistics</a></li>
<li><a href="/mybuddies.php">My Followes</a></li>
<li><a href="/buddies.php">Who follows me</a></li>
<li class="nb-divider" role="presentation"></li>
<li><a href="/recieved-comments">Received Comments</a></li>
<li><a href="/favourites/{{ $userId }}/{{ auth()->user()->username ?? '' }}">My Favourites</a></li>
<li><a href="/gallery/{{ $userId }}/{{ auth()->user()->username ?? '' }}">My Gallery</a></li>
<li class="nb-divider" role="presentation"></li>
<li><a href="/user">Edit Profile</a></li>
<li><a href="/profile/{{ $userId }}/{{ auth()->user()->username ?? '' }}">View My Profile</a></li>
<li class="nb-dropdown-footer clearfix">
<form method="POST" action="/logout" style="margin:0;">
@csrf
<button type="submit" class="nb-btn-link">Logout</button>
</form>
</li>
</ul>
</div>
</li>
<li class="nb-chat">
<button class="nb-chat-toggle" title="Chat"><svg class="icon" viewBox="0 0 24 24"><use xlink:href="#icon-chat"></use></svg></button>
</li>
@else
<li class="nb-link"><a href="/signup" title="Signup for a new account">Join</a></li>
<li class="nb-link"><a href="/login" title="Login to Skinbase account">Sign in</a></li>
@endauth
</ul>
</div>
</div>
<!-- SVG icons -->
<svg style="display:none;" aria-hidden="true">
<symbol id="icon-search" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zM10 14a4 4 0 110-8 4 4 0 010 8z"/></symbol>
<symbol id="icon-cloud" viewBox="0 0 24 24"><path d="M19.36 10.46A7 7 0 005 9a5 5 0 00.11 10H19a4 4 0 00.36-8.54z"/></symbol>
<symbol id="icon-caret" viewBox="0 0 24 24"><path d="M7 10l5 5 5-5z"/></symbol>
<symbol id="icon-list" viewBox="0 0 24 24"><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zm0-8h14V7H7v2z"/></symbol>
<symbol id="icon-upload" viewBox="0 0 24 24"><path d="M19 15v4H5v-4H3v4a2 2 0 002 2h14a2 2 0 002-2v-4h-2zM11 19h2V9h3l-4-5-4 5h3z"/></symbol>
<symbol id="icon-heart" viewBox="0 0 24 24"><path d="M12 21s-7-4.35-9-7.03C-1.2 9.9 4.7 4 8.5 7.5 11 9.7 12 11 12 11s1-1.3 3.5-3.5C19.3 4 25.2 9.9 21 13.97 19 16.65 12 21 12 21z"/></symbol>
<symbol id="icon-mail" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></symbol>
<symbol id="icon-bell" viewBox="0 0 24 24"><path d="M12 22a2 2 0 002-2H10a2 2 0 002 2zm6-6V10c0-3.07-1.63-5.64-4.5-6.32V3a1.5 1.5 0 10-3 0v.68C7.63 4.36 6 6.92 6 10v6l-2 2v1h16v-1l-2-2z"/></symbol>
<symbol id="icon-chat" viewBox="0 0 24 24"><path d="M21 6h-2v9H7l-4 4V6a2 2 0 012-2h16a2 2 0 012 2z"/></symbol>
</svg>
</nav>
<!-- MOBILE MENU (hidden on desktop) -->
<div id="nb-mobile-menu" class="nb-mobile-menu" aria-hidden="true">
<div class="nb-mobile-inner">
@auth
<a href="/upload">Upload</a>
<a href="/favourites/{{ auth()->id() }}/{{ auth()->user()->username ?? '' }}">Favourites</a>
<a href="/messages">Messages</a>
<a href="/notices">Notices</a>
@else
<a href="/signup">Join</a>
<a href="/login">Sign in</a>
@endauth
<hr class="nb-mobile-sep">
<a href="/browse">All Artworks</a>
<a href="/photography">Photography</a>
<a href="/wallpapers">Wallpapers</a>
<a href="/skins">Skins</a>
<a href="/other">Other</a>
<a href="/featured-artworks">Featured</a>
<hr class="nb-mobile-sep">
<a href="/forum">Forum</a>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
</div>

View File

@@ -1,16 +1,32 @@
<div class="photo_frame" itemscope itemtype="http://schema.org/Photograph">
<a href="{{ $art->url ?? '#' }}" itemprop="url">
@php
$img_src = $art->thumb ?? '';
$img_srcset = $art->thumb_srcset ?? '';
$base_height = 360;
$orig_width = (int) ($art->width ?? 0);
$orig_height = (int) ($art->height ?? 0);
$img_height = $base_height;
$img_width = ($orig_width > 0 && $orig_height > 0)
? (int) round($orig_width * ($base_height / $orig_height))
: 600;
@endphp
<article class="artwork" itemscope itemtype="http://schema.org/Photograph">
<a href="{{ $art->url ?? '#' }}" itemprop="url">
<div class="ribbon gid_{{ $art->gid_num }}" title="{{ $art->category_name ?? '' }}" itemprop="genre">
<span>{{ $art->category_name ?? '' }}</span>
</div>
@php
$img_src = $art->thumb ?? '';
$img_srcset = $art->thumb_srcset ?? '';
@endphp
<img src="{{ $img_src }}" srcset="{{ $img_srcset }}" loading="lazy" decoding="async" alt="{{ e($art->name) }}" class="img-responsive" itemprop="thumbnailUrl">
<img
src="{{ $img_src }}"
srcset="{{ $img_srcset }}"
loading="lazy"
decoding="async"
alt="{{ e($art->name) }}"
width="{{ $img_width }}"
height="{{ $img_height }}"
class="img-responsive"
itemprop="thumbnailUrl"
>
<div class="details">
<div class="info" itemprop="author">
@@ -21,8 +37,5 @@
{{ $art->name }}
</div>
</div>
</a>
{{-- debug thumb links removed to avoid textual output beneath thumbnails --}}
</div>
</article>

View File

@@ -0,0 +1,44 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">{{ $page_title ?? 'Followers' }}</h1>
<p>Members who follow you</p>
</header>
</div>
<div class="container-fluid">
<div class="icon-grid">
@forelse($followers as $f)
@php
$icon = $f->icon ?? 'default.jpg';
$uname = $f->uname ?? 'Unknown';
$followerId = $f->user_id ?? null;
@endphp
<div class="icon-flex">
<div>
<a href="/profile/{{ $followerId }}/{{ Str::slug($uname) }}">
<h4>{{ $uname }}</h4>
</a>
</div>
<div>
<a href="/profile/{{ $followerId }}/{{ Str::slug($uname) }}">
<img src="/avatar/{{ $followerId }}/{{ rawurlencode($icon) }}" alt="{{ $uname }}">
</a>
</div>
</div>
@empty
<p>No followers yet.</p>
@endforelse
</div>
@if(method_exists($followers, 'links'))
<div class="mt-3">{{ $followers->links() }}</div>
@endif
</div>
</div>
@endsection

View File

@@ -5,74 +5,106 @@
@endphp
@section('content')
<div class="container-fluid legacy-page category-wrapper">
<div class="container-fluid legacy-page">
@php Banner::ShowResponsiveAd(); @endphp
<div id="category-artworks">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<div class="category-display"><i class="fa fa-bars fa-fw"></i></div>
<div class="category-wrapper">
<div class="row">
<main id="category-artworks" class="col-md-9">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<div class="category-display"><i class="fa fa-bars fa-fw"></i></div>
<div id="location_bar">
<a href="/{{ $contentType->slug }}" title="{{ $contentType->name }}">{{ $contentType->name }}</a>
@foreach ($category->breadcrumbs as $crumb)
&raquo; <a href="{{ $crumb->url }}" title="{{ $crumb->name }}">{{ $crumb->name }}</a>
@endforeach
:
</div>
<div id="location_bar">
<a href="/{{ $contentType->slug }}" title="{{ $contentType->name }}">{{ $contentType->name }}</a>
@foreach ($category->breadcrumbs as $crumb)
&raquo; <a href="{{ $crumb->url }}" title="{{ $crumb->name }}">{{ $crumb->name }}</a>
@endforeach
:
</div>
<h1 class="page-header">{{ $category->name }}</h1>
<p style="clear:both">{!! $category->description ?? ($contentType->name . ' artworks on Skinbase.') !!}</p>
</header>
</div>
<h1 class="page-header">{{ $category->name }}</h1>
<p style="clear:both">{!! $category->description ?? ($contentType->name . ' artworks on Skinbase.') !!}</p>
</header>
</div> <!-- end .effect2 -->
@if ($artworks->count())
<div class="container_photo gallery_box">
@foreach ($artworks as $art)
@include('legacy._artwork_card', ['art' => $art])
@endforeach
</div>
@else
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
<div class="panel-body">
<p>Once uploads arrive they will appear here. Check back soon.</p>
</div>
</div>
@endif
@if ($artworks->count())
<section id="gallery" class="gallery" aria-live="polite">
@foreach ($artworks as $art)
@include('legacy._artwork_card', ['art' => $art])
@endforeach
</section>
@else
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>No Artworks Yet</strong></div>
<div class="panel-body">
<p>Once uploads arrive they will appear here. Check back soon.</p>
</div>
</div>
@endif
<div class="paginationMenu text-center">
{{ $artworks->withQueryString()->links('pagination::bootstrap-4') }}
</div>
</div>
<div class="paginationMenu text-center">
{{ $artworks->withQueryString()->links('pagination::bootstrap-4') }}
</div> <!-- end .paginationMenu -->
</main> <!-- end #category-artworks -->
<div id="category-list">
<div id="artwork_subcategories">
<div class="category-toggle"><i class="fa fa-bars fa-fw"></i></div>
<aside id="category-list" class="col-md-3">
<div id="artwork_subcategories">
<div class="category-toggle"><i class="fa fa-bars fa-fw"></i></div>
<h5 class="browse_categories">Main Categories:</h5>
<ul>
@foreach ($rootCategories as $root)
<li>
<a href="{{ $root->url }}" title="{{ $root->name }}"><i class="fa fa-photo fa-fw"></i> {{ $root->name }}</a>
</li>
@endforeach
</ul>
<h5 class="browse_categories">Main Categories:</h5>
<ul>
@foreach ($rootCategories as $root)
<li>
<a href="{{ $root->url }}" title="{{ $root->name }}"><i class="fa fa-photo fa-fw"></i> {{ $root->name }}</a>
</li>
@endforeach
</ul>
<h5 class="browse_categories">Browse Subcategories:</h5>
<ul class="scrollContent" data-mcs-theme="dark">
@foreach ($subcategories as $sub)
@php $selected = $sub->id === $category->id ? 'selected_group' : ''; @endphp
<li class="subgroup {{ $selected }}">
<a href="{{ $sub->url }}">{{ $sub->name }}</a>
</li>
@endforeach
</ul>
</div>
</div>
</div>
<h5 class="browse_categories">Browse Subcategories:</h5>
<ul class="scrollContent" data-mcs-theme="dark">
@foreach ($subcategories as $sub)
@php $selected = $sub->id === $category->id ? 'selected_group' : ''; @endphp
<li class="subgroup {{ $selected }}">
<a href="{{ $sub->url }}">{{ $sub->name }}</a>
</li>
@endforeach
</ul>
</div> <!-- end #artwork_subcategories -->
</aside> <!-- end #category-list -->
</div> <!-- end .row -->
</div> <!-- end .category-wrapper -->
</div> <!-- end .legacy-page -->
@endsection
@push('scripts')
<script src="/js/legacy-gallery-init.js"></script>
@push('styles')
<style>
.gallery {
columns: 5 260px;
column-gap: 16px;
}
.artwork {
break-inside: avoid;
margin-bottom: 16px;
}
@media (max-width: 992px) {
.gallery {
columns: 3 220px;
}
}
@media (max-width: 768px) {
.gallery {
columns: 2 180px;
}
}
@media (max-width: 576px) {
.gallery {
columns: 1 100%;
}
}
</style>
@endpush

View File

@@ -0,0 +1,73 @@
@extends('layouts.legacy')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<h1 class="page-header">Favourites</h1>
<p class="text-muted">List of uploaded Artworks which were added to user's favourites list</p>
</div>
@if(session('status'))
<div class="alert alert-success">{{ session('status') }}</div>
@endif
<table class="table table-striped">
<thead>
<tr>
<th>Thumb</th>
<th>Name</th>
<th>Author</th>
<th>Date</th>
<th>Dls</th>
<th>Views</th>
<th>Zoom</th>
<th>Rating</th>
</tr>
</thead>
<tbody>
@foreach($results as $ar)
@php
$artId = (int)($ar->id ?? 0);
$nid = (int)($artId / 100);
$picture = rawurlencode($ar->picture ?? '');
@endphp
<tr>
<td style="width:80px;">
<a href="/art/{{ $artId }}/{{ $ar->slug }}"><img src="/files/archive/shots/{{ $nid }}/{{ $picture }}" height="60" alt="{{ $ar->name }}"></a>
</td>
<td>
@if(auth()->check() && auth()->id() == ($ar->user_id ?? null))
<form method="POST" action="{{ route('legacy.favourites.delete', ['userId' => auth()->id(), 'artworkId' => $artId]) }}" style="display:inline" onsubmit="return confirm('Really Remove From Favourites: {{ addslashes($ar->name ?? '') }}');">
@csrf
<button type="submit" class="btn btn-link"><img src="/gfx/icon_delete.gif" alt="Delete"></button>
</form>
@endif
<a href="/art/{{ $artId }}/{{ $ar->slug }}">{{ $ar->name }}</a>
</td>
<td><a href="/profile.php?uname={{ urlencode($ar->uname ?? '') }}">{{ $ar->uname ?? '' }}</a></td>
<td>{{ is_string($ar->datum) ? date('d.m.Y', strtotime($ar->datum)) : '' }}</td>
<td>{{ (int)($ar->dls ?? 0) }}</td>
<td>{{ (int)($ar->views ?? 0) }}</td>
<td>{{ e($ar->zoom ?? '') }}</td>
<td>{{ e($ar->rating ?? '') }}</td>
</tr>
@endforeach
</tbody>
</table>
{{-- Simple pagination controls --}}
@php
$pages = (int) ceil($total / $hits);
@endphp
@if($pages > 1)
<nav>
<ul class="pagination">
@for($i=1;$i<=$pages;$i++)
<li class="{{ $i == $page ? 'active' : '' }}"><a href="{{ route('legacy.favourites', ['id' => $user_id, 'page' => $i]) }}">{{ $i }}</a></li>
@endfor
</ul>
</nav>
@endif
</div>
@endsection

View File

@@ -0,0 +1,60 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Gallery: {{ $user->uname ?? $user->name ?? 'User' }}</h1>
<p>{{ $user->real_name ?? '' }}</p>
</header>
</div>
<div class="row">
<div class="col-md-8">
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>User Gallery</strong></div>
<div class="panel-body">
<div class="gallery-grid">
@foreach($artworks as $art)
<div class="thumb-card effect2">
<a href="/art/{{ $art->id }}/{{ Str::slug($art->name ?? '') }}" class="thumb-link">
<img src="{{ $art->thumb }}" srcset="{{ $art->thumb_srcset }}" alt="{{ $art->name }}" class="img-responsive" loading="lazy" decoding="async">
</a>
<div class="thumb-meta">
<div class="thumb-title">{{ $art->name }}</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="panel panel-default effect2">
<div class="panel-heading"><strong>User</strong></div>
<div class="panel-body">
<img src="/avatar/{{ (int)($user->user_id ?? $user->id) }}/{{ rawurlencode($user->icon ?? '') }}" class="img-responsive" style="max-width:120px;" alt="{{ $user->uname ?? $user->name }}">
<h3>{{ $user->uname ?? $user->name }}</h3>
<p>{{ $user->about_me ?? '' }}</p>
</div>
</div>
</div>
</div>
{{-- Simple pagination controls like legacy site --}}
@php
$pages = (int) ceil($total / $hits);
@endphp
@if($pages > 1)
<nav>
<ul class="pagination">
@for($i=1;$i<=$pages;$i++)
<li class="{{ $i == $page ? 'active' : '' }}"><a href="{{ url('/gallery/'.$user->id.'?page='.$i) }}">{{ $i }}</a></li>
@endfor
</ul>
</nav>
@endif
</div>
@endsection

View File

@@ -80,7 +80,7 @@
<br/><br/>
@if(!empty($ar->username))
@php $username = DB::connection('legacy')->table('users')->where('uname', $ar->username)->value('uname'); @endphp
@php $username = DB::table('users')->where('uname', $ar->username)->value('uname'); @endphp
<div class="interviewGuy">{{ $username }} Gallery (random order):</div><br/>
@foreach($artworks as $artwork)
@php $nid = (int)($artwork->id / 100); @endphp

View File

@@ -0,0 +1,56 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">{{ $page_title ?? 'My Buddies' }}</h1>
<p>List of members you are following</p>
</header>
</div>
<div class="container-fluid">
<div class="icon-grid">
@forelse($buddies as $b)
@php
$icon = $b->icon ?? 'default.jpg';
$uname = $b->uname ?? 'Unknown';
$friendId = $b->friend_id ?? $b->friendId ?? null;
@endphp
<div class="icon-flex">
<div>
<a href="/profile/{{ $friendId }}/{{ Str::slug($uname) }}">
<h4>{{ $uname }}</h4>
</a>
</div>
<div>
<a href="/profile/{{ $friendId }}/{{ Str::slug($uname) }}">
<img src="/avatar/{{ $friendId }}/{{ rawurlencode($icon) }}" alt="{{ $uname }}">
</a>
</div>
@if(auth()->check() && auth()->id() == ($b->user_id ?? null))
<div>
<form method="POST" action="{{ route('legacy.mybuddies.delete', ['id' => $b->id]) }}" onsubmit="return confirm('Really Remove From Friends List: {{ addslashes($uname) }}?');">
@csrf
@method('DELETE')
<button class="btn btn-link" type="submit"><img src="/gfx/icon_delete.gif" alt="remove"></button>
</form>
</div>
@endif
</div>
@empty
<p>No buddies yet.</p>
@endforelse
</div>
@if(method_exists($buddies, 'links'))
<div class="mt-3">{{ $buddies->links() }}</div>
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,55 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
@php \App\Banner::ShowResponsiveAd(); @endphp
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">{{ $page_title }}</h1>
<p style="clear:both">{!! $tidy ?? '' !!}</p>
</header>
</div>
<div class="container_photo gallery_box">
<div class="grid-sizer"></div>
@foreach($artworks as $art)
@include('legacy._artwork_card', ['art' => $art])
@endforeach
</div>
<div class="paginationMenu text-center">
{{ method_exists($artworks, 'withQueryString') ? $artworks->withQueryString()->links() : '' }}
</div>
@if($subcategories && $subcategories->count())
<div id="category-list">
<div id="artwork_subcategories">
<div class="category-toggle"><i class="fa fa-bars fa-fw"></i></div>
<h5 class="browse_categories">Main Categories:</h5>
<ul>
<li><a href="/photography"><i class="fa fa-photo fa-fw"></i> Photography</a></li>
<li><a href="/wallpapers"><i class="fa fa-photo fa-fw"></i> Wallpapers</a></li>
<li><a href="/skins"><i class="fa fa-photo fa-fw"></i> Skins</a></li>
<li><a href="/other"><i class="fa fa-photo fa-fw"></i> Other</a></li>
</ul>
<h5 class="browse_categories">Browse Subcategories:</h5>
<ul class="scrollContent" data-mcs-theme="dark">
@foreach($subcategories as $skupina)
@php
$slug = \Illuminate\Support\Str::slug($skupina->category_name);
$ctype = strtolower($group);
$addit = (isset($id) && $skupina->category_id == $id) ? 'selected_group' : '' ;
@endphp
<li class="subgroup {{ $addit }}">
<a href="/{{ $ctype }}/{{ $slug }}">{{ $skupina->category_name }}</a>
</li>
@endforeach
</ul>
</div>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,63 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Received Comments</h1>
<p>List of comments left on your uploaded artworks.</p>
</header>
</div>
<div class="masonry">
@foreach($comments as $comment)
@php
$author = $comment->user;
$art = $comment->artwork;
$created = optional($comment->created_at);
@endphp
<div class="masonry_item col-sm-6 col-md-4">
<div class="comment_box effect3">
<div class="cb_image">
<a href="/profile/{{ (int)($author->id ?? $author->user_id) }}/{{ rawurlencode($author->name ?? $author->uname ?? '') }}">
<img src="/avatar/{{ (int)($author->id ?? $author->user_id) }}/{{ rawurlencode($author->icon ?? '') }}" width="50" height="50" class="comment_avatar" alt="{{ $author->name ?? $author->uname ?? '' }}">
</a>
</div>
<div class="bubble_comment panel panel-skinbase">
<div class="panel-heading">
<div class="pull-right">{{ $created ? $created->diffForHumans() . ' ago' : '' }}</div>
<h5 class="panel-title">Comment by: <a href="/profile/{{ (int)($author->id ?? $author->user_id) }}/{{ rawurlencode($author->name ?? $author->uname ?? '') }}">{{ $author->name ?? $author->uname ?? '' }}</a></h5>
</div>
<div class="panel-body">
<div class="comment_box_image">
@if($art)
<a href="/art/{{ $art->id }}/{{ Str::slug($art->name ?? '') }}">
<img src="{{ $art->thumb }}" class="img-thumbnail img-responsive" alt="{{ $art->name }}">
</a>
@endif
</div>
<div class="comment_text">{!! nl2br(e($comment->content ?? $comment->description ?? '')) !!}</div>
@if(!empty($author->signature))
<div class="panel-footer comment-footer">
<p>{!! nl2br(e($author->signature)) !!}</p>
</div>
@endif
</div>
</div>
</div>
</div>
@endforeach
</div>
@if($comments->hasPages())
<div class="paginationMenu">
{{ $comments->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,81 @@
@extends('layouts.legacy')
@section('content')
<div class="container legacy-page">
<div class="page-header-wrap">
<div class="clearfix">
<h1 class="page-header" style="margin:0;display:inline-block">Artwork Statistics</h1>
<span class="text-muted pull-right" style="margin-top:10px">
Uploaded: {{ method_exists($artworks, 'total') ? $artworks->total() : $artworks->count() }}
</span>
</div>
<div class="text-muted" style="margin-top:6px;font-size:12px">Sort your uploads by popularity, downloads, comments, and date.</div>
</div>
<div class="panel panel-default uploads-panel">
<div class="panel-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover" style="margin-bottom:0">
<thead>
<tr>
<th style="width:90px" class="text-center">Thumbnail</th>
<th>
<a href="{{ request()->fullUrlWithQuery(['sort' => 'name']) }}">Name</a>
</th>
<th style="width:110px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'dls']) }}">Downloads</a>
</th>
<th style="width:95px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'comments']) }}">Reviews</a>
</th>
<th style="width:180px">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'category']) }}">Section</a>
</th>
<th style="width:130px" class="text-center">
<a href="{{ request()->fullUrlWithQuery(['sort' => 'date']) }}">Date</a>
</th>
</tr>
</thead>
<tbody>
@forelse($artworks as $art)
<tr>
<td class="text-center">
<a href="/art/{{ (int) $art->id }}" title="View">
<img
src="{{ $art->thumb_url ?? '/gfx/sb_join.jpg' }}"
@if(!empty($art->thumb_srcset)) srcset="{{ $art->thumb_srcset }}" @endif
alt="{{ $art->name ?? '' }}"
class="img-thumbnail"
style="width:70px;height:70px;object-fit:cover"
>
</a>
</td>
<td>
<a href="/art/{{ (int) $art->id }}">{{ $art->name ?? '' }}</a>
<div class="text-muted" style="font-size:12px">#{{ (int) $art->id }}</div>
</td>
<td class="text-center">{{ (int) ($art->dls ?? 0) }}</td>
<td class="text-center">{{ (int) ($art->num_comments ?? 0) }}</td>
<td>{{ $art->category_name ?? '' }}</td>
<td class="text-center">
@if(!empty($art->datum))
{{ is_string($art->datum) ? date('d.m.Y', strtotime($art->datum)) : '' }}
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6">No artworks found.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<div class="paginationMenu text-center">
{{ $artworks->links('pagination::bootstrap-3') }}
</div>
</div>
@endsection

View File

@@ -149,7 +149,7 @@
</a>
<ul class="dropdown-menu">
<li><a href="/upload"><i class="fa fa-upload"></i> Upload</a></li>
<li><a href="/manul.php"><i class="fa fa-cloud"></i> Edit Artworks</a></li>
<li><a href="{{ route('dashboard.artworks.index') }}"><i class="fa fa-cloud"></i> Edit Artworks</a></li>
<li role="presentation" class="divider"></li>
<li><a href="/statistics"><i class="fa fa-cog"></i> Statistics</a></li>
<li><a href="/mybuddies.php"><i class="fa fa-cog"></i> My Followes</a></li>

View File

@@ -0,0 +1,153 @@
@extends('layouts.legacy')
@section('content')
<div class="container-fluid legacy-page">
<div class="effect2 page-header-wrap">
<header class="page-heading">
<h1 class="page-header">Edit profile</h1>
<p>update your user account</p>
</header>
</div>
@if(session('status'))
<div class="alert alert-success">{{ session('status') }}</div>
@endif
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<form enctype="multipart/form-data" method="post" action="{{ route('legacy.user') }}" id="editUserProfileForm" role="form">
@csrf
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label><i class="fa fa-envelope"></i> Email:</label>
<input type="text" name="email" class="form-control" readonly value="{{ $user->email }}" />
</div>
<div class="form-group">
<label><i class="fa fa-user"></i> Username:</label>
<input type="text" name="uname" class="form-control" readonly value="{{ $user->uname ?? $user->name }}" />
</div>
<div class="form-group">
<label>Real name:</label>
<input type="text" name="real_name" class="form-control" value="{{ $user->real_name ?? '' }}" />
</div>
<div class="form-group">
<label><i class="fa fa-link"></i> Home page:</label>
<input type="text" name="web" class="form-control" value="{{ $user->web ?? '' }}" />
</div>
<div class="form-inline">
<label><i class="fa fa-birthday-cake"></i> Birthday</label><br>
<input maxlength="2" name="date1" class="form-control" size="2" placeholder="Day" value="{{ $birthDay ?? '' }}"> :
<select name="date2" class="form-control">
@for($i=1;$i<=12;$i++)
@php $mVal = str_pad($i, 2, '0', STR_PAD_LEFT); @endphp
<option value="{{ $mVal }}" {{ (isset($birthMonth) && $birthMonth == $mVal) ? 'selected' : '' }}>{{ DateTime::createFromFormat('!m',$i)->format('F') }}</option>
@endfor
</select> :
<input maxlength="4" class="form-control" name="date3" size="4" placeholder="year" value="{{ $birthYear ?? '' }}">
</div>
<br>
<div class="form-group well">
<label>Gender:</label><br>
<input name="gender" type="radio" value="M" {{ ($user->gender ?? '') == 'M' ? 'checked' : '' }}> Male<br>
<input name="gender" type="radio" value="F" {{ ($user->gender ?? '') == 'F' ? 'checked' : '' }}> Female<br>
<input name="gender" type="radio" value="X" {{ ($user->gender ?? '') == 'X' ? 'checked' : '' }}> N/A
</div>
<div class="form-group">
<label>Country:</label>
@if(isset($countries) && $countries->count())
<select name="country_code" class="form-control">
@foreach($countries as $c)
@php
$code = $c->country_code ?? ($c->code ?? null);
$label = $c->country_name ?? ($c->name ?? $code);
@endphp
<option value="{{ $code }}" {{ ($user->country_code ?? '') == $code ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
@else
<input type="text" name="country_code" class="form-control" value="{{ $user->country_code ?? '' }}">
@endif
</div>
<div class="form-group well">
<input name="newsletter" type="checkbox" value="1" {{ ($user->mlist ?? 0) ? 'checked' : '' }}> Mailing list<br>
<input name="friend_upload_notice" type="checkbox" value="1" {{ ($user->friend_upload_notice ?? 0) ? 'checked' : '' }}> Friends upload notice
</div>
<div class="form-group">
<label>Signature</label>
<textarea name="signature" class="form-control" style="width:100%; height:100px;">{{ $user->signature ?? '' }}</textarea>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" class="form-control" style="width:100%; height:100px;">{{ $user->description ?? '' }}</textarea>
</div>
</div>
<div class="col-md-6">
<div class="form-group well">
<label>Avatar:</label><br>
<input type="file" name="avatar" class="form-control">
@if(!empty($user->icon))
<div style="margin-top:10px"><img src="/avatar/{{ $user->id }}/{{ $user->icon }}" width="50"></div>
@endif
</div>
<div class="form-group well">
<label>Emotion Icon:</label><br>
<input type="file" name="emotion_icon" class="form-control">
</div>
<div class="form-group well">
<label>Personal picture:</label><br>
<input type="file" name="personal_picture" class="form-control">
@if(!empty($user->picture))
<div style="margin-top:10px"><img src="/user-picture/{{ $user->picture }}" style="max-width:300px"></div>
@endif
</div>
</div>
</div>
<div class="form-group">
<label>About Me</label>
<textarea name="about_me" class="summernote form-control" style="width:100%; height:100px;">{{ $user->about_me ?? '' }}</textarea>
</div>
<input type="hidden" name="confirm" value="true">
<input type="submit" class="btn btn-success" value="Update my profile">
</form>
<hr>
<h3>Change password</h3>
<form action="{{ route('legacy.user') }}" method="post">
@csrf
<div class="form-group">
<label>Current password</label>
<input type="password" name="oldpass" class="form-control">
</div>
<div class="form-group">
<label>New password</label>
<input type="password" name="newpass" class="form-control">
</div>
<div class="form-group">
<label>Retype new password</label>
<input type="password" name="newpass2" class="form-control">
</div>
<input type="hidden" name="confirm" value="true_password">
<input type="submit" class="btn btn-success" value="Change Password">
</form>
</div>
@endsection

View File

@@ -0,0 +1,36 @@
@if ($paginator->hasPages())
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled" aria-disabled="true"><span>&laquo;</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="Previous">&laquo;</a></li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="disabled" aria-disabled="true"><span>{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active" aria-current="page"><span>{{ $page }}</span></li>
@else
<li><a href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="Next">&raquo;</a></li>
@else
<li class="disabled" aria-disabled="true"><span>&raquo;</span></li>
@endif
</ul>
@endif

View File

@@ -13,6 +13,7 @@ use App\Http\Controllers\Legacy\DailyUploadsController;
use App\Http\Controllers\Legacy\ChatController;
use App\Http\Controllers\Legacy\ProfileController as LegacyProfileController;
use App\Http\Controllers\Legacy\TopFavouritesController;
use App\Http\Controllers\Legacy\FavouritesController;
use App\Http\Controllers\Legacy\TopAuthorsController;
use App\Http\Controllers\Legacy\TodayInHistoryController;
use App\Http\Controllers\Legacy\TodayDownloadsController;
@@ -21,7 +22,12 @@ use App\Http\Controllers\Legacy\MembersController;
use App\Http\Controllers\Legacy\LatestController;
use App\Http\Controllers\Legacy\LatestCommentsController;
use App\Http\Controllers\Legacy\InterviewController;
use App\Http\Controllers\Legacy\StatisticsController;
use App\Http\Controllers\BrowseCategoriesController;
use App\Http\Controllers\GalleryController;
use App\Http\Controllers\Legacy\ReceivedCommentsController;
use App\Http\Controllers\Legacy\UserController as LegacyUserController;
use App\Http\Controllers\Legacy\PhotographyController;
// Legacy site routes
Route::get('/', [HomeController::class, 'index'])->name('legacy.home');
@@ -42,6 +48,7 @@ Route::get('/category/{group}/{slug?}/{id?}', [CategoryController::class, 'show'
// Short legacy routes for top-level category URLs like /Photography/3
// Short legacy routes for top-level category URLs (mapped to CategoryController@show)
/*
Route::get('/Photography/{id}', [CategoryController::class, 'show'])
->defaults('group', 'Photography')
->where('id', '\\d+');
@@ -57,7 +64,7 @@ Route::get('/Skins/{id}', [CategoryController::class, 'show'])
Route::get('/Other/{id}', [CategoryController::class, 'show'])
->defaults('group', 'Other')
->where('id', '\\d+');
*/
Route::get('/browse', [BrowseController::class, 'index'])->name('legacy.browse');
Route::get('/featured', [FeaturedArtworksController::class, 'index'])->name('legacy.featured');
Route::get('/featured-artworks', [FeaturedArtworksController::class, 'index'])->name('legacy.featured_artworks');
@@ -66,10 +73,39 @@ Route::get('/chat', [ChatController::class, 'index'])->name('legacy.chat');
Route::get('/browse-categories', [BrowseCategoriesController::class, 'index'])->name('browse.categories');
// Support profile URLs with numeric id and optional slug: /profile/1/gregor
Route::get('/profile/{id}/{username?}', [LegacyProfileController::class, 'show'])
->where('id', '\\d+')
->name('legacy.profile.id');
// Backward-compatible route for /profile/{username}
Route::get('/profile/{username?}', [LegacyProfileController::class, 'show'])->name('legacy.profile');
Route::get('/top-favourites', [TopFavouritesController::class, 'index'])->name('legacy.top_favourites');
Route::get('/top-authors', [TopAuthorsController::class, 'index'])->name('legacy.top_authors');
// My buddies / who I follow (legacy)
Route::middleware('auth')->get('/mybuddies.php', [\App\Http\Controllers\Legacy\MyBuddiesController::class, 'index'])->name('legacy.mybuddies.php');
Route::middleware('auth')->get('/mybuddies', [\App\Http\Controllers\Legacy\MyBuddiesController::class, 'index'])->name('legacy.mybuddies');
Route::middleware('auth')->delete('/mybuddies/{id}', [\App\Http\Controllers\Legacy\MyBuddiesController::class, 'destroy'])->name('legacy.mybuddies.delete');
// Who follows me (legacy)
Route::middleware('auth')->get('/buddies.php', [\App\Http\Controllers\Legacy\BuddiesController::class, 'index'])->name('legacy.buddies.php');
Route::middleware('auth')->get('/buddies', [\App\Http\Controllers\Legacy\BuddiesController::class, 'index'])->name('legacy.buddies');
// User favourites (port of oldSite /favourites.php)
Route::get('/favourites/{id?}/{username?}', [FavouritesController::class, 'index'])->name('legacy.favourites');
Route::post('/favourites/{userId}/delete/{artworkId}', [FavouritesController::class, 'destroy'])->name('legacy.favourites.delete');
// User gallery (port of oldSite gallery link)
Route::get('/gallery/{id}/{username?}', [GalleryController::class, 'show'])->name('legacy.gallery');
// Received comments (requires authentication)
Route::middleware('auth')->get('/recieved-comments', [ReceivedCommentsController::class, 'index'])->name('legacy.received_comments');
// User account settings (legacy /user)
Route::middleware('auth')->match(['get','post'], '/user', [LegacyUserController::class, 'index'])->name('legacy.user');
// Content-type landing pages (legacy look)
Route::get('/photography', [PhotographyController::class, 'index'])->name('legacy.photography');
Route::get('/today-in-history', [TodayInHistoryController::class, 'index'])->name('legacy.today_in_history');
Route::get('/today-downloads', [TodayDownloadsController::class, 'index'])->name('legacy.today_downloads');
@@ -79,3 +115,7 @@ Route::get('/members', [MembersController::class, 'index'])->name('legacy.member
Route::get('/latest', [LatestController::class, 'index'])->name('legacy.latest');
Route::get('/latest-comments', [LatestCommentsController::class, 'index'])->name('legacy.latest_comments');
Route::get('/interviews', [InterviewController::class, 'index'])->name('legacy.interviews');
Route::middleware(['auth'])->group(function () {
Route::get('/statistics', [StatisticsController::class, 'index'])->name('legacy.statistics');
});

View File

@@ -2,6 +2,7 @@
use App\Http\Controllers\ProfileController;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Dashboard\ArtworkController as DashboardArtworkController;
// Legacy routes are defined in routes/legacy.php and provide the site's
// legacy pages (art, forum, news, profile, etc.). We keep the auth routes
@@ -12,6 +13,13 @@ Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Route::middleware(['auth'])->prefix('dashboard')->name('dashboard.')->group(function () {
Route::get('/artworks', [DashboardArtworkController::class, 'index'])->name('artworks.index');
Route::get('/artworks/{id}/edit', [DashboardArtworkController::class, 'edit'])->whereNumber('id')->name('artworks.edit');
Route::put('/artworks/{id}', [DashboardArtworkController::class, 'update'])->whereNumber('id')->name('artworks.update');
Route::delete('/artworks/{id}', [DashboardArtworkController::class, 'destroy'])->whereNumber('id')->name('artworks.destroy');
});
Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
@@ -22,6 +30,8 @@ Route::middleware('auth')->group(function () {
require __DIR__.'/auth.php';
Route::view('/blank', 'blank')->name('blank');
// Artwork public show (slug-based). This must come before the category route so the artwork
// slug (last segment) is matched correctly while allowing multi-segment category paths.
Route::get('/{contentTypeSlug}/{categoryPath}/{artwork}', [\App\Http\Controllers\ArtworkController::class, 'show'])

2
storage/debugbar/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -4,7 +4,12 @@ import laravel from 'laravel-vite-plugin';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
input: [
'resources/css/app.css',
'resources/js/app.js',
'resources/scss/nova.scss',
'resources/js/nova.js'
],
refresh: true,
}),
],