More catalog changes.
- Added padding to punishments page. - Also fixes a bug with the expiration date in the Punishment model. - Transactionss. - Created methods for user assets to automatically calculate an item's serial. - Added a "hasAsset" function to the User model to check if a user owns an asset id. - Asset purchase API. - Fix bug where apis could be accessed despite the site being under maintenance. - Fixed rounding issue with number formatting. - Updated shop JS to allow for purchases. - Added userasset downloading to the /asset client api.
This commit is contained in:
parent
0083a01d85
commit
c00e8ccc75
|
|
@ -26,6 +26,6 @@ class NumberHelper
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return number_format($number / $divisor, 0) . $shorthand;
|
return number_format(floor($number / $divisor), 0) . $shorthand;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,17 @@
|
||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
use App\Helpers\ValidationHelper;
|
use App\Helpers\ValidationHelper;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use Illuminate\Http\Request;
|
use App\Models\Transaction;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use App\Models\TransactionType;
|
||||||
use Illuminate\Support\Str;
|
use App\Models\UserAsset;
|
||||||
|
|
||||||
class ShopController extends Controller
|
class ShopController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -89,4 +94,57 @@ class ShopController extends Controller
|
||||||
'data' => $data
|
'data' => $data
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function purchase(Request $request, Asset $asset)
|
||||||
|
{
|
||||||
|
// TODO: XlXi: limiteds
|
||||||
|
$validator = Validator::make($request->all(), [
|
||||||
|
'expectedPrice' => ['int']
|
||||||
|
]);
|
||||||
|
|
||||||
|
if($validator->fails()) {
|
||||||
|
return ValidationHelper::generateValidatorError($validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
$valid = $validator->valid();
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'success' => false,
|
||||||
|
'userFacingMessage' => null,
|
||||||
|
'priceInTokens' => $asset->priceInTokens
|
||||||
|
];
|
||||||
|
$price = $asset->priceInTokens;
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
if($asset->assetType->locked)
|
||||||
|
{
|
||||||
|
$result['userFacingMessage'] = 'This asset cannot be purchased.';
|
||||||
|
$result['priceInTokens'] = null;
|
||||||
|
return response($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($user->hasAsset($asset->id))
|
||||||
|
{
|
||||||
|
$result['userFacingMessage'] = 'You already own this item.';
|
||||||
|
return response($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if($valid['expectedPrice'] != $price)
|
||||||
|
return response($result);
|
||||||
|
|
||||||
|
if($asset->priceInTokens > $user->tokens)
|
||||||
|
{
|
||||||
|
$result['userFacingMessage'] = 'You can\'t afford this item.';
|
||||||
|
return response($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['success'] = true;
|
||||||
|
|
||||||
|
Transaction::createAssetSale($user, $asset);
|
||||||
|
$user->removeFunds($price);
|
||||||
|
$asset->user->addFunds($price * (1-.3)); // XlXi: 30% tax
|
||||||
|
UserAsset::createSerialed($user->id, $asset->id);
|
||||||
|
|
||||||
|
return response($result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use App\Http\Controllers\Controller;
|
||||||
use App\Models\Asset;
|
use App\Models\Asset;
|
||||||
use App\Models\AssetVersion;
|
use App\Models\AssetVersion;
|
||||||
use App\Models\RobloxAsset;
|
use App\Models\RobloxAsset;
|
||||||
|
use App\Models\UserAsset;
|
||||||
|
|
||||||
class ClientController extends Controller
|
class ClientController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -43,16 +44,27 @@ class ClientController extends Controller
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function userAssetValidator()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'userassetid' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists('App\Models\UserAsset', 'id')
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function asset(Request $request)
|
function asset(Request $request)
|
||||||
{
|
{
|
||||||
// TODO: XlXi: userAssetId (owned asset)
|
|
||||||
$reqData = array_change_key_case($request->all());
|
$reqData = array_change_key_case($request->all());
|
||||||
|
$isVersionIdRequest = array_key_exists('assetversionid', $reqData);
|
||||||
|
$isUserAssetIdRequest = array_key_exists('userassetid', $reqData);
|
||||||
|
|
||||||
$validatorRuleSet = 'assetRegularValidator';
|
$validatorRuleSet = 'assetRegularValidator';
|
||||||
if(array_key_exists('assetversionid', $reqData))
|
if($isVersionIdRequest)
|
||||||
$validatorRuleSet = 'assetVersionValidator';
|
$validatorRuleSet = 'assetVersionValidator';
|
||||||
elseif(array_key_exists('userassetid', $reqData))
|
elseif($isUserAssetIdRequest)
|
||||||
return response('todo');
|
$validatorRuleSet = 'userAssetValidator';
|
||||||
|
|
||||||
$validator = Validator::make($reqData, $this->{$validatorRuleSet}());
|
$validator = Validator::make($reqData, $this->{$validatorRuleSet}());
|
||||||
|
|
||||||
|
|
@ -68,20 +80,23 @@ class ClientController extends Controller
|
||||||
$valid = $validator->valid();
|
$valid = $validator->valid();
|
||||||
$asset = null;
|
$asset = null;
|
||||||
|
|
||||||
if(array_key_exists('assetversionid', $reqData)) {
|
if($isVersionIdRequest) {
|
||||||
$assetVersion = AssetVersion::where('id', $valid['assetversionid'])->first();
|
$assetVersion = AssetVersion::where('id', $valid['assetversionid'])->first();
|
||||||
$asset = $assetVersion->asset;
|
$asset = $assetVersion->asset;
|
||||||
|
|
||||||
$valid['version'] = $assetVersion->localVersion;
|
$valid['version'] = $assetVersion->localVersion;
|
||||||
|
} elseif($isUserAssetIdRequest) {
|
||||||
|
$userAsset = UserAsset::where('id', $valid['userassetid'])->first();
|
||||||
|
$asset = $userAsset->asset;
|
||||||
} else {
|
} else {
|
||||||
$asset = Asset::where('id', $valid['id'])->first();
|
$asset = Asset::where('id', $valid['id'])->first();
|
||||||
|
|
||||||
if(!array_key_exists('version', $valid))
|
|
||||||
$valid['version'] = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!$isVersionIdRequest && !array_key_exists('version', $valid))
|
||||||
|
$valid['version'] = 0;
|
||||||
|
|
||||||
if($asset == null) {
|
if($asset == null) {
|
||||||
$validator->errors()->add('version', 'Unknown asset version.');
|
$validator->errors()->add('version', 'Unknown asset' . ($isVersionIdRequest ? ' version' : null) . '.');
|
||||||
return ValidationHelper::generateValidatorError($validator);
|
return ValidationHelper::generateValidatorError($validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,8 @@ class Kernel extends HttpKernel
|
||||||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
|
||||||
|
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||||
|
|
||||||
\App\Http\Middleware\UserPunishmentMiddleware::class
|
\App\Http\Middleware\UserPunishmentMiddleware::class
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ class Punishment extends Model
|
||||||
if(!$this->expiration)
|
if(!$this->expiration)
|
||||||
return 'Never';
|
return 'Never';
|
||||||
|
|
||||||
return $this->created_at->isoFormat('lll');
|
return $this->expiration->isoFormat('lll');
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function activeFor($userId)
|
public static function activeFor($userId)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Transaction extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'transaction_type_id',
|
||||||
|
'asset_id',
|
||||||
|
'place_id',
|
||||||
|
'user_id',
|
||||||
|
'delta',
|
||||||
|
'seller_id'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static function createPurchase($transaction)
|
||||||
|
{
|
||||||
|
return self::create(array_merge($transaction, [
|
||||||
|
'transaction_type_id' => TransactionType::where('name', 'Purchases')->first()->id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function createSale($transaction)
|
||||||
|
{
|
||||||
|
return self::create(array_merge($transaction, [
|
||||||
|
'transaction_type_id' => TransactionType::where('name', 'Sales')->first()->id
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createAssetSale($user, $asset, $placeId = null)
|
||||||
|
{
|
||||||
|
$transaction = [
|
||||||
|
'asset_id' => $asset->id,
|
||||||
|
'place_id' => $placeId,
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'delta' => $asset->priceInTokens,
|
||||||
|
'seller_id' => $asset->creatorId
|
||||||
|
];
|
||||||
|
|
||||||
|
// XlXi: Assets have a 30% tax.
|
||||||
|
return [
|
||||||
|
self::createPurchase($transaction),
|
||||||
|
self::createSale(array_merge($transaction, ['delta' => $transaction['delta'] * (1-.3)]))
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class TransactionType extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
}
|
||||||
|
|
@ -147,6 +147,25 @@ class User extends Authenticatable implements MustVerifyEmail
|
||||||
return $this->hasMany(Punishment::class, 'user_id');
|
return $this->hasMany(Punishment::class, 'user_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasAsset($assetId)
|
||||||
|
{
|
||||||
|
return UserAsset::where('asset_id', $assetId)
|
||||||
|
->where('owner_id', $this->id)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeFunds($delta)
|
||||||
|
{
|
||||||
|
$this->tokens -= $delta;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFunds($delta)
|
||||||
|
{
|
||||||
|
$this->tokens += $delta;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
public function _hasRolesetInternal($roleName)
|
public function _hasRolesetInternal($roleName)
|
||||||
{
|
{
|
||||||
$roleset = Roleset::where('Name', $roleName)->first();
|
$roleset = Roleset::where('Name', $roleName)->first();
|
||||||
|
|
|
||||||
|
|
@ -8,4 +8,29 @@ use Illuminate\Database\Eloquent\Model;
|
||||||
class UserAsset extends Model
|
class UserAsset extends Model
|
||||||
{
|
{
|
||||||
use HasFactory;
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = [
|
||||||
|
'owner_id',
|
||||||
|
'asset_id',
|
||||||
|
'serial'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function asset()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Asset::class, 'asset_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createSerialed($ownerId, $assetId)
|
||||||
|
{
|
||||||
|
return self::create([
|
||||||
|
'owner_id' => $ownerId,
|
||||||
|
'asset_id' => $assetId,
|
||||||
|
'serial' => self::where('asset_id', $assetId)->count()+1
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ return new class extends Migration
|
||||||
$table->unsignedBigInteger('downVotes')->default(0);
|
$table->unsignedBigInteger('downVotes')->default(0);
|
||||||
|
|
||||||
$table->unsignedBigInteger('priceInTokens')->default(15);
|
$table->unsignedBigInteger('priceInTokens')->default(15);
|
||||||
$table->unsignedBigInteger('sales')->default(0);
|
|
||||||
$table->boolean('onSale')->default(false);
|
$table->boolean('onSale')->default(false);
|
||||||
|
|
||||||
$table->unsignedSmallInteger('assetTypeId');
|
$table->unsignedSmallInteger('assetTypeId');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('transactions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->unsignedSmallInteger('transaction_type_id');
|
||||||
|
$table->unsignedBigInteger('asset_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('place_id')->nullable();
|
||||||
|
$table->unsignedBigInteger('user_id');
|
||||||
|
$table->bigInteger('delta');
|
||||||
|
$table->unsignedBigInteger('seller_id');
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('transactions');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('transaction_types', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
$table->string('name');
|
||||||
|
$table->string('format');
|
||||||
|
$table->boolean('hidden')->default(false);
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('transaction_types');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -19,7 +19,8 @@ class DatabaseSeeder extends Seeder
|
||||||
AssetTypeSeeder::class,
|
AssetTypeSeeder::class,
|
||||||
UsageCounterSeeder::class,
|
UsageCounterSeeder::class,
|
||||||
RolesetSeeder::class,
|
RolesetSeeder::class,
|
||||||
PunishmentTypeSeeder::class
|
PunishmentTypeSeeder::class,
|
||||||
|
TransactionTypeSeeder::class
|
||||||
//FFlagSeeder::class
|
//FFlagSeeder::class
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
use App\Models\TransactionType;
|
||||||
|
|
||||||
|
class TransactionTypeSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
TransactionType::create(['name' => 'Purchases', 'format' => 'Purchased ']);
|
||||||
|
TransactionType::create(['name' => 'Sales', 'format' => 'Sold ']);
|
||||||
|
TransactionType::create(['name' => 'Commissions', 'format' => 'Sold ']); // third party sales
|
||||||
|
TransactionType::create(['name' => 'Group Payouts', 'format' => '']);
|
||||||
|
TransactionType::create(['name' => 'Admin Adjustment', 'format' => 'Tokens adjusted by administrator.', 'hidden' => true]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,198 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createRef, Component } from 'react';
|
import { createRef, Component } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
import classNames from 'classnames/bind';
|
import classNames from 'classnames/bind';
|
||||||
|
import ProgressiveImage from './ProgressiveImage';
|
||||||
|
import { buildGenericApiUrl } from '../util/HTTP.js';
|
||||||
|
|
||||||
|
axios.defaults.withCredentials = true;
|
||||||
|
|
||||||
const itemId = 'vb-item';
|
const itemId = 'vb-item';
|
||||||
|
|
||||||
class PurchaseConfirmationModal extends Component {
|
class PurchaseConfirmationModal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
buttonDisabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ModalRef = createRef();
|
||||||
|
this.Modal = null;
|
||||||
|
|
||||||
|
this.purchaseAsset = this.purchaseAsset.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let itemElement = document.getElementById(itemId);
|
||||||
|
if(itemElement) {
|
||||||
|
this.setState({
|
||||||
|
assetId: itemElement.getAttribute('data-asset-id'),
|
||||||
|
assetName: itemElement.getAttribute('data-asset-name'),
|
||||||
|
assetCreator: itemElement.getAttribute('data-asset-creator'),
|
||||||
|
assetType: itemElement.getAttribute('data-asset-type'),
|
||||||
|
assetPrice: parseInt(itemElement.getAttribute('data-asset-price')),
|
||||||
|
assetThumbnail: itemElement.getAttribute('data-asset-thumbnail-2d'),
|
||||||
|
userTokens: parseInt(itemElement.getAttribute('data-user-currency'))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
||||||
|
this.Modal.show();
|
||||||
|
|
||||||
|
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||||
|
this.props.setModal(null);
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
this.setState({buttonDisabled: false});
|
||||||
|
}.bind(this), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.Modal.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
purchaseAsset() {
|
||||||
|
console.log('hi');
|
||||||
|
if(this.state.buttonDisabled) return;
|
||||||
|
|
||||||
|
this.setState({buttonDisabled: true});
|
||||||
|
|
||||||
|
axios.post(buildGenericApiUrl('api', `shop/v1/purchase/${this.state.assetId}?expectedPrice=${this.state.assetPrice}`))
|
||||||
|
.then(res => {
|
||||||
|
if(res.data.success)
|
||||||
|
{
|
||||||
|
this.props.setModal(<PurchaseSuccessModal setModal={this.props.setModal} />);
|
||||||
|
}
|
||||||
|
else if(res.data.userFacingMessage != null)
|
||||||
|
{
|
||||||
|
this.props.setModal(
|
||||||
|
<PurchaseErrorModal setModal={this.props.setModal}>
|
||||||
|
<p>{ res.data.userFacingMessage }</p>
|
||||||
|
</PurchaseErrorModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
let oldPrice = this.state.assetPrice;
|
||||||
|
let newTokens = (this.state.userTokens - res.data.priceInTokens);
|
||||||
|
this.setState({assetPrice: res.data.priceInTokens, buttonDisabled: false});
|
||||||
|
this.props.setModal(
|
||||||
|
<PurchaseErrorModal setModal={ this.props.setModal } purchaseAsset={ this.purchaseAsset } newTokens={ newTokens }>
|
||||||
|
<p>This item's price has changed from <span className="virtubrick-tokens">{ oldPrice }</span> to <span className="virtubrick-tokens">{ res.data.priceInTokens }</span>. {newTokens >= 0 ? 'Would you still like to purchase?' : null }</p>
|
||||||
|
</PurchaseErrorModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
this.props.setModal(
|
||||||
|
<PurchaseErrorModal setModal={this.props.setModal}>
|
||||||
|
<p>An unexpected error occurred while purchasing this item.</p>
|
||||||
|
</PurchaseErrorModal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div ref={this.ModalRef} className="modal fade">
|
||||||
|
<div className="modal-dialog modal-dialog-centered">
|
||||||
|
<div className="modal-content text-center">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 className="modal-title">Purchase Item</h5>
|
||||||
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body d-flex flex-column">
|
||||||
|
<p>Would you like to purchase the { this.state.assetType } <strong>{ this.state.assetName }</strong> from { this.state.assetCreator } for <span className="virtubrick-tokens">{ this.state.assetPrice }</span>?</p>
|
||||||
|
<ProgressiveImage
|
||||||
|
src={ this.state.assetThumbnail }
|
||||||
|
placeholderImg={ buildGenericApiUrl('www', 'images/busy/asset.png') }
|
||||||
|
alt={ this.state.assetName }
|
||||||
|
width='240'
|
||||||
|
height='240'
|
||||||
|
className='mx-auto img-fluid'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer flex-column">
|
||||||
|
<div className="mx-auto">
|
||||||
|
<button className="btn btn-success" disabled={ this.state.buttonDisabled } onClick={ this.purchaseAsset }>Purchase</button>
|
||||||
|
|
||||||
|
<button className="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted pt-1">You will have <span className="virtubrick-tokens">{ Math.max(0, (this.state.userTokens - this.state.assetPrice)) }</span> after this purchase.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PurchaseErrorModal extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
buttonDisabled: true
|
||||||
|
};
|
||||||
|
|
||||||
|
this.ModalRef = createRef();
|
||||||
|
this.Modal = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
||||||
|
this.Modal.show();
|
||||||
|
|
||||||
|
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||||
|
this.props.setModal(null);
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
this.setState({buttonDisabled: false});
|
||||||
|
}.bind(this), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.Modal.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div ref={this.ModalRef} className="modal fade">
|
||||||
|
<div className="modal-dialog modal-dialog-centered">
|
||||||
|
<div className="modal-content text-center">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 className="modal-title">Error Occurred</h5>
|
||||||
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
{ this.props.children }
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer flex-column">
|
||||||
|
{
|
||||||
|
this.props.purchaseAsset && this.props.newTokens >= 0 ?
|
||||||
|
<>
|
||||||
|
<div className="mx-auto">
|
||||||
|
<button className="btn btn-success" disabled={ this.state.buttonDisabled } onClick={ () => { this.setState({buttonDisabled: true}); this.props.purchaseAsset(); } }>Purchase</button>
|
||||||
|
|
||||||
|
<button className="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted pt-1">You will have <span className="virtubrick-tokens">{ this.props.newTokens }</span> after this purchase.</p>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<button className="btn btn-secondary" data-bs-dismiss="modal">Ok</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PurchaseSuccessModal extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
|
@ -24,19 +210,22 @@ class PurchaseConfirmationModal extends Component {
|
||||||
if(itemElement) {
|
if(itemElement) {
|
||||||
this.setState({
|
this.setState({
|
||||||
assetName: itemElement.getAttribute('data-asset-name'),
|
assetName: itemElement.getAttribute('data-asset-name'),
|
||||||
assetCreator: itemElement.getAttribute('data-asset-creator'),
|
assetPrice: parseInt(itemElement.getAttribute('data-asset-price'))
|
||||||
assetType: itemElement.getAttribute('data-asset-type'),
|
|
||||||
assetPrice: parseInt(itemElement.getAttribute('data-asset-price')),
|
|
||||||
userTokens: parseInt(itemElement.getAttribute('data-user-currency'))
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
this.Modal = new Bootstrap.Modal(
|
||||||
|
this.ModalRef.current,
|
||||||
|
{
|
||||||
|
backdrop: 'static',
|
||||||
|
keyboard: false
|
||||||
|
}
|
||||||
|
);
|
||||||
this.Modal.show();
|
this.Modal.show();
|
||||||
|
|
||||||
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
setTimeout(function(){
|
||||||
this.props.setModal(null);
|
window.location.reload();
|
||||||
})
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
@ -49,20 +238,10 @@ class PurchaseConfirmationModal extends Component {
|
||||||
<div className="modal-dialog modal-dialog-centered">
|
<div className="modal-dialog modal-dialog-centered">
|
||||||
<div className="modal-content text-center">
|
<div className="modal-content text-center">
|
||||||
<div className="modal-header">
|
<div className="modal-header">
|
||||||
<h5 className="modal-title">Purchase Item</h5>
|
<h5 className="modal-title">Purchase Successful</h5>
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-body d-flex flex-column">
|
<div className="modal-body">
|
||||||
<p>Would you like to purchase the { this.state.assetType } <strong>{ this.state.assetName }</strong> from { this.state.assetCreator } for <span className="virtubrick-tokens">{ this.state.assetPrice }</span>?</p>
|
<p>You have successfully purchased { this.state.assetName } for <span className="virtubrick-tokens">{ this.state.assetPrice }</span>!</p>
|
||||||
<img src="/images/testing/hat.png" width="240" height="240" alt={ this.state.assetName } className="mx-auto my-2 img-fluid" />
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer flex-column">
|
|
||||||
<div className="mx-auto">
|
|
||||||
<button className="btn btn-success">Purchase</button>
|
|
||||||
|
|
||||||
<button className="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted pt-1">You will have <span className="virtubrick-tokens">{ Math.max(0, (this.state.userTokens - this.state.assetPrice)) }</span> after this purchase.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -130,7 +309,8 @@ class PurchaseButton extends Component {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
showModal: false
|
showModal: false,
|
||||||
|
canPurchase: false
|
||||||
};
|
};
|
||||||
|
|
||||||
this.visibleModal = null;
|
this.visibleModal = null;
|
||||||
|
|
@ -144,7 +324,11 @@ class PurchaseButton extends Component {
|
||||||
if(itemElement) {
|
if(itemElement) {
|
||||||
this.setState({
|
this.setState({
|
||||||
assetOnSale: (itemElement.getAttribute('data-asset-on-sale') === '1'),
|
assetOnSale: (itemElement.getAttribute('data-asset-on-sale') === '1'),
|
||||||
canAfford: (itemElement.getAttribute('data-can-afford') === '1')
|
canAfford: (itemElement.getAttribute('data-can-afford') === '1'),
|
||||||
|
owned: (itemElement.getAttribute('data-owned') === '1')
|
||||||
|
}, function(){
|
||||||
|
let canPurchase = (!this.state.owned && this.state.assetOnSale);
|
||||||
|
this.setState({canPurchase: canPurchase});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -170,23 +354,37 @@ class PurchaseButton extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
this.state.loaded
|
||||||
|
?
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'px-5': true,
|
'px-5': true,
|
||||||
'btn': true,
|
'btn': true,
|
||||||
'btn-lg': true,
|
'btn-lg': true,
|
||||||
'btn-success': this.state.assetOnSale,
|
'btn-success': this.state.canPurchase,
|
||||||
'btn-secondary': !this.state.assetOnSale
|
'btn-secondary': !this.state.canPurchase
|
||||||
})}
|
})}
|
||||||
disabled={ !(this.state.loaded && this.state.assetOnSale) ? true : null }
|
disabled={ !this.state.canPurchase ? true : null }
|
||||||
onClick={ this.showPrompt }
|
onClick={ this.showPrompt }
|
||||||
>
|
>
|
||||||
{ (!this.state.loaded || this.state.assetOnSale) ? 'Buy' : 'Offsale' }
|
{
|
||||||
|
this.state.canPurchase
|
||||||
|
?
|
||||||
|
'Buy'
|
||||||
|
:
|
||||||
|
this.state.owned
|
||||||
|
?
|
||||||
|
'Owned'
|
||||||
|
:
|
||||||
|
'Offsale'
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{ this.state.showModal ? this.visibleModal : null }
|
{ this.state.showModal ? this.visibleModal : null }
|
||||||
</>
|
</>
|
||||||
|
:
|
||||||
|
<button className="px-5 btn btn-lg btn-success" disabled={true}>Buy</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container m-auto">
|
<div class="container m-auto">
|
||||||
<div class="card p-3 virtubrick-moderation-card">
|
<div class="card p-3 my-4 virtubrick-moderation-card">
|
||||||
<h3>{{ $punishment->punishment_type->label }}</h3>
|
<h3>{{ $punishment->punishment_type->label }}</h3>
|
||||||
<p>
|
<p>
|
||||||
Your account has been {{ $punishment->isDeletion() ? 'closed ' : 'temporarily restricted' }} for violating our Terms of Service.
|
Your account has been {{ $punishment->isDeletion() ? 'closed ' : 'temporarily restricted' }} for violating our Terms of Service.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@
|
||||||
|
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="container mx-auto py-5">
|
<div class="container mx-auto py-5">
|
||||||
|
@if(isset($status))
|
||||||
|
<div class="alert alert-success text-center">{{ $status }}</div>
|
||||||
|
@endif
|
||||||
@if(!$asset->approved)
|
@if(!$asset->approved)
|
||||||
<div class="alert alert-danger text-center"><strong>This asset is pending approval.</strong> It will not appear in-game and cannot be voted on or purchased at this time.</div>
|
<div class="alert alert-danger text-center"><strong>This asset is pending approval.</strong> It will not appear in-game and cannot be voted on or purchased at this time.</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -44,7 +47,9 @@
|
||||||
data-asset-name="{{ $asset->name }}"
|
data-asset-name="{{ $asset->name }}"
|
||||||
data-asset-creator="{{ $asset->user->username }}"
|
data-asset-creator="{{ $asset->user->username }}"
|
||||||
data-asset-type="{{ $asset->typeString() }}"
|
data-asset-type="{{ $asset->typeString() }}"
|
||||||
|
data-asset-thumbnail-2d="{{ $asset->getThumbnail() }}"
|
||||||
data-asset-on-sale="{{ $asset->onSale }}"
|
data-asset-on-sale="{{ $asset->onSale }}"
|
||||||
|
data-owned="{{ Auth::user()->hasAsset($asset->id) ? '1' : '0' }}"
|
||||||
@if ($asset->onSale)
|
@if ($asset->onSale)
|
||||||
data-asset-price="{{ $asset->priceInTokens }}"
|
data-asset-price="{{ $asset->priceInTokens }}"
|
||||||
data-user-currency="{{ Auth::user()->tokens }}"
|
data-user-currency="{{ Auth::user()->tokens }}"
|
||||||
|
|
@ -74,7 +79,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-fill">
|
<div class="flex-fill">
|
||||||
<h3 class="mb-0">{{ $asset->name }}</h3>
|
<h3 class="mb-0">{{ $asset->name }}</h3>
|
||||||
<p>By <a class="text-decoration-none fw-normal" href="{{ $asset->user->getProfileUrl() }}">{{ $asset->user->username }}</a></p>
|
<p>
|
||||||
|
By <a class="text-decoration-none fw-normal" href="{{ $asset->user->getProfileUrl() }}">{{ $asset->user->username }}</a>
|
||||||
|
@if(Auth::user()->hasAsset($asset->id))
|
||||||
|
<span>
|
||||||
|
|
||||||
|
|
||||||
|
<i class="fa-solid fa-circle-check text-success"></i>
|
||||||
|
Item Owned
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</p>
|
||||||
<hr />
|
<hr />
|
||||||
{{-- TODO: XlXi: limiteds/trading --}}
|
{{-- TODO: XlXi: limiteds/trading --}}
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ Route::group(['as' => 'comments.', 'prefix' => 'comments'], function() {
|
||||||
Route::group(['as' => 'shop.', 'prefix' => 'shop'], function() {
|
Route::group(['as' => 'shop.', 'prefix' => 'shop'], function() {
|
||||||
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
|
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
|
||||||
Route::get('/list-json', 'ShopController@listJson')->name('list');
|
Route::get('/list-json', 'ShopController@listJson')->name('list');
|
||||||
|
Route::post('/purchase/{asset}', 'ShopController@purchase')->name('purchase');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue