Updated JS copyright headers and added asset comments

This commit is contained in:
Graphictoria 2022-08-03 18:16:57 -04:00
parent 8e0a66247d
commit d8e0aaa50f
25 changed files with 665 additions and 137 deletions

View File

@ -0,0 +1,237 @@
<?php
/*
Graphictoria 2022
This file handles communications between the arbiter and the website.
*/
namespace App\Grid;
use Illuminate\Support\Facades\Storage;
use SoapClient;
class SoapService
{
/**
* SoapClient used by the functions in this class.
*
* @var SoapClient
*/
public $Client;
/**
* Constructs the SoapClient.
*
* Arbiter address should be formatted like "http://127.0.0.1:64989"
*
* @param string $arbiterAddr
* @return null
*/
public function __construct($arbiterAddr) {
$this->Client = new SoapClient(
Storage::path('grid/RCCService.wsdl'),
[
'location' => $arbiterAddr,
'uri' => 'http://roblox.com/',
'exceptions' => false
]
);
}
/**
* Calls on the soap service.
*
* @param string $name
* @param array $args
* @return null
*/
public function CallService($name, $args = []) {
$soapResult = $this->Client->{$name}($args);
if(is_soap_fault($soapResult)) {
// TODO: XlXi: log faults
}
return $soapResult;
}
/* Job constructors */
public static function LuaValue($value)
{
switch ($value) {
case is_bool(json_encode($value)) || $value == 1:
return json_encode($value);
default:
return $value;
}
}
public static function CastType($value)
{
$luaTypeConversions = [
'NULL' => 'LUA_TNIL',
'boolean' => 'LUA_TBOOLEAN',
'integer' => 'LUA_TNUMBER',
'double' => 'LUA_TNUMBER',
'string' => 'LUA_TSTRING',
'array' => 'LUA_TTABLE',
'object' => 'LUA_TNIL'
];
return $luaTypeConversions[gettype($value)];
}
public static function ToLuaArguments($luaArguments = [])
{
$luaValue = ['LuaValue' => []];
foreach ($luaArguments as $argument) {
array_push(
$luaValue['LuaValue'],
[
'type' => SoapService::CastType($argument),
'value' => SoapService::LuaValue($argument)
]
);
}
return $luaValue;
}
public static function MakeJobJSON($jobID, $expiration, $category, $cores, $scriptName, $script, $scriptArgs = [])
{
return [
'job' => [
'id' => $jobID,
'expirationInSeconds' => $expiration,
'category' => $category,
'cores' => $cores
],
'script' => [
'name' => $scriptName,
'script' => $script,
'arguments' => SoapService::ToLuaArguments($scriptArgs)
]
];
}
/* Service functions */
public function HelloWorld()
{
return $this->CallService('HelloWorld');
}
public function GetVersion()
{
return $this->CallService('GetVersion');
}
public function GetStatus()
{
return $this->CallService('GetStatus');
}
/* Job specific functions */
public function BatchJobEx($args)
{
return $this->CallService('BatchJobEx', $args);
}
public function OpenJobEx($args)
{
return $this->CallService('OpenJobEx', $args);
}
public function ExecuteEx($args)
{
return $this->CallService('ExecuteEx', $args);
}
public function GetAllJobsEx()
{
return $this->CallService('GetAllJobsEx');
}
public function CloseExpiredJobs()
{
return $this->CallService('CloseExpiredJobs');
}
public function CloseAllJobs()
{
return $this->CallService('CloseAllJobs');
}
/* Job management */
public function DiagEx($jobID, $type)
{
return $this->CallService(
'DiagEx',
[
'type' => $type,
'jobID' => $jobID
]
);
}
public function CloseJob($jobID)
{
return $this->CallService(
'CloseJob',
[
'jobID' => $jobID
]
);
}
public function GetExpiration($jobID)
{
return $this->CallService(
'GetExpiration',
[
'jobID' => $jobID
]
);
}
public function RenewLease($jobID, $expiration)
{
return $this->CallService(
'RenewLease',
[
'jobID' => $jobID,
'expirationInSeconds' => $expiration
]
);
}
/* dep */
public function BatchJob($args)
{
return $this->BatchJobEx($args);
}
public function OpenJob($args)
{
return $this->OpenJobEx($args);
}
public function Execute($args)
{
return $this->ExecuteEx($args);
}
public function GetAllJobs()
{
return $this->GetAllJobsEx();
}
public function Diag($jobID, $type)
{
return $this->DiagEx($jobID, $type);
}
}

View File

@ -13,23 +13,31 @@ use App\Models\DynamicWebConfiguration;
class GridHelper
{
public static function isIpWhitelisted() {
public static function isIpWhitelisted()
{
$ip = request()->ip();
$whitelistedIps = explode(';', DynamicWebConfiguration::where('name', 'WhitelistedIPs')->first()->value);
return in_array($ip, $whitelistedIps);
}
public static function isAccessKeyValid() {
public static function isAccessKeyValid()
{
$accessKey = DynamicWebConfiguration::where('name', 'ComputeServiceAccessKey')->first()->value;
return (request()->header('AccessKey') == $accessKey);
}
public static function hasAllAccess() {
public static function hasAllAccess()
{
if(app()->runningInConsole()) return true;
if(GridHelper::isIpWhitelisted() && GridHelper::isAccessKeyValid()) return true;
return false;
}
public static function createScript($scripts = [], $arguments = [])
{
}
}

View File

@ -11,6 +11,10 @@ use Illuminate\Validation\Validator;
class ValidationHelper
{
public static function generateValidatorError($validator) {
return response(self::generateErrorJSON($validator), 400);
}
public static function generateErrorJSON(Validator $validator) {
$errorModel = [
'errors' => []

View File

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Carbon\Carbon;
use App\Helpers\ValidationHelper;
use App\Http\Controllers\Controller;
use App\Models\Asset;
use App\Models\Comment;
class CommentsController extends Controller
{
protected function listJson(Request $request)
{
$validator = Validator::make($request->all(), [
'assetId' => [
'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();
$comments = Comment::where('asset_id', $valid['assetId'])
->where('deleted', false)
->orderByDesc('id')
->cursorPaginate(15);
$prevCursor = $comments->previousCursor();
$nextCursor = $comments->nextCursor();
$result = [
'data' => [],
'prev_cursor' => ($prevCursor ? $prevCursor->encode() : null),
'next_cursor' => ($nextCursor ? $nextCursor->encode() : null)
];
foreach($comments as $comment) {
// TODO: XlXi: user profile link
// TODO: XlXi: user thumbnail
$poster = [
'name' => $comment->user->username,
'thumbnail' => 'https://www.gtoria.local/images/testing/headshot.png',
'url' => 'https://gtoria.local/todo123'
];
$postDate = $comment['updated_at'];
if(Carbon::now()->greaterThan($postDate->copy()->addDays(2)))
$postDate = $postDate->isoFormat('lll');
else
$postDate = $postDate->calendar();
array_push($result['data'], [
'commentId' => $comment->id,
'poster' => $poster,
'content' => $comment->content,
'time' => $postDate
]);
}
return $result;
}
protected function share(Request $request)
{
$validator = Validator::make($request->all(), [
'assetId' => [
'required',
Rule::exists('App\Models\Asset', 'id')->where(function($query) {
return $query->where('moderated', false);
})
],
'content' => ['required', 'max:200']
]);
if($validator->fails())
return ValidationHelper::generateValidatorError($validator);
$valid = $validator->valid();
$comment = new Comment();
$comment->asset_id = $valid['assetId'];
$comment->author_id = Auth::id();
$comment->content = $valid['content'];
$comment->save();
return response(['success' => true]);
}
}

View File

@ -16,7 +16,7 @@ class FeedController extends Controller
{
// TODO: XlXi: Group shouts.
$postsQuery = Shout::getPosts()
->orderByDesc('created_at')
->orderByDesc('id')
->cursorPaginate(15);
/* */
@ -38,10 +38,12 @@ class FeedController extends Controller
if($post['poster_type'] == 'user') {
$user = User::where('id', $post['poster_id'])->first();
// TODO: XlXi: user profile link
$poster = [
'type' => 'User',
'name' => $user->username,
'thumbnail' => 'https://www.gtoria.local/images/testing/headshot.png'
'thumbnail' => 'https://www.gtoria.local/images/testing/headshot.png',
'url' => 'https://gtoria.local/todo123'
];
}
@ -49,7 +51,7 @@ class FeedController extends Controller
$postDate = $post['updated_at'];
if(Carbon::now()->greaterThan($postDate->copy()->addDays(2)))
$postDate = $postDate->isoFormat('LLLL');
$postDate = $postDate->isoFormat('lll');
else
$postDate = $postDate->calendar();

View File

@ -23,10 +23,6 @@ class ShopController extends Controller
'32' // Packages
];
protected static function generateValidatorError($validator) {
return response(ValidationHelper::generateErrorJSON($validator), 400);
}
protected static function getAssets($assetTypeIds, $gearGenre=null)
{
// TODO: XlXi: Group owned assets
@ -49,7 +45,7 @@ class ShopController extends Controller
]);
if($validator->fails()) {
return ShopController::generateValidatorError($validator);
return ValidationHelper::generateValidatorError($validator);
}
$valid = $validator->valid();
@ -57,13 +53,13 @@ class ShopController extends Controller
foreach(explode(',', $valid['assetTypeId']) as $assetTypeId) {
if(!in_array($assetTypeId, $this->validAssetTypeIds)) {
$validator->errors()->add('assetTypeId', 'Invalid assetTypeId supplied.');
return ShopController::generateValidatorError($validator);
return ValidationHelper::generateValidatorError($validator);
}
}
if($valid['assetTypeId'] != '19' && isset($valid['gearGenreId'])) {
$validator->errors()->add('gearGenreId', 'gearGenreId can only be used with assetTypeId 19.');
return ShopController::generateValidatorError($validator);
return ValidationHelper::generateValidatorError($validator);
}
/* */

View File

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
class Comment extends Model
{
use HasFactory;
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class, 'author_id');
}
}

View File

@ -0,0 +1,35 @@
<?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('comments', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('author_id');
$table->longText('asset_id');
$table->longText('content');
$table->boolean('deleted')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
};

View File

@ -1,15 +1,7 @@
/**
* First we will load all of this project's JavaScript dependencies which
* includes React and other helpers. It's a great starting point while
* building robust, powerful web applications using React + Laravel.
*/
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
require('./bootstrap');
/**
* Next, we will create a fresh React component instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
require('./components/Main');

View File

@ -1,18 +1,6 @@
window._ = require('lodash');
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// forceTLS: true
// });
window._ = require('lodash');

View File

@ -1,29 +1,185 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import { Component } from 'react';
import { Component, createRef } from 'react';
import axios from 'axios';
import Twemoji from 'react-twemoji';
import { buildGenericApiUrl } from '../util/HTTP.js';
import Loader from './Loader';
const commentsId = 'gt-comments'; // XlXi: Keep this in sync with the Item page.
axios.defaults.withCredentials = true;
class Comments extends Component {
constructor(props) {
super(props);
this.state = {
loaded: false,
loadingCursor: false,
disabled: false,
error: '',
comments: [],
mouseHover: -1
};
this.inputBox = createRef();
this.loadComments = this.loadComments.bind(this);
this.loadMore = this.loadMore.bind(this);
this.postComment = this.postComment.bind(this);
}
componentWillMount() {
window.addEventListener('scroll', this.loadMore);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.loadMore);
}
componentDidMount() {
let commentsElement = document.getElementById(commentsId);
if (commentsElement) {
this.assetId = commentsElement.getAttribute('data-asset-id')
this.setState({
canComment: (commentsElement.getAttribute('data-can-comment') === '1')
});
}
this.loadComments();
}
loadComments() {
axios.get(buildGenericApiUrl('api', `comments/v1/list-json?assetId=${this.assetId}`))
.then(res => {
const comments = res.data;
this.nextCursor = comments.next_cursor;
this.setState({ comments: comments.data, loaded: true });
});
}
// XlXi: https://stackoverflow.com/questions/57778950/how-to-load-more-search-results-when-scrolling-down-the-page-in-react-js
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.loadingCursor) {
this.setState({ loadingCursor: true });
axios.get(buildGenericApiUrl('api', `comments/v1/list-json?assetId=${this.assetId}&cursor=${this.nextCursor}`))
.then(res => {
const comments = res.data;
this.nextCursor = comments.next_cursor;
this.setState({ comments: this.state.comments.concat(comments.data), loadingCursor: false });
});
}
}
}
postComment() {
this.setState({ disabled: true });
const postText = this.inputBox.current.value;
if (postText == '') {
this.setState({ disabled: false, error: 'Your comment cannot be blank.' });
this.inputBox.current.focus();
return;
}
axios.post(buildGenericApiUrl('api', `comments/v1/share`), { assetId: this.assetId, content: postText })
.then(res => {
this.inputBox.current.value = '';
this.setState({ loaded: false, loadingCursor: false, disabled: false });
this.loadComments();
})
.catch(err => {
const data = err.response.data;
this.setState({ disabled: false, error: data.message });
this.inputBox.current.focus();
});
}
render() {
return (
<>
<h4 className="pt-3">Comments</h4>
{ /* TODO: XlXi: Hide comment input when logged out*/ }
<div className="card mb-2">
<div className="input-group p-2">
<input disabled="disabled" type="text" className="form-control" placeholder="Write a comment!" />
<button disabled="disabled" type="submit" className="btn btn-secondary">Share</button>
{
this.state.canComment != false
?
<div className="card mb-2">
{
this.state.error != ''
?
<div className="alert alert-danger graphictoria-alert graphictoria-error-popup m-2 mb-0">{ this.state.error }</div>
:
null
}
<div className="input-group p-2">
<input disabled={ (!this.state.loaded || this.state.disabled) } type="text" className="form-control" placeholder="Write a comment!" ref={ this.inputBox }/>
<button disabled={ (!this.state.loaded || this.state.disabled) } type="submit" className="btn btn-secondary" onClick={ this.postComment }>Post</button>
</div>
</div>
</div>
<div className="d-flex">
<Loader />
</div>
:
null
}
{
this.state.loaded
?
(
this.state.comments.length > 0
?
<>
<div className="card">
{
this.state.comments.map(({ commentId, poster, time, content }, index) =>
<>
<div className="d-flex p-2" onMouseEnter={ () => this.setState({ mouseHover: index }) } onMouseLeave={ () => this.setState({ mouseHover: -1 }) }>
<div className="me-2">
<a href={ poster.url }>
<img src={ poster.thumbnail } alt={ poster.name } width="50" height="50" className="border graphictora-feed-user-circle" />
</a>
</div>
<div className="flex-fill">
<div className="d-flex">
<a href={ poster.url } className="text-decoration-none me-auto fw-bold">{ poster.name }{ poster.icon ? <>&nbsp;<i className={ poster.icon }></i></> : null }</a>
{ this.state.mouseHover == index ? <a href={ buildGenericApiUrl('www', `report/comment/${commentId}`) } target="_blank" className="text-decoration-none link-danger me-2">Report <i className="fa-solid fa-circle-exclamation"></i></a> : null }
<p className="text-muted">{ time }</p>
</div>
<Twemoji options={{ className: 'twemoji', base: '/images/twemoji/', folder: 'svg', ext: '.svg' }}>
<p>{ content }</p>
</Twemoji>
</div>
</div>
{ this.state.comments.length != (index+1) ? <hr className="m-0" /> : null }
</>
)
}
</div>
{
this.state.loadingCursor ?
<div className="d-flex mt-2">
<Loader />
</div>
:
null
}
</>
:
<div className="text-center mt-3">
<p className="text-muted">No comments were found. { this.state.canComment ? 'You could be the first!' : null }</p>
</div>
)
:
<div className="d-flex">
<Loader />
</div>
}
</>
);
}

View File

@ -1,10 +1,10 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import { Component, createRef } from 'react';
import axios from 'axios';
import Twemoji from 'react-twemoji';
import { buildGenericApiUrl } from '../util/HTTP.js';
@ -124,7 +124,7 @@ class Feed extends Component {
<>
<div className="d-flex p-2" onMouseEnter={ () => this.setState({ mouseHover: index }) } onMouseLeave={ () => this.setState({ mouseHover: -1 }) }>
<div className="me-2">
<a href={ buildGenericApiUrl('www', (poster.type == 'User' ? `users/${poster.name}/profile` : `groups/${poster.id}`)) }>
<a href={ poster.url }>
{ poster.type == 'User' ?
<img src={ poster.thumbnail } alt={ poster.name } width="50" height="50" className="border graphictora-feed-user-circle" /> :
<img src={ poster.thumbnail } alt={ poster.name } width="50" height="50" className="img-fluid" />
@ -133,8 +133,8 @@ class Feed extends Component {
</div>
<div className="flex-fill">
<div className="d-flex">
<a href={ buildGenericApiUrl('www', (poster.type == 'User' ? `users/${poster.name}/profile` : `groups/${poster.id}`)) } className="text-decoration-none fw-bold me-auto">{ poster.name }{ poster.icon ? <>&nbsp;<i className={ poster.icon }></i></> : null }</a>
{ this.state.mouseHover == index ? <a href={ buildGenericApiUrl('www', `report/user-wall/${postId}`) } target="_blank" className="text-decoration-none link-danger me-2">Report <i className="fa-solid fa-circle-exclamation"></i></a> : null }
<a href={ poster.url } className="text-decoration-none fw-bold me-auto">{ poster.name }{ poster.icon ? <>&nbsp;<i className={ poster.icon }></i></> : null }</a>
{ this.state.mouseHover == index ? <a href={ buildGenericApiUrl('www', `report/wall/${postId}`) } target="_blank" className="text-decoration-none link-danger me-2">Report <i className="fa-solid fa-circle-exclamation"></i></a> : null }
<p className="text-muted">{ time }</p>
</div>
<Twemoji options={{ className: 'twemoji', base: '/images/twemoji/', folder: 'svg', ext: '.svg' }}>

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
const Loader = () => {
return (

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import $ from 'jquery';
import * as Bootstrap from 'bootstrap';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import { createRef, Component } from 'react';
import ReactDOM from 'react-dom';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import { useState, useRef, useEffect } from 'react';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import { Component, createRef } from 'react';
@ -239,7 +241,7 @@ class Shop extends Component {
navigateCategory(categoryId, data) {
this.setState({selectedCategoryId: categoryId, pageLoaded: false});
let url = buildGenericApiUrl('api', 'catalog/v1/list-json');
let url = buildGenericApiUrl('api', 'shop/v1/list-json');
let paramIterator = 0;
Object.keys(data).filter(key => {
if (key == 'label')

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import $ from 'jquery';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import $ from 'jquery';
@ -10,7 +12,7 @@ import Comments from '../components/Comments';
import PurchaseButton from '../components/PurchaseButton';
const purchaseId = 'gt-purchase-button';
const commentsId = 'gt-comments';
const commentsId = 'gt-comments'; // XlXi: Keep this in sync with the Comments component.
$(document).ready(function() {
if (document.getElementById(commentsId)) {

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import axios from 'axios';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
import $ from 'jquery';

View File

@ -1,5 +1,7 @@
// © XlXi 2022
// Graphictoria 5
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
const urlObject = new URL(document.location.href);

View File

@ -7,47 +7,6 @@
@endsection
@section('content')
{{-- XlXi: MOVE THESE TO JS --}}
@if(false)
<div class="modal fade show" id="purchase-modal" aria-hidden="true" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content text-center">
<div class="modal-header">
<h5 class="modal-title">Purchase Item</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-flex flex-column">
<p>Would you like to purchase the {{ $asset->typeString() }} "<strong>{{ $asset->name }}</strong>" from {{ $asset->user->username }} for <strong style="color:#e59800!important;font-weight:bold"><img src="{{ asset('images/symbols/token.svg') }}" height="16" width="16" class="img-fluid" style="margin-top:-1px" />{{ number_format($asset->priceInTokens) }}</strong>?</p>
<img src={{ asset('images/testing/hat.png') }} width="240" height="240" alt="{{ $asset->name }}" class="mx-auto my-2 img-fluid" />
</div>
<div class="modal-footer flex-column">
<div class="mx-auto">
<button class="btn btn-success" data-bs-dismiss="modal">Purchase</button>
<button class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
<p class="text-muted pt-1">You will have <strong style="color:#e59800!important;font-weight:bold"><img src="{{ asset('images/symbols/token.svg') }}" height="16" width="16" class="img-fluid" style="margin-top:-1px" />{{ max(0, number_format(Auth::user()->tokens - $asset->priceInTokens)) }}</strong> after this purchase.</p>
</div>
</div>
</div>
</div>
<div class="modal fade show" id="purchase-modal" aria-hidden="true" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content text-center">
<div class="modal-header">
<h5 class="modal-title">Insufficient Funds</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>You don't have enough tokens to buy this item.</p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" data-bs-dismiss="modal">Ok</button>
</div>
</div>
</div>
</div>
@endif
<div class="container mx-auto py-5">
@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>
@ -119,7 +78,10 @@
</div>
</div>
</div>
<div id="gt-comments"></div>
<div id="gt-comments"
data-can-comment="{{ intval(Auth::check()) }}"
data-asset-id="{{ $asset->id }}"
></div>
</div>
</div>
@endsection

View File

@ -11,7 +11,14 @@ Route::middleware('auth')->group(function () {
});
});
Route::group(['as' => 'catalog.', 'prefix' => 'catalog'], function() {
Route::group(['as' => 'comments.', 'prefix' => 'comments'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/list-json', 'CommentsController@listJson')->name('list');
Route::post('/share', 'CommentsController@share')->name('share')->middleware(['auth', 'throttle:3,2']);
});
});
Route::group(['as' => 'shop.', 'prefix' => 'shop'], function() {
Route::group(['as' => 'v1.', 'prefix' => 'v1'], function() {
Route::get('/list-json', 'ShopController@listJson')->name('list');
});

View File

@ -1,17 +1,11 @@
/*
Graphictoria 5 (https://gtoria.net)
Copyright © XlXi 2022
*/
const mix = require('laravel-mix');
require('laravel-mix-banner');
/*
|--------------------------------------------------------------------------
| Mix Asset Management
|--------------------------------------------------------------------------
|
| Mix provides a clean, fluent API for defining some Webpack build steps
| for your Laravel application. By default, we are compiling the Sass
| file for the application as well as bundling up all the JS files.
|
*/
mix.js('resources/js/app.js', 'public/js')
.js('resources/js/pages/Maintenance.js', 'public/js')
.js('resources/js/pages/Dashboard.js', 'public/js')