mirror of
https://github.com/sp-tarkov/forge.git
synced 2025-02-13 04:30:41 -05:00
Merge branch 'user-profiles' into develop
This commit is contained in:
commit
65e416e4d9
@ -21,12 +21,17 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', Rule::unique('users')->ignore($user->id)],
|
||||
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
|
||||
'cover' => ['nullable', 'mimes:jpg,jpeg,png', 'max:2048'],
|
||||
])->validateWithBag('updateProfileInformation');
|
||||
|
||||
if (isset($input['photo'])) {
|
||||
$user->updateProfilePhoto($input['photo']);
|
||||
}
|
||||
|
||||
if (isset($input['cover'])) {
|
||||
$user->updateCoverPhoto($input['cover']);
|
||||
}
|
||||
|
||||
if ($input['email'] !== $user->email &&
|
||||
$user instanceof MustVerifyEmail) {
|
||||
$this->updateVerifiedUser($user, $input);
|
||||
|
22
app/Http/Controllers/Api/V0/ApiController.php
Normal file
22
app/Http/Controllers/Api/V0/ApiController.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ApiController extends Controller
|
||||
{
|
||||
public static function shouldInclude(string $relationship): bool
|
||||
{
|
||||
$param = request()->get('include');
|
||||
|
||||
if (! $param) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$includeValues = explode(',', Str::lower($param));
|
||||
|
||||
return in_array(Str::lower($relationship), $includeValues);
|
||||
}
|
||||
}
|
@ -2,13 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V0\StoreModRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateModRequest;
|
||||
use App\Http\Resources\Api\V0\ModResource;
|
||||
use App\Models\Mod;
|
||||
|
||||
class ModController extends Controller
|
||||
class ModController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
|
@ -2,13 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V0;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Api\V0\StoreUserRequest;
|
||||
use App\Http\Requests\Api\V0\UpdateUserRequest;
|
||||
use App\Http\Resources\Api\V0\UserResource;
|
||||
use App\Models\User;
|
||||
|
||||
class UsersController extends Controller
|
||||
class UsersController extends ApiController
|
||||
{
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
|
25
app/Http/Controllers/UserController.php
Normal file
25
app/Http/Controllers/UserController.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public function show(Request $request, User $user, string $username)
|
||||
{
|
||||
if ($user->slug() !== $username) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if ($request->user()?->cannot('view', $user)) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return view('user.show', compact('user'));
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Resources\Api\V0;
|
||||
|
||||
use App\Http\Controllers\Api\V0\ApiController;
|
||||
use App\Models\Mod;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
@ -35,23 +36,29 @@ class ModResource extends JsonResource
|
||||
'published_at' => $this->published_at,
|
||||
],
|
||||
'relationships' => [
|
||||
'users' => [
|
||||
'data' => $this->users->map(fn ($user) => [
|
||||
'users' => $this->users->map(fn ($user) => [
|
||||
'data' => [
|
||||
'type' => 'user',
|
||||
'id' => $user->id,
|
||||
])->toArray(),
|
||||
|
||||
// TODO: Provide 'links.self' to user profile
|
||||
//'links' => ['self' => '#'],
|
||||
],
|
||||
],
|
||||
'links' => [
|
||||
'self' => $user->profileUrl(),
|
||||
],
|
||||
])->toArray(),
|
||||
'license' => [
|
||||
'data' => [
|
||||
'type' => 'license',
|
||||
'id' => $this->license_id,
|
||||
[
|
||||
'data' => [
|
||||
'type' => 'license',
|
||||
'id' => $this->license_id,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'included' => $this->users->map(fn ($user) => new UserResource($user)),
|
||||
|
||||
'includes' => $this->when(
|
||||
ApiController::shouldInclude('users'),
|
||||
fn () => $this->users->map(fn ($user) => new UserResource($user))
|
||||
),
|
||||
|
||||
// TODO: Provide 'included' data for attached 'license':
|
||||
//new LicenseResource($this->license)
|
||||
|
@ -30,11 +30,13 @@ class UserResource extends JsonResource
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
// TODO: Provide 'included' data for attached 'user_role'
|
||||
//'included' => [new UserRoleResource($this->role)],
|
||||
|
||||
// TODO: Provide 'links.self' to user profile:
|
||||
//'links' => ['self' => '#'],
|
||||
'links' => [
|
||||
'self' => $this->profileUrl(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
// Stream some data locally so that we don't have to keep accessing the Hub's database. Use MySQL temporary
|
||||
// tables to store the data to save on memory; we don't want this to be a hog.
|
||||
$this->bringUserAvatarLocal();
|
||||
$this->bringFileAuthorsLocal();
|
||||
$this->bringFileOptionsLocal();
|
||||
$this->bringFileContentLocal();
|
||||
@ -58,6 +59,34 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
Artisan::call('cache:clear');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the user avatar table from the Hub database to the local database temporary table.
|
||||
*/
|
||||
protected function bringUserAvatarLocal(): void
|
||||
{
|
||||
DB::statement('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||
DB::statement('CREATE TEMPORARY TABLE temp_user_avatar (
|
||||
avatarID INT,
|
||||
avatarExtension VARCHAR(255),
|
||||
userID INT,
|
||||
fileHash VARCHAR(255)
|
||||
)');
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_user_avatar')
|
||||
->orderBy('avatarID')
|
||||
->chunk(200, function ($avatars) {
|
||||
foreach ($avatars as $avatar) {
|
||||
DB::table('temp_user_avatar')->insert([
|
||||
'avatarID' => (int) $avatar->avatarID,
|
||||
'avatarExtension' => $avatar->avatarExtension,
|
||||
'userID' => (int) $avatar->userID,
|
||||
'fileHash' => $avatar->fileHash,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the file authors from the Hub database to the local database temporary table.
|
||||
*/
|
||||
@ -172,15 +201,33 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
*/
|
||||
protected function importUsers(): void
|
||||
{
|
||||
// Initialize a cURL handler for downloading mod thumbnails.
|
||||
$curl = curl_init();
|
||||
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
|
||||
|
||||
DB::connection('mysql_hub')
|
||||
->table('wcf1_user as u')
|
||||
->select('u.userID', 'u.username', 'u.email', 'u.password', 'u.registrationDate', 'u.banned', 'u.banReason', 'u.banExpires', 'u.rankID', 'r.rankTitle')
|
||||
->select(
|
||||
'u.userID',
|
||||
'u.username',
|
||||
'u.email',
|
||||
'u.password',
|
||||
'u.registrationDate',
|
||||
'u.banned',
|
||||
'u.banReason',
|
||||
'u.banExpires',
|
||||
'u.coverPhotoHash',
|
||||
'u.coverPhotoExtension',
|
||||
'u.rankID',
|
||||
'r.rankTitle',
|
||||
)
|
||||
->leftJoin('wcf1_user_rank as r', 'u.rankID', '=', 'r.rankID')
|
||||
->chunkById(250, function (Collection $users) {
|
||||
->chunkById(250, function (Collection $users) use ($curl) {
|
||||
$userData = $bannedUsers = $userRanks = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$userData[] = $this->collectUserData($user);
|
||||
$userData[] = $this->collectUserData($curl, $user);
|
||||
|
||||
$bannedUserData = $this->collectBannedUserData($user);
|
||||
if ($bannedUserData) {
|
||||
@ -197,15 +244,20 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
$this->handleBannedUsers($bannedUsers);
|
||||
$this->handleUserRoles($userRanks);
|
||||
}, 'userID');
|
||||
|
||||
// Close the cURL handler.
|
||||
curl_close($curl);
|
||||
}
|
||||
|
||||
protected function collectUserData($user): array
|
||||
protected function collectUserData(CurlHandle $curl, object $user): array
|
||||
{
|
||||
return [
|
||||
'hub_id' => (int) $user->userID,
|
||||
'name' => $user->username,
|
||||
'email' => mb_convert_case($user->email, MB_CASE_LOWER, 'UTF-8'),
|
||||
'email' => Str::lower($user->email),
|
||||
'password' => $this->cleanPasswordHash($user->password),
|
||||
'profile_photo_path' => $this->fetchUserAvatar($curl, $user),
|
||||
'cover_photo_path' => $this->fetchUserCoverPhoto($curl, $user),
|
||||
'created_at' => $this->cleanRegistrationDate($user->registrationDate),
|
||||
'updated_at' => now('UTC')->toDateTimeString(),
|
||||
];
|
||||
@ -224,6 +276,75 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
return str_starts_with($clean, '$2') ? $clean : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user avatar from the Hub and store it anew.
|
||||
*/
|
||||
protected function fetchUserAvatar(CurlHandle $curl, object $user): string
|
||||
{
|
||||
// Fetch the user's avatar data from the temporary table.
|
||||
$avatar = DB::table('temp_user_avatar')->where('userID', $user->userID)->first();
|
||||
|
||||
if (! $avatar) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hashShort = substr($avatar->fileHash, 0, 2);
|
||||
$fileName = $avatar->fileHash.'.'.$avatar->avatarExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/avatars/'.$hashShort.'/'.$avatar->avatarID.'-'.$fileName;
|
||||
$relativePath = 'user-avatars/'.$fileName;
|
||||
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and store an image from the Hub.
|
||||
*/
|
||||
protected function fetchAndStoreImage(CurlHandle $curl, string $hubUrl, string $relativePath): string
|
||||
{
|
||||
// Determine the disk to use based on the environment.
|
||||
$disk = match (config('app.env')) {
|
||||
'production' => 'r2', // Cloudflare R2 Storage
|
||||
default => 'public', // Local
|
||||
};
|
||||
|
||||
// Check to make sure the image doesn't already exist.
|
||||
if (Storage::disk($disk)->exists($relativePath)) {
|
||||
return $relativePath; // Already exists, return the path.
|
||||
}
|
||||
|
||||
// Download the image using the cURL handler.
|
||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||
$image = curl_exec($curl);
|
||||
|
||||
if ($image === false) {
|
||||
Log::error('There was an error attempting to download the image. cURL error: '.curl_error($curl));
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Store the image on the disk.
|
||||
Storage::disk($disk)->put($relativePath, $image);
|
||||
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the user avatar from the Hub and store it anew.
|
||||
*/
|
||||
protected function fetchUserCoverPhoto(CurlHandle $curl, object $user): string
|
||||
{
|
||||
if (empty($user->coverPhotoHash) || empty($user->coverPhotoExtension)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hashShort = substr($user->coverPhotoHash, 0, 2);
|
||||
$fileName = $user->coverPhotoHash.'.'.$user->coverPhotoExtension;
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/images/coverPhotos/'.$hashShort.'/'.$user->userID.'-'.$fileName;
|
||||
$relativePath = 'user-covers/'.$fileName;
|
||||
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean the registration date from the Hub database.
|
||||
*/
|
||||
@ -295,9 +416,6 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Build an array of user rank data ready to be inserted into the local database.
|
||||
*/
|
||||
protected function collectUserRankData($user): ?array
|
||||
{
|
||||
if ($user->rankID && $user->rankTitle) {
|
||||
@ -590,31 +708,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
$hubUrl = 'https://hub.sp-tarkov.com/files/images/file/'.$hashShort.'/'.$fileName;
|
||||
$relativePath = 'mods/'.$fileName;
|
||||
|
||||
// Determine the disk to use based on the environment.
|
||||
$disk = match (config('app.env')) {
|
||||
'production' => 'r2', // Cloudflare R2 Storage
|
||||
default => 'public', // Local
|
||||
};
|
||||
|
||||
// Check to make sure the image doesn't already exist.
|
||||
if (Storage::disk($disk)->exists($relativePath)) {
|
||||
return $relativePath; // Already exists, return the path.
|
||||
}
|
||||
|
||||
// Download the image using the cURL handler.
|
||||
curl_setopt($curl, CURLOPT_URL, $hubUrl);
|
||||
$image = curl_exec($curl);
|
||||
|
||||
if ($image === false) {
|
||||
Log::error('There was an error attempting to download a mod thumbnail. cURL error: '.curl_error($curl));
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Store the image on the disk.
|
||||
Storage::disk($disk)->put($relativePath, $image);
|
||||
|
||||
return $relativePath;
|
||||
return $this->fetchAndStoreImage($curl, $hubUrl, $relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -693,6 +787,7 @@ class ImportHubData implements ShouldBeUnique, ShouldQueue
|
||||
public function failed(Exception $exception): void
|
||||
{
|
||||
// Explicitly drop the temporary tables.
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_user_avatar');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_author');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_option_values');
|
||||
DB::unprepared('DROP TEMPORARY TABLE IF EXISTS temp_file_content');
|
||||
|
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
76
app/Livewire/Profile/UpdateProfileForm.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Profile;
|
||||
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
|
||||
use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm;
|
||||
use Livewire\Features\SupportRedirects\Redirector;
|
||||
|
||||
class UpdateProfileForm extends UpdateProfileInformationForm
|
||||
{
|
||||
/**
|
||||
* The new cover photo for the user.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $cover;
|
||||
|
||||
/**
|
||||
* When the photo is temporarily uploaded.
|
||||
*/
|
||||
public function updatedPhoto(): void
|
||||
{
|
||||
$this->validate([
|
||||
'photo' => 'image|mimes:jpg,jpeg,png|max:1024', // 1MB Max
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the cover is temporarily uploaded.
|
||||
*/
|
||||
public function updatedCover(): void
|
||||
{
|
||||
$this->validate([
|
||||
'cover' => 'image|mimes:jpg,jpeg,png|max:2048', // 2MB Max
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's profile information.
|
||||
*/
|
||||
public function updateProfileInformation(UpdatesUserProfileInformation $updater): RedirectResponse|Redirector|null
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
|
||||
$updater->update(
|
||||
Auth::user(),
|
||||
$this->photo || $this->cover
|
||||
? array_merge($this->state, array_filter([
|
||||
'photo' => $this->photo,
|
||||
'cover' => $this->cover,
|
||||
])) : $this->state
|
||||
);
|
||||
|
||||
if (isset($this->photo) || isset($this->cover)) {
|
||||
return redirect()->route('profile.show');
|
||||
}
|
||||
|
||||
$this->dispatch('saved');
|
||||
|
||||
$this->dispatch('refresh-navigation-menu');
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user's profile photo.
|
||||
*/
|
||||
public function deleteCoverPhoto(): void
|
||||
{
|
||||
Auth::user()->deleteCoverPhoto();
|
||||
|
||||
$this->dispatch('refresh-navigation-menu');
|
||||
}
|
||||
}
|
@ -11,6 +11,9 @@ class License extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The relationship between a license and mod.
|
||||
*/
|
||||
public function mods(): HasMany
|
||||
{
|
||||
return $this->hasMany(Mod::class);
|
||||
|
@ -25,6 +25,9 @@ class Mod extends Model
|
||||
{
|
||||
use HasFactory, Searchable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Post boot method to configure the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Apply the global scope to exclude disabled mods.
|
||||
@ -34,18 +37,24 @@ class Mod extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* The users that belong to the mod.
|
||||
* The relationship between a mod and its users.
|
||||
*/
|
||||
public function users(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its license.
|
||||
*/
|
||||
public function license(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(License::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its versions.
|
||||
*/
|
||||
public function versions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class)->orderByDesc('version');
|
||||
@ -62,6 +71,9 @@ class Mod extends Model
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod and its last updated version.
|
||||
*/
|
||||
public function lastUpdatedVersion(): HasOne
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
@ -69,7 +81,7 @@ class Mod extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indexable data array for the model.
|
||||
* The data that is searchable by Scout.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
@ -97,11 +109,12 @@ class Mod extends Model
|
||||
{
|
||||
return $this->hasOne(ModVersion::class)
|
||||
->orderByDesc('version')
|
||||
->orderByDesc('updated_at')
|
||||
->take(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model should be searchable.
|
||||
* Determine if the model instance should be searchable.
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
@ -109,7 +122,7 @@ class Mod extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the thumbnail.
|
||||
* Build the URL to the mod's thumbnail.
|
||||
*/
|
||||
public function thumbnailUrl(): Attribute
|
||||
{
|
||||
@ -121,7 +134,7 @@ class Mod extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disk where the thumbnail is stored.
|
||||
* Get the disk where the thumbnail is stored based on the current environment.
|
||||
*/
|
||||
protected function thumbnailDisk(): string
|
||||
{
|
||||
@ -131,6 +144,9 @@ class Mod extends Model
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
@ -142,7 +158,7 @@ class Mod extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the slug is always lower case when retrieved and slugified when saved.
|
||||
* Mutate the slug attribute to always be lower case on get and slugified on set.
|
||||
*/
|
||||
protected function slug(): Attribute
|
||||
{
|
||||
|
@ -19,12 +19,18 @@ class ModVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Post boot method to configure the model.
|
||||
*/
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::addGlobalScope(new DisabledScope);
|
||||
static::addGlobalScope(new PublishedScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and mod.
|
||||
*/
|
||||
public function mod(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Mod::class);
|
||||
@ -38,6 +44,9 @@ class ModVersion extends Model
|
||||
return $this->hasMany(ModDependency::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a mod version and SPT version.
|
||||
*/
|
||||
public function sptVersion(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(SptVersion::class);
|
||||
|
@ -11,6 +11,9 @@ class SptVersion extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The relationship between an SPT version and mod version.
|
||||
*/
|
||||
public function modVersions(): HasMany
|
||||
{
|
||||
return $this->hasMany(ModVersion::class);
|
||||
|
@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use App\Notifications\ResetPassword;
|
||||
use App\Notifications\VerifyEmail;
|
||||
use App\Traits\HasCoverPhoto;
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@ -21,6 +22,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
use Bannable;
|
||||
use HasApiTokens;
|
||||
use HasCoverPhoto;
|
||||
use HasFactory;
|
||||
use HasProfilePhoto;
|
||||
use Notifiable;
|
||||
@ -38,11 +40,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
'profile_photo_url',
|
||||
];
|
||||
|
||||
/**
|
||||
* The relationship between a user and their mods.
|
||||
*/
|
||||
public function mods(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Mod::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* The data that is searchable by Scout.
|
||||
*/
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
@ -51,28 +59,25 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the model instance should be searchable.
|
||||
*/
|
||||
public function shouldBeSearchable(): bool
|
||||
{
|
||||
return ! is_null($this->email_verified_at);
|
||||
}
|
||||
|
||||
public function assignRole(UserRole $role): bool
|
||||
{
|
||||
$this->role()->associate($role);
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserRole::class, 'user_role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has the role of a moderator.
|
||||
*/
|
||||
public function isMod(): bool
|
||||
{
|
||||
return Str::lower($this->role?->name) === 'moderator';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has the role of an administrator.
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return Str::lower($this->role?->name) === 'administrator';
|
||||
@ -94,12 +99,41 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
$this->notify(new ResetPassword($token));
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
/**
|
||||
* Get the relative URL to the user's profile page.
|
||||
*/
|
||||
public function profileUrl(): string
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
return route('user.show', [
|
||||
'user' => $this->id,
|
||||
'username' => $this->slug(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the slug of the user's name.
|
||||
*/
|
||||
public function slug(): string
|
||||
{
|
||||
return Str::lower(Str::slug($this->name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a role to the user.
|
||||
*/
|
||||
public function assignRole(UserRole $role): bool
|
||||
{
|
||||
$this->role()->associate($role);
|
||||
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* The relationship between a user and their role.
|
||||
*/
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(UserRole::class, 'user_role_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -107,9 +141,17 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||
*/
|
||||
protected function profilePhotoDisk(): string
|
||||
{
|
||||
return match (config('app.env')) {
|
||||
'production' => 'r2', // Cloudflare R2 Storage
|
||||
default => 'public', // Local
|
||||
};
|
||||
return config('filesystems.asset_upload', 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* The attributes that should be cast to native types.
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,9 @@ class UserRole extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The relationship between a user role and users.
|
||||
*/
|
||||
public function users(): HasMany
|
||||
{
|
||||
return $this->hasMany(User::class);
|
||||
|
@ -8,7 +8,7 @@ use App\Models\User;
|
||||
class ModPolicy
|
||||
{
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
* Determine whether the user can view multiple models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
@ -16,7 +16,7 @@ class ModPolicy
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
* Determine whether the user can view a specific model.
|
||||
*/
|
||||
public function view(?User $user, Mod $mod): bool
|
||||
{
|
||||
|
47
app/Policies/UserPolicy.php
Normal file
47
app/Policies/UserPolicy.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class UserPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function view(User $userCurrent, User $userResource): bool
|
||||
{
|
||||
// TODO: check to see if the userResource has blocked the userCurrent.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function update(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function delete(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function restore(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function forceDelete(User $user, User $model): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
72
app/Traits/HasCoverPhoto.php
Normal file
72
app/Traits/HasCoverPhoto.php
Normal file
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
trait HasCoverPhoto
|
||||
{
|
||||
/**
|
||||
* Update the user's cover photo.
|
||||
*/
|
||||
public function updateCoverPhoto(UploadedFile $cover, $storagePath = 'cover-photos'): void
|
||||
{
|
||||
tap($this->cover_photo_path, function ($previous) use ($cover, $storagePath) {
|
||||
$this->forceFill([
|
||||
'cover_photo_path' => $cover->storePublicly(
|
||||
$storagePath, ['disk' => $this->coverPhotoDisk()]
|
||||
),
|
||||
])->save();
|
||||
|
||||
if ($previous) {
|
||||
Storage::disk($this->coverPhotoDisk())->delete($previous);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the disk that cover photos should be stored on.
|
||||
*/
|
||||
protected function coverPhotoDisk(): string
|
||||
{
|
||||
return config('filesystems.asset_upload', 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the user's cover photo.
|
||||
*/
|
||||
public function deleteCoverPhoto(): void
|
||||
{
|
||||
if (is_null($this->cover_photo_path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage::disk($this->coverPhotoDisk())->delete($this->cover_photo_path);
|
||||
|
||||
$this->forceFill([
|
||||
'cover_photo_path' => null,
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to the user's cover photo.
|
||||
*/
|
||||
public function coverPhotoUrl(): Attribute
|
||||
{
|
||||
return Attribute::get(function (): string {
|
||||
return $this->cover_photo_path
|
||||
? Storage::disk($this->coverPhotoDisk())->url($this->cover_photo_path)
|
||||
: $this->defaultCoverPhotoUrl();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default profile photo URL if no profile photo has been uploaded.
|
||||
*/
|
||||
protected function defaultCoverPhotoUrl(): string
|
||||
{
|
||||
return 'https://picsum.photos/seed/'.urlencode($this->name).'/720/100?blur=2';
|
||||
}
|
||||
}
|
@ -15,6 +15,18 @@ return [
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Asset Upload Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that assets should be
|
||||
| uploaded to. Typically, this will be either the "public" or "r2" disk.
|
||||
|
|
||||
*/
|
||||
|
||||
'asset_upload' => env('ASSET_UPLOAD_DISK', 'public'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|
@ -207,7 +207,7 @@ return [
|
||||
'maxJobs' => 0,
|
||||
'memory' => 256,
|
||||
'tries' => 1,
|
||||
'timeout' => 900, // 15 Minutes
|
||||
'timeout' => 1500, // 25 Minutes
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
|
@ -64,17 +64,17 @@ return [
|
||||
*/
|
||||
|
||||
'temporary_file_upload' => [
|
||||
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
||||
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||
'disk' => null,
|
||||
'rules' => ['file', 'max:12288'],
|
||||
'directory' => null,
|
||||
'middleware' => 'throttle:5,1',
|
||||
'preview_mimes' => [
|
||||
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||
],
|
||||
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
||||
'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
|
||||
'max_upload_time' => 5,
|
||||
'cleanup' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|
@ -29,7 +29,8 @@ return new class extends Migration
|
||||
->nullOnDelete()
|
||||
->cascadeOnUpdate();
|
||||
$table->rememberToken();
|
||||
$table->string('profile_photo_path', 2048)->nullable();
|
||||
$table->string('profile_photo_path', 2048)->nullable()->default(null);
|
||||
$table->string('cover_photo_path', 2048)->nullable()->default(null);
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
|
@ -21,7 +21,9 @@
|
||||
{{ $mod->{$versionScope}->sptVersion->version }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">By {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
||||
<p class="text-sm italic text-slate-600 dark:text-gray-200">
|
||||
By {{ $mod->users->pluck('name')->implode(', ') }}
|
||||
</p>
|
||||
<p class="mt-2 text-slate-500 dark:text-gray-300">{{ $mod->teaser }}</p>
|
||||
</div>
|
||||
<x-mod-list-stats :mod="$mod" :modVersion="$mod->{$versionScope}"/>
|
||||
|
@ -32,7 +32,12 @@
|
||||
{{ $latestVersion->sptVersion->version }}
|
||||
</span>
|
||||
</div>
|
||||
<p>{{ __('Created by') }} {{ $mod->users->pluck('name')->implode(', ') }}</p>
|
||||
<p>
|
||||
{{ __('Created by') }}
|
||||
@foreach ($mod->users as $user)
|
||||
<a href="{{ $user->profileUrl() }}" class="text-slate-600 dark:text-gray-200 hover:underline">{{ $user->name }}</a>{{ $loop->last ? '' : ',' }}
|
||||
@endforeach
|
||||
</p>
|
||||
<p>{{ $latestVersion->sptVersion->version }} {{ __('Compatible') }}</p>
|
||||
<p>{{ Number::format($mod->total_downloads) }} {{ __('Downloads') }}</p>
|
||||
</div>
|
||||
|
@ -63,18 +63,24 @@
|
||||
</svg>
|
||||
{{ __('Dashboard') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col py-1.5">
|
||||
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||
<a href="{{ auth()->user()->profileUrl() }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM3.751 20.105a8.25 8.25 0 0116.498 0 .75.75 0 01-.437.695A18.683 18.683 0 0112 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 01-.437-.695z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ __('Profile') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col py-1.5">
|
||||
<a href="{{ route('profile.show') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ __('Edit Profile') }}
|
||||
</a>
|
||||
@if (Laravel\Jetstream\Jetstream::hasApiFeatures())
|
||||
<a href="{{ route('api-tokens.index') }}" class="flex items-center gap-2 bg-slate-100 px-4 py-2 text-sm text-slate-700 hover:bg-slate-800/5 hover:text-black focus-visible:bg-slate-800/10 focus-visible:text-black focus-visible:outline-none dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-100/5 dark:hover:text-white dark:focus-visible:bg-slate-100/10 dark:focus-visible:text-white" role="menuitem">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" fill="currentColor" class="w-4 h-4">
|
||||
<path fill-rule="evenodd" d="M11.078 2.25c-.917 0-1.699.663-1.85 1.567L9.05 4.889c-.02.12-.115.26-.297.348a7.493 7.493 0 00-.986.57c-.166.115-.334.126-.45.083L6.3 5.508a1.875 1.875 0 00-2.282.819l-.922 1.597a1.875 1.875 0 00.432 2.385l.84.692c.095.078.17.229.154.43a7.598 7.598 0 000 1.139c.015.2-.059.352-.153.43l-.841.692a1.875 1.875 0 00-.432 2.385l.922 1.597a1.875 1.875 0 002.282.818l1.019-.382c.115-.043.283-.031.45.082.312.214.641.405.985.57.182.088.277.228.297.35l.178 1.071c.151.904.933 1.567 1.85 1.567h1.844c.916 0 1.699-.663 1.85-1.567l.178-1.072c.02-.12.114-.26.297-.349.344-.165.673-.356.985-.57.167-.114.335-.125.45-.082l1.02.382a1.875 1.875 0 002.28-.819l.923-1.597a1.875 1.875 0 00-.432-2.385l-.84-.692c-.095-.078-.17-.229-.154-.43a7.614 7.614 0 000-1.139c-.016-.2.059-.352.153-.43l.84-.692c.708-.582.891-1.59.433-2.385l-.922-1.597a1.875 1.875 0 00-2.282-.818l-1.02.382c-.114.043-.282.031-.449-.083a7.49 7.49 0 00-.985-.57c-.183-.087-.277-.227-.297-.348l-.179-1.072a1.875 1.875 0 00-1.85-1.567h-1.843zM12 15.75a3.75 3.75 0 100-7.5 3.75 3.75 0 000 7.5z" clip-rule="evenodd"/>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23-.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5" />
|
||||
</svg>
|
||||
{{ __('API Tokens') }}
|
||||
</a>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<div>
|
||||
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
|
||||
@if (Laravel\Fortify\Features::canUpdateProfileInformation())
|
||||
@livewire('profile.update-profile-information-form')
|
||||
@livewire('profile.update-profile-form')
|
||||
|
||||
<x-section-border />
|
||||
@endif
|
||||
|
@ -8,7 +8,7 @@
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="form">
|
||||
<!-- Profile Photo -->
|
||||
<!-- Profile Picture -->
|
||||
@if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
|
||||
<div x-data="{photoName: null, photoPreview: null}" class="col-span-6 sm:col-span-4">
|
||||
<!-- Profile Photo File Input -->
|
||||
@ -24,7 +24,7 @@
|
||||
reader.readAsDataURL($refs.photo.files[0]);
|
||||
" />
|
||||
|
||||
<x-label for="photo" value="{{ __('Photo') }}" />
|
||||
<x-label for="photo" value="{{ __('Profile Picture') }}" />
|
||||
|
||||
<!-- Current Profile Photo -->
|
||||
<div class="mt-2" x-show="! photoPreview">
|
||||
@ -52,6 +52,48 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Cover Picture -->
|
||||
<div x-data="{coverName: null, coverPreview: null}" class="col-span-6 sm:col-span-4">
|
||||
<!-- Cover Picture File Input -->
|
||||
<input type="file" id="cover" class="hidden"
|
||||
wire:model.live="cover"
|
||||
x-ref="cover"
|
||||
x-on:change="
|
||||
coverName = $refs.cover.files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
coverPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL($refs.cover.files[0]);
|
||||
" />
|
||||
|
||||
<x-label for="cover" value="{{ __('Cover Picture') }}" />
|
||||
|
||||
<!-- Current Cover Photo -->
|
||||
<div class="mt-2" x-show="! coverPreview">
|
||||
<img src="{{ $this->user->cover_photo_url }}" alt="{{ $this->user->name }}" class="rounded-sm h-20 w-60 object-cover">
|
||||
</div>
|
||||
|
||||
<!-- New Cover Photo Preview -->
|
||||
<div class="mt-2" x-show="coverPreview" style="display: none;">
|
||||
<span class="block h-20 w-60 bg-cover bg-no-repeat bg-center"
|
||||
x-bind:style="'background-image: url(\'' + coverPreview + '\');'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<x-secondary-button class="mt-2 me-2" type="button" x-on:click.prevent="$refs.cover.click()">
|
||||
{{ __('Select A New Cover Photo') }}
|
||||
</x-secondary-button>
|
||||
|
||||
@if ($this->user->cover_photo_path)
|
||||
<x-secondary-button type="button" class="mt-2" wire:click="deleteCoverPhoto">
|
||||
{{ __('Remove Cover Photo') }}
|
||||
</x-secondary-button>
|
||||
@endif
|
||||
|
||||
<x-input-error for="cover" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<!-- Name -->
|
||||
<div class="col-span-6 sm:col-span-4">
|
||||
<x-label for="name" value="{{ __('Name') }}" />
|
||||
@ -88,7 +130,7 @@
|
||||
{{ __('Saved.') }}
|
||||
</x-action-message>
|
||||
|
||||
<x-button wire:loading.attr="disabled" wire:target="photo">
|
||||
<x-button wire:loading.attr="disabled" wire:target="photo,cover">
|
||||
{{ __('Save') }}
|
||||
</x-button>
|
||||
</x-slot>
|
||||
|
35
resources/views/user/show.blade.php
Normal file
35
resources/views/user/show.blade.php
Normal file
@ -0,0 +1,35 @@
|
||||
<x-app-layout>
|
||||
|
||||
<div class="sm:-mt-12 dark:bg-gray-800 dark:text-gray-100">
|
||||
<div>
|
||||
<img class="h-32 w-full object-cover lg:h-48" src="{{ $user->cover_photo_url }}" alt="{{ $user->name }}">
|
||||
</div>
|
||||
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="-mt-12 sm:-mt-16 sm:flex sm:items-end sm:space-x-5">
|
||||
<div class="flex">
|
||||
<img class="h-24 w-24 rounded-full ring-4 ring-white dark:ring-gray-800 sm:h-32 sm:w-32" src="{{ $user->profile_photo_url }}" alt="{{ $user->name }}" />
|
||||
</div>
|
||||
<div class="mt-6 sm:flex sm:min-w-0 sm:flex-1 sm:items-center sm:justify-end sm:space-x-6 sm:pb-1">
|
||||
<div class="mt-6 min-w-0 flex-1 sm:hidden md:block">
|
||||
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||
</div>
|
||||
{{--
|
||||
<div class="mt-6 flex flex-col justify-stretch space-y-3 sm:flex-row sm:space-x-4 sm:space-y-0">
|
||||
<button type="button" class="inline-flex justify-center rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-100 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<svg class="-ml-0.5 mr-1.5 h-5 w-5 text-gray-400 dark:text-gray-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path d="M3 4a2 2 0 00-2 2v1.161l8.441 4.221a1.25 1.25 0 001.118 0L19 7.162V6a2 2 0 00-2-2H3z" />
|
||||
<path d="M19 8.839l-7.77 3.885a2.75 2.75 0 01-2.46 0L1 8.839V14a2 2 0 002 2h14a2 2 0 002-2V8.839z" />
|
||||
</svg>
|
||||
<span>Message</span>
|
||||
</button>
|
||||
</div>
|
||||
--}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 hidden min-w-0 flex-1 sm:block md:hidden">
|
||||
<h1 class="truncate text-2xl font-bold text-gray-900 dark:text-gray-100">{{ $user->name }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-app-layout>
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ModController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth.banned'])->group(function () {
|
||||
@ -11,7 +12,11 @@ Route::middleware(['auth.banned'])->group(function () {
|
||||
|
||||
Route::controller(ModController::class)->group(function () {
|
||||
Route::get('/mods', 'index')->name('mods');
|
||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['id' => '[0-9]+'])->name('mod.show');
|
||||
Route::get('/mod/{mod}/{slug}', 'show')->where(['mod' => '[0-9]+'])->name('mod.show');
|
||||
});
|
||||
|
||||
Route::controller(UserController::class)->group(function () {
|
||||
Route::get('/user/{user}/{username}', 'show')->where(['user' => '[0-9]+'])->name('user.show');
|
||||
});
|
||||
|
||||
Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () {
|
||||
|
Loading…
x
Reference in New Issue
Block a user