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;
|
||||
}
|
||||
|
||||
return number_format($number / $divisor, 0) . $shorthand;
|
||||
return number_format(floor($number / $divisor), 0) . $shorthand;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,17 @@
|
|||
|
||||
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\Http\Controllers\Controller;
|
||||
use App\Models\Asset;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\Transaction;
|
||||
use App\Models\TransactionType;
|
||||
use App\Models\UserAsset;
|
||||
|
||||
class ShopController extends Controller
|
||||
{
|
||||
|
|
@ -89,4 +94,57 @@ class ShopController extends Controller
|
|||
'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\AssetVersion;
|
||||
use App\Models\RobloxAsset;
|
||||
use App\Models\UserAsset;
|
||||
|
||||
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)
|
||||
{
|
||||
// TODO: XlXi: userAssetId (owned asset)
|
||||
$reqData = array_change_key_case($request->all());
|
||||
$isVersionIdRequest = array_key_exists('assetversionid', $reqData);
|
||||
$isUserAssetIdRequest = array_key_exists('userassetid', $reqData);
|
||||
|
||||
$validatorRuleSet = 'assetRegularValidator';
|
||||
if(array_key_exists('assetversionid', $reqData))
|
||||
if($isVersionIdRequest)
|
||||
$validatorRuleSet = 'assetVersionValidator';
|
||||
elseif(array_key_exists('userassetid', $reqData))
|
||||
return response('todo');
|
||||
elseif($isUserAssetIdRequest)
|
||||
$validatorRuleSet = 'userAssetValidator';
|
||||
|
||||
$validator = Validator::make($reqData, $this->{$validatorRuleSet}());
|
||||
|
||||
|
|
@ -68,20 +80,23 @@ class ClientController extends Controller
|
|||
$valid = $validator->valid();
|
||||
$asset = null;
|
||||
|
||||
if(array_key_exists('assetversionid', $reqData)) {
|
||||
if($isVersionIdRequest) {
|
||||
$assetVersion = AssetVersion::where('id', $valid['assetversionid'])->first();
|
||||
$asset = $assetVersion->asset;
|
||||
|
||||
$valid['version'] = $assetVersion->localVersion;
|
||||
} elseif($isUserAssetIdRequest) {
|
||||
$userAsset = UserAsset::where('id', $valid['userassetid'])->first();
|
||||
$asset = $userAsset->asset;
|
||||
} else {
|
||||
$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) {
|
||||
$validator->errors()->add('version', 'Unknown asset version.');
|
||||
$validator->errors()->add('version', 'Unknown asset' . ($isVersionIdRequest ? ' version' : null) . '.');
|
||||
return ValidationHelper::generateValidatorError($validator);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ class Kernel extends HttpKernel
|
|||
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
|
||||
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
|
||||
|
||||
\App\Http\Middleware\UserPunishmentMiddleware::class
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ class Punishment extends Model
|
|||
if(!$this->expiration)
|
||||
return 'Never';
|
||||
|
||||
return $this->created_at->isoFormat('lll');
|
||||
return $this->expiration->isoFormat('lll');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$roleset = Roleset::where('Name', $roleName)->first();
|
||||
|
|
|
|||
|
|
@ -8,4 +8,29 @@ use Illuminate\Database\Eloquent\Model;
|
|||
class UserAsset extends Model
|
||||
{
|
||||
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('priceInTokens')->default(15);
|
||||
$table->unsignedBigInteger('sales')->default(0);
|
||||
$table->boolean('onSale')->default(false);
|
||||
|
||||
$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,
|
||||
UsageCounterSeeder::class,
|
||||
RolesetSeeder::class,
|
||||
PunishmentTypeSeeder::class
|
||||
PunishmentTypeSeeder::class,
|
||||
TransactionTypeSeeder::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 axios from 'axios';
|
||||
|
||||
import classNames from 'classnames/bind';
|
||||
import ProgressiveImage from './ProgressiveImage';
|
||||
import { buildGenericApiUrl } from '../util/HTTP.js';
|
||||
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const itemId = 'vb-item';
|
||||
|
||||
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) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
|
@ -24,19 +210,22 @@ class PurchaseConfirmationModal extends Component {
|
|||
if(itemElement) {
|
||||
this.setState({
|
||||
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')),
|
||||
userTokens: parseInt(itemElement.getAttribute('data-user-currency'))
|
||||
assetPrice: parseInt(itemElement.getAttribute('data-asset-price'))
|
||||
});
|
||||
}
|
||||
|
||||
this.Modal = new Bootstrap.Modal(this.ModalRef.current);
|
||||
this.Modal = new Bootstrap.Modal(
|
||||
this.ModalRef.current,
|
||||
{
|
||||
backdrop: 'static',
|
||||
keyboard: false
|
||||
}
|
||||
);
|
||||
this.Modal.show();
|
||||
|
||||
this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
|
||||
this.props.setModal(null);
|
||||
})
|
||||
setTimeout(function(){
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
@ -49,20 +238,10 @@ class PurchaseConfirmationModal extends Component {
|
|||
<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>
|
||||
<h5 className="modal-title">Purchase Successful</h5>
|
||||
</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>
|
||||
<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 className="modal-body">
|
||||
<p>You have successfully purchased { this.state.assetName } for <span className="virtubrick-tokens">{ this.state.assetPrice }</span>!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -130,7 +309,8 @@ class PurchaseButton extends Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
loaded: false,
|
||||
showModal: false
|
||||
showModal: false,
|
||||
canPurchase: false
|
||||
};
|
||||
|
||||
this.visibleModal = null;
|
||||
|
|
@ -144,7 +324,11 @@ class PurchaseButton extends Component {
|
|||
if(itemElement) {
|
||||
this.setState({
|
||||
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() {
|
||||
return (
|
||||
this.state.loaded
|
||||
?
|
||||
<>
|
||||
<button
|
||||
className={classNames({
|
||||
'px-5': true,
|
||||
'btn': true,
|
||||
'btn-lg': true,
|
||||
'btn-success': this.state.assetOnSale,
|
||||
'btn-secondary': !this.state.assetOnSale
|
||||
'btn-success': this.state.canPurchase,
|
||||
'btn-secondary': !this.state.canPurchase
|
||||
})}
|
||||
disabled={ !(this.state.loaded && this.state.assetOnSale) ? true : null }
|
||||
disabled={ !this.state.canPurchase ? true : null }
|
||||
onClick={ this.showPrompt }
|
||||
>
|
||||
{ (!this.state.loaded || this.state.assetOnSale) ? 'Buy' : 'Offsale' }
|
||||
{
|
||||
this.state.canPurchase
|
||||
?
|
||||
'Buy'
|
||||
:
|
||||
this.state.owned
|
||||
?
|
||||
'Owned'
|
||||
:
|
||||
'Offsale'
|
||||
}
|
||||
</button>
|
||||
|
||||
{ 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')
|
||||
<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>
|
||||
<p>
|
||||
Your account has been {{ $punishment->isDeletion() ? 'closed ' : 'temporarily restricted' }} for violating our Terms of Service.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@
|
|||
|
||||
@section('content')
|
||||
<div class="container mx-auto py-5">
|
||||
@if(isset($status))
|
||||
<div class="alert alert-success text-center">{{ $status }}</div>
|
||||
@endif
|
||||
@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>
|
||||
@endif
|
||||
|
|
@ -44,7 +47,9 @@
|
|||
data-asset-name="{{ $asset->name }}"
|
||||
data-asset-creator="{{ $asset->user->username }}"
|
||||
data-asset-type="{{ $asset->typeString() }}"
|
||||
data-asset-thumbnail-2d="{{ $asset->getThumbnail() }}"
|
||||
data-asset-on-sale="{{ $asset->onSale }}"
|
||||
data-owned="{{ Auth::user()->hasAsset($asset->id) ? '1' : '0' }}"
|
||||
@if ($asset->onSale)
|
||||
data-asset-price="{{ $asset->priceInTokens }}"
|
||||
data-user-currency="{{ Auth::user()->tokens }}"
|
||||
|
|
@ -74,7 +79,17 @@
|
|||
</div>
|
||||
<div class="flex-fill">
|
||||
<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 />
|
||||
{{-- TODO: XlXi: limiteds/trading --}}
|
||||
<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' => 'v1.', 'prefix' => 'v1'], function() {
|
||||
Route::get('/list-json', 'ShopController@listJson')->name('list');
|
||||
Route::post('/purchase/{asset}', 'ShopController@purchase')->name('purchase');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue