From ad71c712b90bfcda69f05b074d59fee2dfb8eaea Mon Sep 17 00:00:00 2001 From: Graphictoria Date: Sun, 8 Jan 2023 21:26:52 -0500 Subject: [PATCH] Shop, thumbnail, transactions, and admin changes. - Fix shop view all button. - Fix internal server error when accessing an asset page while logged out. - User render busy icon. - Virtubrick server-side rendering paginator. - Pagination in user search. - Details of punishments on user admin page. - Updated some APIs to not return virtubrick.local thumbnail urls. - Compacted admin user search and turned it into a GET request. - Removed testing thumbnail from user circle component. - Removed testing thumbnail from user admin page. - Updated dashboard and profile to use new function for user thumbnails. - Grammar edit on user admin page for "user is a (power type)" label. - Made MarkdownHelper play nicely with site alerts. - Created a transactions page and APIs to go along with it. - Website can now render user thumbnails. - Reworked avatar/closeup render scripts. - Added a "userToJson" function to the user model for use in APIs that return user data. --- web/app/Helpers/MarkdownHelper.php | 16 +- .../Controllers/Api/CommentsController.php | 2 +- .../Http/Controllers/Api/FeedController.php | 16 +- .../Http/Controllers/Api/MoneyController.php | 148 ++++++++ .../Controllers/Api/ThumbnailController.php | 49 ++- .../Http/Controllers/Web/AdminController.php | 77 ++-- .../Http/Controllers/Web/AvatarController.php | 15 + .../Http/Controllers/Web/MoneyController.php | 14 + web/app/Jobs/ArbiterRender.php | 15 +- web/app/Models/RenderTracker.php | 2 +- web/app/Models/Transaction.php | 26 ++ web/app/Models/TransactionType.php | 5 + web/app/Models/User.php | 54 +++ web/app/Models/asset.php | 2 +- .../2014_10_12_000000_create_users_table.php | 1 + web/public/images/busy/user.png | Bin 0 -> 12032 bytes web/resources/js/components/Shop.js | 1 + web/resources/js/components/Transactions.js | 328 ++++++++++++++++++ web/resources/js/pages/Transactions.js | 18 + .../admin/user-search-input.blade.php | 7 +- .../views/components/user-circle.blade.php | 3 +- web/resources/views/layouts/nav.blade.php | 16 +- .../views/pagination/virtubrick.blade.php | 13 + .../views/web/admin/useradmin.blade.php | 33 +- .../views/web/admin/usersearch.blade.php | 1 + .../views/web/avatar/editor.blade.php | 48 +++ .../views/web/home/dashboard.blade.php | 2 +- .../views/web/money/transactions.blade.php | 26 ++ web/resources/views/web/shop/asset.blade.php | 2 +- .../views/web/user/profile.blade.php | 2 +- web/routes/api.php | 3 + web/routes/web.php | 17 +- web/storage/app/grid/scripts/Avatar.lua | 2 +- web/storage/app/grid/scripts/Closeup.lua | 2 +- web/webpack.mix.js | 1 + 35 files changed, 884 insertions(+), 83 deletions(-) create mode 100644 web/app/Http/Controllers/Api/MoneyController.php create mode 100644 web/app/Http/Controllers/Web/AvatarController.php create mode 100644 web/app/Http/Controllers/Web/MoneyController.php create mode 100644 web/public/images/busy/user.png create mode 100644 web/resources/js/components/Transactions.js create mode 100644 web/resources/js/pages/Transactions.js create mode 100644 web/resources/views/pagination/virtubrick.blade.php create mode 100644 web/resources/views/web/avatar/editor.blade.php create mode 100644 web/resources/views/web/money/transactions.blade.php diff --git a/web/app/Helpers/MarkdownHelper.php b/web/app/Helpers/MarkdownHelper.php index 33343a1..0a2dd3c 100644 --- a/web/app/Helpers/MarkdownHelper.php +++ b/web/app/Helpers/MarkdownHelper.php @@ -10,19 +10,31 @@ namespace App\Helpers; use Illuminate\Support\HtmlString; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\Extension\CommonMark\Node\Inline\Link; +use League\CommonMark\Extension\CommonMark\Node\Inline\Image; +use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension; use League\CommonMark\Extension\Table\TableExtension; use League\CommonMark\MarkdownConverter; class MarkdownHelper { - // XlXi: This bit was taken from https://github.com/laravel/framework/blob/b9203fca96960ef9cd8860cb4ec99d1279353a8d/src/Illuminate/Mail/Markdown.php line 106 + // TODO: XlXi: Add a non-nav alert mode for links. + // XlXi: This bit was partially taken from https://github.com/laravel/framework/blob/b9203fca96960ef9cd8860cb4ec99d1279353a8d/src/Illuminate/Mail/Markdown.php line 106 public static function parse($text) { $environment = new Environment([ - // + 'default_attributes' => [ + Link::class => [ + 'class' => ['text-decoration-none'] + ], + Image::class => [ + 'classes' => ['img-fluid'] + ] + ] ]); $environment->addExtension(new CommonMarkCoreExtension); $environment->addExtension(new TableExtension); + $environment->addExtension(new DefaultAttributesExtension); $converter = new MarkdownConverter($environment); diff --git a/web/app/Http/Controllers/Api/CommentsController.php b/web/app/Http/Controllers/Api/CommentsController.php index eec55f6..70b9732 100644 --- a/web/app/Http/Controllers/Api/CommentsController.php +++ b/web/app/Http/Controllers/Api/CommentsController.php @@ -48,7 +48,7 @@ class CommentsController extends Controller foreach($comments as $comment) { $poster = [ 'name' => $comment->user->username, - 'thumbnail' => 'https://www.virtubrick.local/images/testing/headshot.png', + 'thumbnail' => $comment->user->getHeadshotImageUrl(), 'url' => $comment->user->getProfileUrl() ]; diff --git a/web/app/Http/Controllers/Api/FeedController.php b/web/app/Http/Controllers/Api/FeedController.php index 4e72fca..f51ae5b 100644 --- a/web/app/Http/Controllers/Api/FeedController.php +++ b/web/app/Http/Controllers/Api/FeedController.php @@ -2,13 +2,14 @@ namespace App\Http\Controllers\Api; +use Carbon\Carbon; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; + use App\Http\Controllers\Controller; use App\Models\Friend; use App\Models\Shout; use App\Models\User; -use Carbon\Carbon; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; class FeedController extends Controller { @@ -31,19 +32,12 @@ class FeedController extends Controller ]; foreach($postsQuery as $post) { - // TODO: XlXi: icons/colors // TODO: XlXi: groups $poster = []; if($post['poster_type'] == 'user') { $user = User::where('id', $post['poster_id'])->first(); - - $poster = [ - 'type' => 'User', - 'name' => $user->username, - 'thumbnail' => 'https://www.virtubrick.local/images/testing/headshot.png', - 'url' => $user->getProfileUrl() - ]; + $poster = $user->userToJson(); } /* */ diff --git a/web/app/Http/Controllers/Api/MoneyController.php b/web/app/Http/Controllers/Api/MoneyController.php new file mode 100644 index 0000000..f06e439 --- /dev/null +++ b/web/app/Http/Controllers/Api/MoneyController.php @@ -0,0 +1,148 @@ + [], + 'total' => 0 + ]; + + $dataPoints = [ + [ + 'Name' => 'Item Purchases', + 'Points' => ['Purchases'] + ], + [ + 'Name' => 'Sale of Goods', + 'Points' => ['Sales', 'Commissions'] + ], + [ + 'Name' => 'Group Payouts', + 'Points' => ['Group Payouts'] + ] + ]; + + foreach($dataPoints as $dataPoint) + { + $newColumn = ['name' => $dataPoint['Name'], 'total' => 0]; + + foreach($dataPoint['Points'] as $transactionType) + { + $newColumn['total'] += Transaction::where('user_id', Auth::user()->id) + ->where('transaction_type_id', TransactionType::IDFromType($transactionType)) + ->where(function($query) use($request) { + if(!$request->has('filter')) + return $query; + + $now = Carbon::now(); + switch($request->get('filter')) + { + case 'pastday': + return $query->where('created_at', '>', $now->subDay()); + case 'pastweek': + return $query->where('created_at', '>', $now->subWeek()); + case 'pastmonth': + return $query->where('created_at', '>', $now->subMonth()); + case 'pastyear': + return $query->where('created_at', '>', $now->subYear()); + default: + return $query; + } + }) + ->sum('delta'); + } + + array_push($result['columns'], $newColumn); + $result['total'] += $newColumn['total']; + } + + return response($result); + } + + public function userTransactions(Request $request) + { + $validator = Validator::make($request->all(), [ + 'filter' => ['required', 'in:purchases,sales,commissions,grouppayouts'] + ]); + + if($validator->fails()) + return ValidationHelper::generateValidatorError($validator); + + $valid = $validator->valid(); + + $resultData = []; + $transactionType = 0; + switch($valid['filter']) + { + case 'purchases': + $transactionType = TransactionType::where('name', 'Purchases')->first(); + break; + case 'sales': + $transactionType = TransactionType::where('name', 'Sales')->first(); + break; + case 'commissions': + $transactionType = TransactionType::where('name', 'Commissions')->first(); + break; + case 'grouppayouts': + $transactionType = TransactionType::where('name', 'Group Payouts')->first(); + break; + } + + $transactions = Transaction::where(function($query) use($valid) { + if($valid['filter'] == 'sales') + return $query->where('seller_id', Auth::user()->id); + return $query->where('user_id', Auth::user()->id); + }) + ->where('transaction_type_id', $transactionType->id) + ->orderByDesc('id') + ->cursorPaginate(30); + $prevCursor = $transactions->previousCursor(); + $nextCursor = $transactions->nextCursor(); + + foreach($transactions as $transaction) + { + $user = null; + if($valid['filter'] != 'sales') + $user = $transaction->seller; + else + $user = $transaction->user; + + $asset = null; + if($transactionType->format != '') + $asset = [ + 'url' => route('shop.asset', ['asset' => $transaction->asset, 'assetName' => Str::slug($transaction->asset->name, '-')]), + 'name' => $transaction->asset->name + ]; + + array_push($resultData, [ + 'date' => $transaction->created_at->isoFormat('lll'), + 'member' => $user->userToJson(), + 'description' => $transactionType->format, + 'amount' => $transaction->delta, + 'item' => $asset + ]); + } + + return response([ + 'data' => $resultData, + 'prev_cursor' => ($prevCursor ? $prevCursor->encode() : null), + 'next_cursor' => ($nextCursor ? $nextCursor->encode() : null) + ]); + } +} diff --git a/web/app/Http/Controllers/Api/ThumbnailController.php b/web/app/Http/Controllers/Api/ThumbnailController.php index a251606..1bcd402 100644 --- a/web/app/Http/Controllers/Api/ThumbnailController.php +++ b/web/app/Http/Controllers/Api/ThumbnailController.php @@ -41,7 +41,7 @@ class ThumbnailController extends Controller ]; } - private function handleRender(Request $request, string $renderType) + private function handleRender(Request $request, string $renderType, bool $assetId = null) { $validator = Validator::make($request->all(), $this->{strtolower($renderType) . 'ValidationRules'}()); @@ -64,6 +64,24 @@ class ThumbnailController extends Controller $valid['position'] = 'Full'; $valid['position'] = strtolower($valid['position']); + + if($valid['position'] != 'full' && $valid['type'] == '3d') + { + $validator->errors()->add('type', 'Cannot render non-full avatar as 3D.'); + return ValidationHelper::generateValidatorError($validator); + } + + switch($valid['position']) + { + case 'full': + if($model->thumbnail2DHash && $valid['type'] == '2d') + return response(['status' => 'success', 'data' => route('content', $model->thumbnail2DHash)]); + break; + case 'bust': + if($model->thumbnailBustHash && $valid['type'] == '2d') + return response(['status' => 'success', 'data' => route('content', $model->thumbnailBustHash)]); + break; + } } elseif($renderType == 'Asset') { if($model->moderated) return response(['status' => 'success', 'data' => '/thumbs/DeletedThumbnail.png']); @@ -78,16 +96,17 @@ class ThumbnailController extends Controller $validator->errors()->add('id', 'This asset cannot be rendered.'); return ValidationHelper::generateValidatorError($validator); } + + if($model->thumbnail2DHash && $valid['type'] == '2d') + return response(['status' => 'success', 'data' => route('content', $model->thumbnail2DHash)]); } - - if($model->thumbnail2DHash && $valid['type'] == '2d') - return response(['status' => 'success', 'data' => route('content', $model->thumbnail2DHash)]); - if($model->thumbnail3DHash && $valid['type'] == '3d') return response(['status' => 'success', 'data' => route('content', $model->thumbnail3DHash)]); $trackerType = sprintf('%s%s', strtolower($renderType), $valid['type']); + if($renderType == 'User' && $valid['position'] == 'bust') + $trackerType .= 'bust'; $tracker = RenderTracker::where('type', $trackerType) ->where('target', $valid['id']) ->where('created_at', '>', Carbon::now()->subMinute()); @@ -101,7 +120,7 @@ class ThumbnailController extends Controller ArbiterRender::dispatch( $tracker, $valid['type'] == '3d', - ($renderType == 'User' ? $valid['position'] : $model->typeString()), + ($renderType == 'User' ? $valid['position'] == 'full' ? 'Avatar' : 'Bust' : $model->typeString()), $model->id ); } @@ -119,8 +138,22 @@ class ThumbnailController extends Controller return $this->handleRender($request, 'User'); } - public function tryAsset() + public function tryAsset(Request $request) { - // + $validator = Validator::make($request->all(), [ + 'id' => [ + 'required', + Rule::exists('App\Models\Asset', 'id')->where(function($query) { + return $query->where('moderated', false); + }) + ] + ]); + + if($validator->fails()) + return ValidationHelper::generateValidatorError($validator); + + $valid = $validator->valid(); + + return $this->handleRender($request, 'User', $valid['id']); } } diff --git a/web/app/Http/Controllers/Web/AdminController.php b/web/app/Http/Controllers/Web/AdminController.php index 588518c..c568e2a 100644 --- a/web/app/Http/Controllers/Web/AdminController.php +++ b/web/app/Http/Controllers/Web/AdminController.php @@ -41,44 +41,63 @@ class AdminController extends Controller } // GET admin.usersearch - function userSearch() + function userSearch(Request $request) { + $types = [ + 'userid' => 'UserId', + 'username' => 'UserName', + 'ipaddress' => 'IpAddress' + ]; + + foreach($types as $type => &$func) + { + if($type == $request->has($type)) + return $this->{'userSearchQuery' . $func}($request); + } + return view('web.admin.usersearch'); } // POST admin.usersearchquery - function userSearchQuery(Request $request) + function userSearchQueryUserId(Request $request) { - if($request->has('userid-button')) - { - $request->validate([ - 'userid-search' => ['required', 'int'] - ]); - return view('web.admin.usersearch')->with('users', User::where('id', $request->get('userid-search'))->get()); - } + $request->validate([ + 'userid' => ['required', 'int'] + ]); - if($request->has('username-button')) - { - $request->validate([ - 'username-search' => ['required', 'string'] - ]); - return view('web.admin.usersearch')->with('users', User::where('username', 'like', '%' . $request->get('username-search') . '%')->get()); - } + $users = User::where('id', $request->get('userid')) + ->paginate(25) + ->appends($request->all()); - if($request->has('ipaddress-button')) - { - $request->validate([ - 'ipaddress-search' => ['required', 'ip'] - ]); - - $result = UserIp::where('ipAddress', $request->get('ipaddress-search')) - ->join('users', 'users.id', '=', 'user_ips.userId') - ->orderBy('users.id', 'desc'); - - return view('web.admin.usersearch')->with('users', $result->get())->with('isIpSearch', true); - } + return view('web.admin.usersearch')->with('users', $users); + } + + function userSearchQueryUserName(Request $request) + { + $request->validate([ + 'username' => ['required', 'string'] + ]); - return view('web.admin.usersearch')->with('error', 'Input validation failed.'); + $users = User::where('username', 'like', '%' . $request->get('username') . '%') + ->paginate(25) + ->appends($request->all()); + + return view('web.admin.usersearch')->with('users', $users); + } + + function userSearchQueryIpAddress(Request $request) + { + $request->validate([ + 'ipaddress' => ['required', 'ip'] + ]); + + $users = UserIp::where('ipAddress', $request->get('ipaddress')) + ->join('users', 'users.id', '=', 'user_ips.userId') + ->orderBy('users.id', 'desc') + ->paginate(25) + ->appends($request->all()); + + return view('web.admin.usersearch')->with('users', $users)->with('isIpSearch', true); } // GET admin.userlookup diff --git a/web/app/Http/Controllers/Web/AvatarController.php b/web/app/Http/Controllers/Web/AvatarController.php new file mode 100644 index 0000000..1449e17 --- /dev/null +++ b/web/app/Http/Controllers/Web/AvatarController.php @@ -0,0 +1,15 @@ +type) { + case 'Bust': + $this->type = 'Closeup'; + array_push($arguments, false); // Quadratic + array_push($arguments, 30); // Base Hat Zoom + array_push($arguments, 100); // Max Hat Zoom + array_push($arguments, 0); // Camera Offset X + array_push($arguments, 0); // Camera Offset Y + case 'Avatar': + $arguments[0] = url('test', ['id' => $this->assetId]); // TODO: this + break; case 'Head': case 'Shirt': case 'Pants': @@ -185,7 +195,10 @@ class ArbiterRender implements ShouldQueue ->resize($arguments[2]/4, $arguments[3]/4) ->toString(); - $this->tracker->targetObj->set2DHash(CdnHelper::SaveContentB64(base64_encode($image), 'image/png')); + if($this->type == 'Closeup') + $this->tracker->targetObj->setBustHash(CdnHelper::SaveContentB64(base64_encode($image), 'image/png')); + else + $this->tracker->targetObj->set2DHash(CdnHelper::SaveContentB64(base64_encode($image), 'image/png')); } $this->tracker->delete(); diff --git a/web/app/Models/RenderTracker.php b/web/app/Models/RenderTracker.php index 5170629..a6fe9b9 100644 --- a/web/app/Models/RenderTracker.php +++ b/web/app/Models/RenderTracker.php @@ -21,7 +21,7 @@ class RenderTracker extends Model public function targetObj() { - if($this->type == 'user2d' || $this->type == 'user3d') + if($this->type == 'user2d' || $this->type == 'user2dbust' || $this->type == 'user3d') return $this->belongsTo(User::class, 'target'); elseif($this->type == 'asset2d' || $this->type == 'asset3d') return $this->belongsTo(Asset::class, 'target'); diff --git a/web/app/Models/Transaction.php b/web/app/Models/Transaction.php index 01bc7b8..13b7b05 100644 --- a/web/app/Models/Transaction.php +++ b/web/app/Models/Transaction.php @@ -9,6 +9,16 @@ class Transaction extends Model { use HasFactory; + /** + * The attributes that should be cast. + * + * @var array + */ + protected $casts = [ + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + /** * The attributes that are mass assignable. * @@ -23,9 +33,25 @@ class Transaction extends Model 'seller_id' ]; + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + public function seller() + { + return $this->belongsTo(User::class, 'seller_id'); + } + + public function asset() + { + return $this->belongsTo(Asset::class, 'asset_id'); + } + protected static function createPurchase($transaction) { return self::create(array_merge($transaction, [ + 'delta' => -$transaction->delta, 'transaction_type_id' => TransactionType::where('name', 'Purchases')->first()->id ])); } diff --git a/web/app/Models/TransactionType.php b/web/app/Models/TransactionType.php index 6edd82d..1faae90 100644 --- a/web/app/Models/TransactionType.php +++ b/web/app/Models/TransactionType.php @@ -8,4 +8,9 @@ use Illuminate\Database\Eloquent\Model; class TransactionType extends Model { use HasFactory; + + public static function IDFromType($type) + { + return self::where('name', $type)->first()->id; + } } diff --git a/web/app/Models/User.php b/web/app/Models/User.php index 9d8004b..f152925 100644 --- a/web/app/Models/User.php +++ b/web/app/Models/User.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Http; use Laravel\Sanctum\HasApiTokens; use App\Notifications\ResetPasswordNotification; @@ -166,6 +167,59 @@ class User extends Authenticatable implements MustVerifyEmail $this->save(); } + public function getImageUrl() + { + $renderId = $this->id; + + $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $renderId, 'position' => 'full', 'type' => '2d'])); + if($thumbnail->json('status') == 'loading') + return '/images/busy/user.png'; + + return $thumbnail->json('data'); + } + + public function getHeadshotImageUrl() + { + $renderId = $this->id; + + $thumbnail = Http::get(route('thumbnails.v1.user', ['id' => $renderId, 'position' => 'bust', 'type' => '2d'])); + if($thumbnail->json('status') == 'loading') + return '/images/busy/user.png'; + + return $thumbnail->json('data'); + } + + public function set2DHash($hash) + { + $this->thumbnail2DHash = $hash; + $this->timestamps = false; + $this->save(); + } + + public function setBustHash($hash) + { + $this->thumbnailBustHash = $hash; + $this->timestamps = false; + $this->save(); + } + + public function set3DHash($hash) + { + $this->thumbnail3DHash = $hash; + $this->timestamps = false; + $this->save(); + } + + public function userToJson() + { + return [ + 'type' => 'User', + 'name' => $this->username, + 'thumbnail' => $this->getHeadshotImageUrl(), + 'url' => $this->getProfileUrl() + ]; + } + public function _hasRolesetInternal($roleName) { $roleset = Roleset::where('Name', $roleName)->first(); diff --git a/web/app/Models/asset.php b/web/app/Models/asset.php index b04cd62..7d6c899 100644 --- a/web/app/Models/asset.php +++ b/web/app/Models/asset.php @@ -143,7 +143,7 @@ class Asset extends Model $thumbnail = Http::get(route('thumbnails.v1.asset', ['id' => $renderId, 'type' => '2d'])); if($thumbnail->json('status') == 'loading') - return ($this->assetTypeId == 9 ? 'https://virtubrick.local/images/busy/game.png' : 'https://virtubrick.local/images/busy/asset.png'); + return ($this->assetTypeId == 9 ? '/images/busy/game.png' : '/images/busy/asset.png'); return $thumbnail->json('data'); } diff --git a/web/database/migrations/2014_10_12_000000_create_users_table.php b/web/database/migrations/2014_10_12_000000_create_users_table.php index 65d9b31..eefc4a9 100644 --- a/web/database/migrations/2014_10_12_000000_create_users_table.php +++ b/web/database/migrations/2014_10_12_000000_create_users_table.php @@ -26,6 +26,7 @@ return new class extends Migration $table->unsignedBigInteger('tokens')->default(0); $table->dateTime('next_reward')->useCurrent(); + $table->string('thumbnailBustHash')->nullable(); $table->string('thumbnail2DHash')->nullable(); $table->string('thumbnail3DHash')->nullable(); diff --git a/web/public/images/busy/user.png b/web/public/images/busy/user.png new file mode 100644 index 0000000000000000000000000000000000000000..76d29e2207db185e75f0b92e2229f1d5c65115db GIT binary patch literal 12032 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 { + return obj.tag === tag + }); + this.categoryDropdown.current.innerText = selectedTag.label; + + this.state.filters.map(({ label, tag, ref }) => { + if(ref.current.innerText == selectedTag.label) + ref.current.classList.add('active'); + else + ref.current.classList.remove('active'); + }); + + axios.get(buildGenericApiUrl('api', `shop/v1/user-summary?filter=${tag}`)) + .then(res => { + const response = res.data; + this.setState({ items: response.columns, total: response.total, loaded: true }); + }); + } + + render() { + return ( +
+

Time Period:

+ +
    + { + this.state.filters.map(({ label, tag, ref }) => +
  • + ) + } +
+ { + this.state.loaded + ? + <> +

Tokens

+ + + + + + + + + { + this.state.items.map(({ name, total }, index) => + + + + + ) + } + +
CategoriesCredit
{ name }{ total != 0 ? { total } : null }
+

Total { this.state.total }

+ + : +
+ +
+ } +
+ ) + } +} + +class TransactionsTab extends Component { + constructor(props) { + super(props); + this.state = { + loaded: false, + loadingMore: false, + filters: [ + {label: 'Purchases', tag: 'purchases', ref: createRef()}, + {label: 'Sales', tag: 'sales', ref: createRef()}, + {label: 'Commissions', tag: 'commissions', ref: createRef()}, + {label: 'Group Payouts', tag: 'grouppayouts', ref: createRef()} + ], + items: [] + }; + + this.categoryDropdown = createRef(); + + this.setTag = this.setTag.bind(this); + this.loadMore = this.loadMore.bind(this); + } + + componentWillMount() { + window.addEventListener('scroll', this.loadMore); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.loadMore); + } + + componentDidMount() { + this.setTag('purchases'); + } + + setTag(tag) { + this.setState({ loaded: false }); + + let selectedTag = this.state.filters.find(obj => { + return obj.tag === tag + }); + this.categoryDropdown.current.innerText = selectedTag.label; + + this.state.filters.map(({ label, tag, ref }) => { + if(ref.current.innerText == selectedTag.label) + ref.current.classList.add('active'); + else + ref.current.classList.remove('active'); + }); + + this.nextCursor = null; + + axios.get(buildGenericApiUrl('api', `shop/v1/user-transactions?filter=${tag}`)) + .then(res => { + const response = res.data; + + this.nextCursor = response.next_cursor; + this.setState({ items: response.data, filter: tag, loaded: true }); + }); + } + + loadMore() { + // XlXi: Taking the height of the footer into account. + if (window.innerHeight + document.documentElement.scrollTop >= document.scrollingElement.scrollHeight-200) { + if (!!(this.nextCursor) && !this.state.loading && !this.state.loadingMore) { + this.setState({ loadingMore: true }); + + axios.get(buildGenericApiUrl('api', `shop/v1/user-transactions?filter=${this.state.filter}&cursor=${this.nextCursor}`)) + .then(res => { + const response = res.data; + + this.nextCursor = response.next_cursor; + this.setState({ items: this.state.items.concat(response.data), loadingMore: false }); + }); + } + } + } + + render() { + return ( +
+

Currently Selected:

+ +
    + { + this.state.filters.map(({ label, tag, ref }) => +
  • + ) + } +
+ { + this.state.loaded + ? + <> +

Tokens

+ + + + + + + + + + + { + this.state.items.map(({ date, member, description, amount, item }, index) => + + + + + + + ) + } + +
DateMemberDescriptionAmount
{ date }{ member.name }{ description }{ item != null ? { item.name } : null }

{ amount }

+ { + this.state.loadingMore + ? +
+ +
+ : + null + } + + : +
+ +
+ } +
+ ) + } +} + +class Transactions extends Component { + constructor(props) { + super(props); + this.state = { + loaded: false, + tabs: [ + {label: 'Summary', name: 'summary', ref: createRef()}, + {label: 'My Transactions', name: 'transactions', ref: createRef()} + ] + }; + + this.tabIndex = 0; + + this.setCurrentTab = this.setCurrentTab.bind(this); + this.setTab = this.setTab.bind(this); + } + + componentDidMount() { + const params = new Proxy(new URLSearchParams(window.location.search), { + get: (searchParams, prop) => searchParams.get(prop), + }); + let queryTab = this.state.tabs.find(obj => { + if(!params.tab) + return false; + + return obj.name === params.tab.toLowerCase(); + }); + + if(queryTab) + this.setTab(queryTab.name); + else + this.setTab('summary'); + } + + setCurrentTab(instance) + { + this.currentTab = instance; + this.setState({loaded: true}); + } + + setTab(tabType) { + this.setState({loaded: false}); + this.tabIndex += 1; + + switch(tabType) + { + case 'summary': + this.setCurrentTab(); + break; + case 'transactions': + this.setCurrentTab(); + break; + } + + this.state.tabs.map(({ name, ref }) => { + if(name == tabType) + ref.current.classList.add('active'); + else + ref.current.classList.remove('active'); + }); + } + + render() { + return ( + <> +
    + { + this.state.tabs.map(({ label, name, ref }) => +
  • + +
  • + ) + } +
+
+ { this.state.loaded ? this.currentTab : } +
+ + ); + } +} + +export default Transactions; \ No newline at end of file diff --git a/web/resources/js/pages/Transactions.js b/web/resources/js/pages/Transactions.js new file mode 100644 index 0000000..271ad8a --- /dev/null +++ b/web/resources/js/pages/Transactions.js @@ -0,0 +1,18 @@ +/* + Copyright © XlXi 2022 +*/ + +import $ from 'jquery'; + +import React from 'react'; +import { render } from 'react-dom'; + +import Transactions from '../components/Transactions'; + +const transactionsId = 'vb-transactions'; + +$(document).ready(function() { + if (document.getElementById(transactionsId)) { + render(, document.getElementById(transactionsId)); + } +}); \ No newline at end of file diff --git a/web/resources/views/components/admin/user-search-input.blade.php b/web/resources/views/components/admin/user-search-input.blade.php index e6f18ae..7690d7f 100644 --- a/web/resources/views/components/admin/user-search-input.blade.php +++ b/web/resources/views/components/admin/user-search-input.blade.php @@ -9,21 +9,20 @@ @if(!isset($nolabel) || !$nolabel) @endif -
- @csrf +
- +
\ No newline at end of file diff --git a/web/resources/views/components/user-circle.blade.php b/web/resources/views/components/user-circle.blade.php index 391d71b..116bc4e 100644 --- a/web/resources/views/components/user-circle.blade.php +++ b/web/resources/views/components/user-circle.blade.php @@ -24,9 +24,8 @@ @endphp - {{-- TODO: XlXi: User headshots --}}
-

- {{ \App\Helpers\NumberHelper::Abbreviate(Auth::user()->tokens) }} -

+ + +

+ {{ \App\Helpers\NumberHelper::Abbreviate(Auth::user()->tokens) }} +

+
+
diff --git a/web/resources/views/pagination/virtubrick.blade.php b/web/resources/views/pagination/virtubrick.blade.php new file mode 100644 index 0000000..2785d40 --- /dev/null +++ b/web/resources/views/pagination/virtubrick.blade.php @@ -0,0 +1,13 @@ +@if ($paginator->hasPages()) + +@endif \ No newline at end of file diff --git a/web/resources/views/web/admin/useradmin.blade.php b/web/resources/views/web/admin/useradmin.blade.php index 4f6a77f..c852907 100644 --- a/web/resources/views/web/admin/useradmin.blade.php +++ b/web/resources/views/web/admin/useradmin.blade.php @@ -56,7 +56,7 @@ 'virtubrick-error-popup', 'mt-3' => !$isProtected ])> - This user is a {{ $powerType }}. + This user is a(n) {{ $powerType }}. @endif {{ $user->username }} @@ -71,7 +71,7 @@ Website TODO
- +
User Homepage
@@ -85,6 +85,7 @@ + @@ -96,7 +97,12 @@ @foreach($user->punishments as $punishment) - + + + + + @endforeach
ID Action Moderator
{{ $punishment->id }} + +  {{ $punishment->id }} {{ $punishment->punishment_type->label }} @@ -107,6 +113,27 @@ {{ $punishment->expirationStr() }} {{ $punishment->active ? 'No' : 'Yes' }}
+
+

Note to User: {{ $punishment->user_note }}

+ @if($punishment->context->count() > 0) +

Abuses:

+ @endif + @foreach($punishment->context as $context) +
+

Reason: {{ $context->user_note }} (TODO: audit)

+ @if($context->description) +

Offensive Item: {{ $context->description }}

+ @endif + @if($context->content_hash) + + @endif +
+ @endforeach +
+
diff --git a/web/resources/views/web/admin/usersearch.blade.php b/web/resources/views/web/admin/usersearch.blade.php index 0ac3cfb..f2b9c47 100644 --- a/web/resources/views/web/admin/usersearch.blade.php +++ b/web/resources/views/web/admin/usersearch.blade.php @@ -83,6 +83,7 @@ @endforeach + {{ $users->links('pagination.virtubrick') }}
@endif
diff --git a/web/resources/views/web/avatar/editor.blade.php b/web/resources/views/web/avatar/editor.blade.php new file mode 100644 index 0000000..38f9673 --- /dev/null +++ b/web/resources/views/web/avatar/editor.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.app') + +@section('title', 'Avatar Editor') + +@section('content') +
+

Avatar Editor

+
+
+
+ +
+ +

Colors

+
+ +
+
+
+ +
+ +
+ +

Currently Wearing

+
+ +
+
+
+

Todo:

+
    +
  • Character Preview
  • +
  • 3d Character Preview
  • +
  • Redraw button
  • +
  • Section for changing body colors
  • +
  • Section for wearing items
  • +
  • Section for taking off worn items
  • +
+
+@endsection \ No newline at end of file diff --git a/web/resources/views/web/home/dashboard.blade.php b/web/resources/views/web/home/dashboard.blade.php index 525d01f..6f0b365 100644 --- a/web/resources/views/web/home/dashboard.blade.php +++ b/web/resources/views/web/home/dashboard.blade.php @@ -12,7 +12,7 @@

Hello, {{ Auth::user()->username }}!

- +

Blog

diff --git a/web/resources/views/web/money/transactions.blade.php b/web/resources/views/web/money/transactions.blade.php new file mode 100644 index 0000000..6277106 --- /dev/null +++ b/web/resources/views/web/money/transactions.blade.php @@ -0,0 +1,26 @@ +@extends('layouts.app') + +@section('title', 'Transactions') + +@section('page-specific') + +@endsection + +@section('content') +
+

Transactions

+
+ +
+ +
+
+
+@endsection \ No newline at end of file diff --git a/web/resources/views/web/shop/asset.blade.php b/web/resources/views/web/shop/asset.blade.php index aee52eb..4b99c05 100644 --- a/web/resources/views/web/shop/asset.blade.php +++ b/web/resources/views/web/shop/asset.blade.php @@ -81,7 +81,7 @@

{{ $asset->name }}

By {{ $asset->user->username }} - @if(Auth::user()->hasAsset($asset->id)) + @if(Auth::user() && Auth::user()->hasAsset($asset->id))     diff --git a/web/resources/views/web/user/profile.blade.php b/web/resources/views/web/user/profile.blade.php index 8567946..0710744 100644 --- a/web/resources/views/web/user/profile.blade.php +++ b/web/resources/views/web/user/profile.blade.php @@ -18,7 +18,7 @@

- User avatar of {{ $user->username }} + User avatar of {{ $user->username }}
{{-- TODO: XlXi: Advanced presence --}} diff --git a/web/routes/api.php b/web/routes/api.php index c8566da..8ae8318 100644 --- a/web/routes/api.php +++ b/web/routes/api.php @@ -51,6 +51,9 @@ Route::group(['as' => 'shop.', 'prefix' => 'shop'], function() { Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() { Route::get('/list-json', 'ShopController@listJson')->name('list'); Route::post('/purchase/{asset}', 'ShopController@purchase')->name('purchase'); + + Route::get('/user-summary', 'MoneyController@userSummary')->name('summary'); + Route::get('/user-transactions', 'MoneyController@userTransactions')->name('transactions'); }); }); diff --git a/web/routes/web.php b/web/routes/web.php index 9b4dc80..2ad86fa 100644 --- a/web/routes/web.php +++ b/web/routes/web.php @@ -35,7 +35,14 @@ Route::group(['as' => 'games.', 'prefix' => 'games'], function() { Route::middleware('auth')->group(function () { Route::group(['as' => 'user.', 'prefix' => 'my'], function() { - Route::get('/settings', 'SettingsController@index')->name('index'); + Route::get('/settings', 'SettingsController@index')->name('settings'); + Route::get('/avatar', 'AvatarController@index')->name('avatarEditor'); + Route::get('/transactions', 'MoneyController@transactions')->name('transactions'); + }); + + Route::group(['as' => 'punishment.', 'prefix' => 'membership'], function() { + Route::get('/not-approved', 'ModerationController@notice')->name('notice'); + Route::post('/not-approved', 'ModerationController@reactivate')->name('reactivate'); }); }); @@ -50,7 +57,6 @@ Route::group(['as' => 'admin.', 'prefix' => 'admin'], function() { Route::group(['prefix' => 'users'], function() { Route::get('/useradmin', 'AdminController@userAdmin')->name('useradmin'); Route::get('/find', 'AdminController@userSearch')->name('usersearch'); - Route::post('/find', 'AdminController@userSearchQuery')->name('usersearchquery'); Route::get('/userlookuptool', 'AdminController@userLookup')->name('userlookup'); Route::post('/userlookuptool', 'AdminController@userLookupQuery')->name('userlookupquery'); }); @@ -118,13 +124,6 @@ Route::group(['as' => 'auth.', 'namespace' => 'Auth'], function() { }); }); -Route::group(['as' => 'punishment.', 'prefix' => 'membership'], function() { - Route::middleware('auth')->group(function () { - Route::get('/not-approved', 'ModerationController@notice')->name('notice'); - Route::post('/not-approved', 'ModerationController@reactivate')->name('reactivate'); - }); -}); - Route::withoutMiddleware(['csrf'])->group(function () { Route::group(['as' => 'client.'], function() { Route::get('/asset', 'ClientController@asset')->name('asset'); diff --git a/web/storage/app/grid/scripts/Avatar.lua b/web/storage/app/grid/scripts/Avatar.lua index e47fe1d..0d6200f 100644 --- a/web/storage/app/grid/scripts/Avatar.lua +++ b/web/storage/app/grid/scripts/Avatar.lua @@ -1,4 +1,4 @@ -local characterAppearanceUrl, baseUrl, fileExtension, x, y = ... +local characterAppearanceUrl, fileExtension, x, y, baseUrl = ... pcall(function() game:GetService("ContentProvider"):SetBaseUrl(baseUrl) end) game:GetService("ContentProvider"):SetThreadPool(16) diff --git a/web/storage/app/grid/scripts/Closeup.lua b/web/storage/app/grid/scripts/Closeup.lua index aaef846..f4dc86f 100644 --- a/web/storage/app/grid/scripts/Closeup.lua +++ b/web/storage/app/grid/scripts/Closeup.lua @@ -1,4 +1,4 @@ -local baseUrl, characterAppearanceUrl, fileExtension, x, y, quadratic, baseHatZoom, maxHatZoom, cameraOffsetX, cameraOffsetY = ... +local characterAppearanceUrl, fileExtension, x, y, baseUrl, quadratic, baseHatZoom, maxHatZoom, cameraOffsetX, cameraOffsetY = ... pcall(function() game:GetService("ContentProvider"):SetBaseUrl(baseUrl) end) game:GetService("ContentProvider"):SetThreadPool(16) diff --git a/web/webpack.mix.js b/web/webpack.mix.js index deb4fa3..d6d9dc7 100644 --- a/web/webpack.mix.js +++ b/web/webpack.mix.js @@ -11,6 +11,7 @@ mix.js('resources/js/app.js', 'public/js') .js('resources/js/pages/Dashboard.js', 'public/js') .js('resources/js/pages/Shop.js', 'public/js') .js('resources/js/pages/Games.js', 'public/js') + .js('resources/js/pages/Transactions.js', 'public/js') .js('resources/js/pages/Item.js', 'public/js') .js('resources/js/pages/Place.js', 'public/js') .js('resources/js/pages/AppDeployer.js', 'public/js/adm')