Przeglądaj źródła

Merge pull request #820 from pixelfed/frontend-ui-refactor

Frontend ui refactor
daniel 6 lat temu
rodzic
commit
08631ada2b

+ 5 - 0
app/Avatar.php

@@ -15,4 +15,9 @@ class Avatar extends Model
      * @var array
      */
     protected $dates = ['deleted_at'];
+
+    public function profile()
+    {
+    	return $this->belongsTo(Profile::class);
+    }
 }

+ 75 - 0
app/Console/Commands/FixDuplicateProfiles.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+use App\{
+    Like,
+    Media,
+    Profile,
+    Status,
+    User
+};
+
+class FixDuplicateProfiles extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'fix:profile:duplicates';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Fix duplicate profiles';
+
+    /**
+     * Create a new command instance.
+     *
+     * @return void
+     */
+    public function __construct()
+    {
+        parent::__construct();
+    }
+
+    /**
+     * Execute the console command.
+     *
+     * @return mixed
+     */
+    public function handle()
+    {
+        $profiles = Profile::selectRaw('count(user_id) as count,user_id,id')->whereNotNull('user_id')->groupBy('user_id')->orderBy('user_id', 'desc')->get()->where('count', '>', 1);
+        $count = $profiles->count();
+        if($count == 0) {
+            $this->info("No duplicate profiles found!");
+            return;
+        }
+        $this->info("Found {$count} accounts with duplicate profiles...");
+        $bar = $this->output->createProgressBar($count);
+        $bar->start();
+
+        foreach ($profiles as $profile) {
+            $dup = Profile::whereUserId($profile->user_id)->get();
+
+            if(
+                $dup->first()->username === $dup->last()->username && 
+                $dup->last()->statuses()->count() == 0 && 
+                $dup->last()->followers()->count() == 0 && 
+                $dup->last()->likes()->count() == 0 &&
+                $dup->last()->media()->count() == 0
+            ) {
+                $dup->last()->avatar->forceDelete();
+                $dup->last()->forceDelete();
+            }
+            $bar->advance();
+        }
+        $bar->finish();
+    }
+}

+ 39 - 1
app/Http/Controllers/AdminController.php

@@ -26,6 +26,7 @@ use App\Http\Controllers\Admin\{
   AdminSettingsController
 };
 use App\Util\Lexer\PrettyNumber;
+use Illuminate\Validation\Rule;
 
 class AdminController extends Controller
 {
@@ -181,10 +182,47 @@ class AdminController extends Controller
 
     public function profiles(Request $request)
     {
-      $profiles = Profile::orderBy('id','desc')->paginate(10);
+      $this->validate($request, [
+        'search' => 'nullable|string|max:250',
+        'filter' => [
+          'nullable',
+          'string',
+          Rule::in(['id','username','statuses_count','followers_count','likes_count'])
+        ],
+        'order' => [
+          'nullable',
+          'string',
+          Rule::in(['asc','desc'])
+        ],
+        'layout' => [
+          'nullable',
+          'string',
+          Rule::in(['card','list'])
+        ],
+        'limit' => 'nullable|integer|min:1|max:50'
+      ]);
+      $search = $request->input('search');
+      $filter = $request->input('filter');
+      $order = $request->input('order') ?? 'desc';
+      $limit = $request->input('limit') ?? 12;
+      if($search) {
+        $profiles = Profile::where('username','like', "%$search%")->orderBy('id','desc')->paginate($limit);
+      } else if($filter && $order) {
+        $profiles = Profile::withCount(['likes','statuses','followers'])->orderBy($filter, $order)->paginate($limit);
+      } else {
+        $profiles = Profile::orderBy('id','desc')->paginate($limit);
+      }
+
       return view('admin.profiles.home', compact('profiles'));
     }
 
+    public function profileShow(Request $request, $id)
+    {
+      $profile = Profile::findOrFail($id);
+      $user = $profile->user;
+      return view('admin.profiles.edit', compact('profile', 'user'));
+    }
+
     public function appsHome(Request $request)
     {
       $filter = $request->input('filter');

+ 75 - 0
app/Observers/AvatarObserver.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace App\Observers;
+
+use App\Avatar;
+
+class AvatarObserver
+{
+    /**
+     * Handle the avatar "created" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function created(Avatar $avatar)
+    {
+        //
+    }
+
+    /**
+     * Handle the avatar "updated" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function updated(Avatar $avatar)
+    {
+        //
+    }
+
+    /**
+     * Handle the avatar "deleted" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function deleted(Avatar $avatar)
+    {
+        //
+    }
+
+    /**
+     * Handle the avatar "deleting" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function deleting(Avatar $avatar)
+    {
+        $path = storage_path('app/'.$avatar->media_path);
+        @unlink($path);
+    }
+
+    /**
+     * Handle the avatar "restored" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function restored(Avatar $avatar)
+    {
+        //
+    }
+
+    /**
+     * Handle the avatar "force deleted" event.
+     *
+     * @param  \App\Avatar  $avatar
+     * @return void
+     */
+    public function forceDeleted(Avatar $avatar)
+    {
+        //
+    }
+}

+ 0 - 0
app/Observer/UserObserver.php → app/Observers/UserObserver.php


+ 9 - 15
app/Providers/AppServiceProvider.php

@@ -2,10 +2,12 @@
 
 namespace App\Providers;
 
-use App\Observers\UserObserver;
-use App\User;
-use Auth;
-use Horizon;
+use App\Observers\{
+    AvatarObserver,
+    UserObserver
+};
+use App\{Avatar,User};
+use Auth, Horizon, URL;
 use Illuminate\Support\Facades\Blade;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Support\ServiceProvider;
@@ -21,6 +23,7 @@ class AppServiceProvider extends ServiceProvider
     {
         Schema::defaultStringLength(191);
 
+        Avatar::observe(AvatarObserver::class);
         User::observe(UserObserver::class);
 
         Horizon::auth(function ($request) {
@@ -43,17 +46,8 @@ class AppServiceProvider extends ServiceProvider
         });
 
         Blade::directive('prettySize', function ($expression) {
-            $size = intval($expression);
-            $precision = 0;
-            $short = true;
-            $units = $short ?
-                ['B', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'] :
-                ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
-            for ($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {
-            }
-            $res = round($size, $precision).$units[$i];
-
-            return "<?php echo '$res'; ?>";
+            $size = \App\Util\Lexer\PrettyNumber::size($expression);
+            return "<?php echo '$size'; ?>";
         });
 
         Blade::directive('maxFileSize', function () {

+ 2 - 2
resources/lang/en/passwords.php

@@ -15,8 +15,8 @@ return [
 
     'password' => 'Passwords must be at least six characters and match the confirmation.',
     'reset'    => 'Your password has been reset!',
-    'sent'     => 'We have e-mailed your password reset link!',
+    'sent'     => 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. Please check your spam folder if you didn\'t receive this email.',
     'token'    => 'This password reset token is invalid.',
-    'user'     => "We can't find a user with that e-mail address.",
+    'user'     => 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes. Please check your spam folder if you didn\'t receive this email.',
 
 ];

+ 234 - 0
resources/views/admin/profiles/edit.blade.php

@@ -0,0 +1,234 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+  <div class="title d-flex justify-content-between align-items-center">
+    <span><a href="{{route('admin.profiles')}}" class="btn btn-outline-secondary btn-sm font-weight-bold">Back</a></span>
+    <h3 class="font-weight-bold">Edit Profile</h3>
+    <span><a href="#" class="btn btn-outline-primary btn-sm font-weight-bold disabled">Enable Editing</a></span>
+  </div>
+  <hr>
+
+  <div class="row mb-3">
+    <div class="col-12 col-md-4">
+      <div class="card">
+        <div class="card-body text-center">
+          <img src="{{$profile->avatarUrl()}}" class="box-shadow rounded-circle" width="128px" height="128px">
+        </div>
+        {{-- <div class="card-footer bg-white">
+          <p class="font-weight-bold mb-0 small">Last updated: {{$profile->avatar->updated_at->diffForHumans()}}</p>
+        </div> --}}
+      </div>
+    </div>
+    <div class="col-12 col-md-8">
+      <table class="table table-striped table-borderless table-sm">
+        <tbody>
+          @if($user)
+          <tr>
+            <th scope="row">user id</th>
+            <td>{{$user->id}}</td>
+          </tr>
+          @endif
+          <tr>
+            <th scope="row">profile id</th>
+            <td>{{$profile->id}}</td>
+          </tr>
+          <tr>
+            <th scope="row">username</th>
+            <td>
+              {{$profile->username}}
+              @if($user && $user->is_admin == true)
+                <span class="badge badge-danger ml-3">Admin</span>
+              @endif
+            </td>
+          </tr>
+          <tr>
+            <th scope="row">display name</th>
+            <td>{{$profile->name}}</td>
+          </tr>
+          <tr>
+            <th scope="row">joined</th>
+            <td>{{$profile->created_at->format('M j Y')}}</td>
+          </tr>
+          @if($user)
+          <tr>
+            <th scope="row">email</th>
+            <td>
+                {{$user->email}}
+              @if($user->email_verified_at)
+              <span class="text-success font-weight-bold small pl-2">Verified</span>
+              @else
+              <span class="text-danger font-weight-bold small pl-2">Unverified</span>
+              @endif
+            </td>
+          </tr>
+          @endif
+        </tbody>
+      </table>
+      {{-- <div class="py-3">
+        <p class="font-weight-bold mb-0">
+          {{$profile->username}}
+        </p>
+        <p class="h3 font-weight-bold">
+          {{$profile->emailUrl()}}
+        </p>
+        <p class="font-weight-bold mb-0 text-muted">
+          Member Since: {{$profile->created_at->format('M Y')}}
+        </p>
+      </div> --}}
+    </div>
+  </div>
+  <div class="row mb-3">
+  	<div class="col-12 col-md-4 mb-3">
+  		<div class="card">
+  			<div class="card-body text-center">
+  				<p class="h4 mb-0 font-weight-bold">{{$profile->statusCount()}}</p>
+  				<p class="text-muted font-weight-bold small mb-0">Posts</p>
+  			</div>
+  		</div>
+  	</div>
+    <div class="col-12 col-md-4 mb-3">
+      <div class="card">
+        <div class="card-body text-center">
+          <p class="h4 mb-0 font-weight-bold">{{$profile->followingCount()}}</p>
+          <p class="text-muted font-weight-bold small mb-0">Following</p>
+        </div>
+      </div>
+    </div>
+    <div class="col-12 col-md-4 mb-3">
+      <div class="card">
+        <div class="card-body text-center">
+          <p class="h4 mb-0 font-weight-bold">{{$profile->followerCount()}}</p>
+          <p class="text-muted font-weight-bold small mb-0">Followers</p>
+        </div>
+      </div>
+    </div>
+  	<div class="col-12 col-md-3 mb-3">
+  		<div class="card">
+  			<div class="card-body text-center">
+  				<p class="h4 mb-0 font-weight-bold">{{$profile->bookmarks()->count()}}</p>
+  				<p class="text-muted font-weight-bold small mb-0">Bookmarks</p>
+  			</div>
+  		</div>
+  	</div>
+    <div class="col-12 col-md-3 mb-3">
+      <div class="card">
+        <div class="card-body text-center">
+          <p class="h4 mb-0 font-weight-bold">{{$profile->likes()->count()}}</p>
+          <p class="text-muted font-weight-bold small mb-0">Likes</p>
+        </div>
+      </div>
+    </div>
+  	<div class="col-12 col-md-3 mb-3">
+  		<div class="card">
+  			<div class="card-body text-center">
+  				<p class="h4 mb-0 font-weight-bold">{{$profile->reports()->count()}}</p>
+  				<p class="text-muted font-weight-bold small mb-0">Reports Made</p>
+  			</div>
+  		</div>
+  	</div>
+  	<div class="col-12 col-md-3 mb-3">
+  		<div class="card">
+  			<div class="card-body text-center">
+  				<p class="h4 mb-0 font-weight-bold">{{PrettyNumber::size($profile->media()->sum('size'))}}</p>
+  				<p class="text-muted font-weight-bold small mb-0">Storage Used</p>
+  			</div>
+  		</div>
+  	</div>
+  </div>
+
+  <hr>
+  {{-- <div class="mx-3">
+  	  <div class="sub-title h4 font-weight-bold mb-4">
+  	  	Account Settings
+  	  </div>
+	  <form>
+	  	<div class="form-group">
+	  		<label class="font-weight-bold text-muted">Display Name</label>
+	  		<input type="text" class="form-control" value="{{$user->name}}">
+	  	</div>
+	  	<div class="form-group">
+	  		<label class="font-weight-bold text-muted">Username</label>
+	  		<input type="text" class="form-control" value="{{$user->username}}">
+	  	</div>
+	  	<div class="form-group">
+	  		<label class="font-weight-bold text-muted">Email address</label>
+	  		<input type="email" class="form-control" value="{{$user->email}}" placeholder="Enter email">
+	        <p class="help-text small text-muted font-weight-bold">
+	          @if($user->email_verified_at)
+	          <span class="text-success">Verified</span> for {{$user->email_verified_at->diffForHumans()}}
+	          @else
+	          <span class="text-danger">Unverified</span> email.
+	          @endif
+	        </p>
+	  	</div>
+	  </form>
+  </div>
+  <hr> --}}
+  <div class="mx-3">
+      <div class="sub-title h4 font-weight-bold mb-4">
+        Account Actions
+      </div>
+      <div class="row">
+
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-primary py-0 font-weight-bold">Enforce CW</button>
+            <p class="help-text text-muted font-weight-bold small">Adds a CW to every post made by this account.</p>
+          </form>
+        </div>
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-primary py-0 font-weight-bold">Unlisted Posts</button>
+            <p class="help-text text-muted font-weight-bold small">Removes account from public/network timelines.</p>
+          </form>
+        </div>
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-primary py-0 font-weight-bold">No Autolinking</button>
+            <p class="help-text text-muted font-weight-bold small">Do not transform mentions, hashtags or urls into HTML.</p>
+          </form>
+        </div>
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-primary py-0 font-weight-bold">Disable Account</button>
+            <p class="help-text text-muted font-weight-bold small">Temporarily disable account until next time user log in.</p>
+          </form>
+        </div>
+
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-primary py-0 font-weight-bold">Suspend Account</button>
+            <p class="help-text text-muted font-weight-bold small">This prevents any new interactions, without deleting existing data.</p>
+          </form>
+        </div>
+
+        <div class="col-12 col-md-4">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-danger py-0 font-weight-bold">Lock down Account</button>
+            <p class="help-text text-muted font-weight-bold small">This disables the account and changes the password, forcing account to reset password via verified email.</p>
+          </form>
+        </div>
+
+        <div class="col-12">
+          <form method="post" action="/i/admin/users/moderation/update" class="pb-3">
+            @csrf
+            <input type="hidden" name="profile_id" value="{{$profile->id}}">
+            <button class="btn btn-outline-danger font-weight-bold btn-block">Delete Account</button>
+            <p class="help-text text-muted font-weight-bold small">Permanently delete this account.</p>
+          </form>
+        </div>
+      </div>
+  </div>
+@endsection

+ 264 - 31
resources/views/admin/profiles/home.blade.php

@@ -4,10 +4,22 @@
 <div class="title">
 	<h3 class="font-weight-bold d-inline-block">Profiles</h3>
 	<span class="float-right">
-		<a class="btn btn-{{request()->input('layout')!=='list'?'primary':'light'}} btn-sm" href="{{route('admin.profiles')}}">
+		<a class="btn btn-{{request()->input('layout')=='card'?'primary':'light'}} btn-sm" href="{{route('admin.profiles',[
+			'layout'=>'card', 
+			'search' => request()->input('search'),
+			'page' => request()->input('page') ?? 1,
+			'filter' => request()->filter,
+			'order' => request()->order
+			])}}">
 			<i class="fas fa-th"></i>
 		</a>
-		<a class="btn btn-{{request()->input('layout')=='list'?'primary':'light'}} btn-sm mr-3" href="{{route('admin.profiles',['layout'=>'list', 'page' => request()->input('page') ?? 1])}}">
+		<a class="btn btn-{{request()->input('layout')!=='card'?'primary':'light'}} btn-sm mr-3" href="{{route('admin.profiles',[
+			'layout'=>'list', 
+			'search' => request()->input('search'),
+			'page' => request()->input('page') ?? 1,
+			'filter' => request()->filter,
+			'order' => request()->order
+			])}}">
 			<i class="fas fa-list"></i>
 		</a>
 		<div class="dropdown d-inline-block">
@@ -20,63 +32,155 @@
 						<input type="hidden" name="layout" value="{{request()->input('layout')}}"></input>
 						<input type="hidden" name="page" value="{{request()->input('page')}}"></input>
 						<div class="input-group input-group-sm">
-							<input class="form-control" name="search" placeholder="Filter by username, mime type" autocomplete="off"></input>
+							<input class="form-control" name="search" placeholder="Filter by username" autocomplete="off" value="{{request()->input('search')}}">
 							<div class="input-group-append">
 								<button class="btn btn-outline-primary" type="submit">Filter</button>
 							</div>
 						</div>
 					</form>
 				</div>
-				<div class="dropdown-divider"></div>
-				<p class="text-wrap p-1 p-md-3 text-center">
-					<a class="badge badge-primary p-1 btn-filter" href="#" data-filter="cw" data-filter-state="true" data-toggle="tooltip" title="Show Content Warning media">CW</a> 
-					<a class="badge badge-primary p-1 btn-filter" href="#" data-filter="remote" data-filter-state="true" data-toggle="tooltip" title="Show remote media">Remote Media</a> 
-					<a class="badge badge-primary p-1 btn-filter" href="#" data-filter="images" data-filter-state="true" data-toggle="tooltip" title="Show image media">Images</a> 
-					<a class="badge badge-primary p-1 btn-filter" href="#" data-filter="videos" data-filter-state="true" data-toggle="tooltip" title="Show video media">Videos</a> 
-					<a class="badge badge-light p-1 btn-filter" href="#" data-filter="stories" data-filter-state="false" data-toggle="tooltip" title="Show stories media">Stories</a> 
-					<a class="badge badge-light p-1 btn-filter" href="#" data-filter="banned" data-filter-state="false" data-toggle="tooltip" title="Show banned media">Banned</a> 
-					<a class="badge badge-light p-1 btn-filter" href="#" data-filter="reported" data-filter-state="false" data-toggle="tooltip" title="Show reported media">Reported</a> 
-					<a class="badge badge-light p-1 btn-filter" href="#" data-filter="unlisted" data-filter-state="false" data-toggle="tooltip" title="Show unlisted media">Unlisted</a> 
-				</p>
 			</div>
 		</div>
 	</span>
 </div>
 <hr>
-@if(request()->input('layout') == 'list')
+@if(request()->input('layout') !== 'card')
+<div class="mb-3 bulk-actions d-none">
+	<div class="d-flex justify-content-between">
+		<span>
+			<span class="bulk-count font-weight-bold" data-count="0">
+				0
+			</span>
+			<span class="bulk-desc"> items selected</span>
+		</span>
+		<span class="d-inline-flex">
+			<select class="custom-select custom-select-sm font-weight-bold bulk-action">
+				<option selected disabled="">Select Bulk Action</option>
+				<option value="1" disabled="">Review (Coming in v0.9.0)</option>
+				<option value="2">Add C/W</option>
+				<option value="3">Unlist from timelines</option>
+				<option value="4">No Autolinking</option>
+				<option value="5">Suspend</option>
+				<option value="6">Delete</option>
+			</select>
+			<a class="btn btn-outline-primary btn-sm ml-3 font-weight-bold apply-bulk" href="#">
+				Apply
+			</a>
+		</span>
+	</div>
+</div>
 <div class="table-responsive">
 	<table class="table">
 		<thead class="bg-light">
-			<tr class="text-center">
-				<th scope="col" class="border-0" width="10%">
-					<span>ID</span> 
+			<tr>
+				<th class="border-0" width="5%">
+					<div class="custom-control custom-checkbox table-check">
+						<input type="checkbox" class="custom-control-input row-check-item row-check-all" id="row-check-all">
+						<label class="custom-control-label" for="row-check-all"></label>
+					</div>
 				</th>
-				<th scope="col" class="border-0" width="30%">
-					<span>Username</span>
+				<th scope="col" class="border-0" width="10%">
+					<span>
+						ID
+						@if(request()->filter && request()->filter == 'id' && request()->order == 'asc')
+						<a href="#" class="col-ord" data-col="id" data-dir="desc"><i class="fas fa-chevron-down"></i></a>
+						@else
+						<a href="#" class="col-ord" data-col="id" data-dir="asc"><i class="fas fa-chevron-up"></i></a>
+						@endif
+					</span> 
 				</th>
 				<th scope="col" class="border-0" width="15%">
-					<span>Statuses</span>
+					<span>
+						Username
+						@if(request()->filter && request()->filter == 'username' && request()->order == 'asc')
+						<a href="#" class="col-ord" data-col="username" data-dir="desc"><i class="fas fa-chevron-down"></i></a>
+						@else
+						<a href="#" class="col-ord" data-col="username" data-dir="asc"><i class="fas fa-chevron-up"></i></a>
+						@endif
+					</span>
 				</th>
-				<th scope="col" class="border-0" width="15%">
-					<span>Storage</span>
+				<th scope="col" class="border-0" width="20%">
+					<span>
+						Followers
+						@if(request()->filter && request()->filter == 'followers_count' && request()->order == 'asc')
+						<a href="#" class="col-ord" data-col="followers_count" data-dir="desc"><i class="fas fa-chevron-down"></i></a>
+						@else
+						<a href="#" class="col-ord" data-col="followers_count" data-dir="asc"><i class="fas fa-chevron-up"></i></a>
+						@endif
+					</span>
+				</th>
+				<th scope="col" class="border-0" width="20%">
+					<span>
+						Likes
+						@if(request()->filter && request()->filter == 'likes_count' && request()->order == 'asc')
+						<a href="#" class="col-ord" data-col="likes_count" data-dir="desc"><i class="fas fa-chevron-down"></i></a>
+						@else
+						<a href="#" class="col-ord" data-col="likes_count" data-dir="asc"><i class="fas fa-chevron-up"></i></a>
+						@endif
+					</span>
 				</th>
-				<th scope="col" class="border-0" width="30%">
+				<th scope="col" class="border-0" width="20%">
+					<span>
+						Statuses
+						@if(request()->filter && request()->filter == 'statuses_count' && request()->order == 'asc')
+						<a href="#" class="col-ord" data-col="statuses_count" data-dir="desc"><i class="fas fa-chevron-down"></i></a>
+						@else
+						<a href="#" class="col-ord" data-col="statuses_count" data-dir="asc"><i class="fas fa-chevron-up"></i></a>
+						@endif
+					</span>
+				</th>
+				<th scope="col" class="border-0" width="10%">
+					<span>
+						Storage
+					</span>
+				</th>
+				<th scope="col" class="border-0" width="10%">
 					<span>Actions</span>
 				</th>
 			</tr>
 		</thead>
 		@foreach($profiles as $profile)
 		<tr class="font-weight-bold text-center user-row">
-			<th scope="row">
-				{{$profile->id}}
+			<th scope="">
+				<div class="custom-control custom-checkbox">
+					<input type="checkbox" class="custom-control-input row-check-item" id="row-check-{{$profile->id}}" data-id="{{$profile->id}}">
+					<label class="custom-control-label" for="row-check-{{$profile->id}}"></label>
+				</div>
 			</th>
+			<td>
+				{{$profile->id}}
+			</td>
+			<td class="text-truncate" data-toggle="tooltip" data-placement="bottom" title="{{$profile->username}}" style="max-width: 150px;">
+				{{$profile->username}}
+			</td>
+			<td>
+				{{$profile->followers()->count()}}
+			</td>
+			<td>
+				{{$profile->likes()->count()}}
+			</td>
+			<td>
+				{{$profile->statuses()->count()}}
+			</td>
+			<td>
+				<div class="filesize" data-size="{{$profile->media()->sum('size')}}">{{$profile->media()->sum('size')}} bytes</div>
+				
+			</td>
+			<td>
+				<a class="btn btn-outline-secondary btn-sm py-0 mr-3" href="/i/admin/profiles/edit/{{$profile->id}}">Edit</a>
+			</td>
 		</tr>
 		@endforeach
 	</tbody>
 </table>
 </div>
 <div class="d-flex justify-content-center mt-5 small">
-	{{$profiles->links()}}
+	{{$profiles->appends([
+		'layout'=>request()->layout,
+		'search'=>request()->search,
+		'filter'=>request()->filter,
+		'order'=>request()->order
+		])->links()}}
 </div>
 @else
 <div class="row">
@@ -84,7 +188,7 @@
 	<div class="col-12 col-md-4 mb-4">
 		<div class="card">
 			<div class="card-header bg-white text-center" style="min-height: 80px">
-				<img class="box-shadow rounded-circle mb-3" src="{{$profile->avatarUrl()}}" width="64px">
+				<img class="box-shadow rounded-circle mb-3" src="{{$profile->avatarUrl()}}" width="64px" height="64px">
 				<p class="font-weight-bold mb-0 text-truncate">{{$profile->username}}</p>
 			</div>
 			<ul class="list-group list-group-flush small">
@@ -104,7 +208,7 @@
 				</li>
 				<li class="list-group-item text-center">
 					<a class="btn btn-outline-primary btn-sm py-0" href="{{$profile->url()}}">View</a>
-					<a class="btn btn-outline-secondary btn-sm py-0" href="#">Actions</a>
+					<a class="btn btn-outline-secondary btn-sm py-0" href="/i/admin/profiles/edit/{{$profile->id}}">Edit</a>
 				</li>
 			</ul>
 		</div>
@@ -112,7 +216,12 @@
 	@endforeach
 </div>
 <div class="d-flex justify-content-center mt-5 small">
-	{{$profiles->links()}}
+	{{$profiles->appends([
+		'layout'=>request()->layout,
+		'search'=>request()->search,
+		'filter'=>request()->filter,
+		'order'=>request()->order
+		])->links()}}
 </div>
 @endif
 @endsection
@@ -124,7 +233,8 @@
 	display: none;
 }
 
-.user-row:hover {
+.user-row:hover,
+.user-row-active {
 	background-color: #eff8ff;
 }
 .user-row:hover .action-row {
@@ -140,5 +250,128 @@
 	$('.filesize').each(function(k,v) {
 		$(this).text(filesize(v.getAttribute('data-size'), {unix:true, round:0}))
 	});
+	$('.col-ord').on('click', function(e) {
+		e.preventDefault();
+		let el = $(this);
+		let ord = el.data('dir');
+		let col = el.data('col');
+		let wurl = new URL(window.location.href);
+		let query_string = wurl.search;
+		let search_params = new URLSearchParams(query_string); 
+
+		if(ord == 'asc') {
+			search_params.set('filter', col);
+			search_params.set('order', ord);
+			wurl.search = search_params.toString();
+			el.find('i').removeClass('fa-chevron-up').addClass('fa-chevron-down');
+			el.data('dir', 'desc');
+		} else {
+			search_params.set('filter', col);
+			search_params.set('order', ord);
+			wurl.search = search_params.toString();
+			el.find('i').removeClass('fa-chevron-down').addClass('fa-chevron-up');
+			el.data('dir', 'asc');
+		}
+		window.location.href = wurl.toString();
+	});
+
+
+	$(document).on('click', '#row-check-all', function(e) {
+		return;
+		let el = $(this);
+		let attr = el.attr('checked');
+
+		if (typeof attr !== typeof undefined && attr !== false) {
+			$('tbody .user-row').removeClass('user-row-active');
+			$('.bulk-actions').addClass('d-none');
+			$('.row-check-item').removeAttr('checked').prop('checked', false);
+			el.removeAttr('checked').prop('checked', false);
+		} else {
+			$('tbody .user-row').addClass('user-row-active');
+			$('.bulk-actions').removeClass('d-none');
+			el.attr('checked', '').prop('checked', true);
+			$('.row-check-item').attr('checked', '').prop('checked', true);
+		}
+
+		let len = $('.row-check-item:checked').length;
+		if(attr == true) {
+			len--;
+		}
+		$('.bulk-count').text(len).attr('data-count', len);
+	});
+
+
+	$(document).on('click', '.row-check-item', function(e) {
+		return;
+		var el = $(this)[0];
+		let len = $('.row-check-item:checked').length;
+		if($('#row-check-all:checked').length > 0) {
+			len--;
+		}
+		if($(this).hasClass('row-check-all')) {
+			return;
+		};
+		if(el.checked == true) {
+			$(this).parents().eq(2).addClass('user-row-active');
+			$('.bulk-actions').removeClass('d-none');
+			$('.bulk-count').text(len).attr('data-count', len);
+		} else {
+			$(this).parents().eq(2).removeClass('user-row-active');
+			if(len == 0) {
+				$('.bulk-actions').addClass('d-none');
+			} else {
+				$('.bulk-count').text(len).attr('data-count', len);   
+			}
+		}
+		if(len == 0) {
+			$('.bulk-actions').addClass('d-none');
+			$('#row-check-all').prop('checked', false);
+		} else {
+			$('.bulk-actions').removeClass('d-none');
+		}
+	});
+
+	$(document).on('click', '.apply-bulk', function(e) {
+		return;
+		e.preventDefault();
+		let len = $('.row-check-item:checked').length;
+		if($('#row-check-all:checked').length > 0) {
+			len--;
+		}
+		if(len == 0) {
+			return;
+		}
+		let action = $('.bulk-action').val();
+		let ids = $('.row-check-item:checked').get().filter(i => {
+			let el = $(i);
+			if(el.hasClass('row-check-all')) {
+				return false;
+			}
+			return true;
+		}).map(i => {
+			return $(i).data('id');
+		});
+		let actions = [
+			'',
+			'review',
+			'cw',
+			'unlist',
+			'noautolink',
+			'suspend',
+			'delete'
+		];
+		action = actions[action];
+		if(!action) {
+			return;
+		}
+		swal(
+			'Confirm', 
+			'Are you sure you want to perform this action?',
+			'warning'
+		).then(res => {
+			console.log(action);
+			console.log(ids);
+		})
+	});
 </script>
 @endpush

+ 2 - 4
resources/views/admin/reports/home.blade.php

@@ -66,12 +66,10 @@
     <thead class="bg-light">
       <tr>
         <th scope="col">
-          <div class="">
             <div class="custom-control custom-checkbox table-check">
               <input type="checkbox" class="custom-control-input row-check-item" id="row-check-all">
               <label class="custom-control-label" for="row-check-all"></label>
             </div>
-          </div>
         </th>
         <th scope="col">#</th>
         <th scope="col">Reporter</th>
@@ -84,14 +82,14 @@
     <tbody>
       @foreach($reports as $report)
       <tr>
-        <td class="">
+        <td scope="row">
           <div class="custom-control custom-checkbox">
             <input type="checkbox" class="custom-control-input row-check-item" id="row-check-{{$report->id}}" data-resolved="{{$report->admin_seen?'true':'false'}}" data-id="{{$report->id}}">
             <label class="custom-control-label" for="row-check-{{$report->id}}"></label>
           </div>
         </td>
         <td>
-          <a href="{{$report->url()}}" class="btn btn-sm btn-outline-primary">
+          <a href="{{$report->url()}}" class="btn btn-sm btn-outline-primary my-0 py-0">
             {{$report->id}}
           </a>
           

+ 3 - 9
resources/views/auth/passwords/email.blade.php

@@ -8,9 +8,9 @@
                 <div class="card-header bg-white p-3 text-center font-weight-bold">{{ __('Reset Password') }}</div>
 
                 <div class="card-body">
-                    @if (session('status'))
+                    @if (session('status') || $errors->has('email'))
                         <div class="alert alert-success">
-                            {{ session('status') }}
+                            {{ session('status') ?? $errors->first('email') }}
                         </div>
                     @endif
 
@@ -19,13 +19,7 @@
 
                         <div class="form-group row">
                             <div class="col-md-12">
-                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" placeholder="{{ __('E-Mail Address') }}" value="{{ old('email') }}" required>
-
-                                @if ($errors->has('email'))
-                                    <span class="invalid-feedback">
-                                        <strong>{{ $errors->first('email') }}</strong>
-                                    </span>
-                                @endif
+                                <input id="email" type="email" class="form-control" name="email" placeholder="{{ __('E-Mail Address') }}" value="{{ old('email') }}" required>
                             </div>
                         </div>
 

+ 1 - 0
routes/web.php

@@ -13,6 +13,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
     Route::get('statuses/show/{id}', 'AdminController@showStatus');
     Route::redirect('profiles', '/i/admin/profiles/list');
     Route::get('profiles/list', 'AdminController@profiles')->name('admin.profiles');
+    Route::get('profiles/edit/{id}', 'AdminController@profileShow');
     Route::redirect('users', '/users/list');
     Route::get('users/list', 'AdminController@users')->name('admin.users');
     Route::get('users/edit/{id}', 'AdminController@editUser');