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 0000000..76d29e2 Binary files /dev/null and b/web/public/images/busy/user.png differ diff --git a/web/resources/js/components/Shop.js b/web/resources/js/components/Shop.js index 91bd98a..fd1d280 100644 --- a/web/resources/js/components/Shop.js +++ b/web/resources/js/components/Shop.js @@ -127,6 +127,7 @@ class ShopCategoryButton extends Component { let categoryAssetTypeIds = this.props.getCategoryAssetTypeIds(categoryName); switch(typeof(categoryAssetTypeIds.assetTypeId)) { + case 'string': case 'number': assetTypes[categoryAssetTypeIds.assetTypeId] = true; break; diff --git a/web/resources/js/components/Transactions.js b/web/resources/js/components/Transactions.js new file mode 100644 index 0000000..9583997 --- /dev/null +++ b/web/resources/js/components/Transactions.js @@ -0,0 +1,328 @@ +/* + Copyright © XlXi 2022 +*/ + +import { Component, createRef } from 'react'; +import axios from 'axios'; + +import { buildGenericApiUrl } from '../util/HTTP.js'; +import Loader from './Loader'; + +axios.defaults.withCredentials = true; + +class SummaryTab extends Component { + constructor(props) { + super(props); + this.state = { + loaded: false, + filters: [ + {label: 'Past Day', tag: 'pastday', ref: createRef()}, + {label: 'Past Week', tag: 'pastweek', ref: createRef()}, + {label: 'Past Month', tag: 'pastmonth', ref: createRef()}, + {label: 'Past Year', tag: 'pastyear', ref: createRef()}, + {label: 'All Time', tag: 'all', ref: createRef()} + ], + items: [], + total: null + }; + + this.categoryDropdown = createRef(); + + this.setTag = this.setTag.bind(this); + } + + componentDidMount() { + this.setTag('pastday'); + } + + 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'); + }); + + 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.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.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')