@@ -201,7 +201,7 @@ class BillingController extends Controller | |||
$transaction = Transaction::find($transaction_id); | |||
if ($transaction->completed) { | |||
return; | |||
abort(422, 'Bad transaction ID'); | |||
} | |||
$user = $transaction->user; | |||
@@ -0,0 +1,35 @@ | |||
<?php | |||
namespace App\Http\Controllers; | |||
use Illuminate\Http\Request; | |||
use Mail; | |||
use App\Models\Ticket; | |||
class Ticket extends Controller | |||
{ | |||
public function send(Request $request){ | |||
$validated = $request->validate([ | |||
'topic' => 'required', | |||
'message' = 'required' | |||
]); | |||
$ticket = $this->create($request->topic); | |||
Mail::to('donotreply@trendplays.com')->send(new | |||
SupportTicket($ticket, $request->message)); | |||
} | |||
//Should probably have a minimum character restriction later | |||
public function create(String $type){ | |||
$ticket = new Ticket; | |||
$ticket->user_id = Auth::user()->id; | |||
$ticket->type = $type; | |||
$ticket->status = 'processing'; | |||
$ticket->complete = false; | |||
$ticket->save(); | |||
return $ticket; | |||
} | |||
} |
@@ -7,12 +7,14 @@ use App\Models\User; | |||
use App\Models\Order; | |||
use App\Models\Service; | |||
use App\Notifications\ChangeEmail; | |||
use Illuminate\Support\Facades\Log; | |||
use Illuminate\Support\Facades\Hash; | |||
use Illuminate\Support\Facades\URL; | |||
use Illuminate\Auth\Events\Registered; | |||
use Illuminate\Support\Facades\Password; | |||
use Illuminate\Support\Facades\Auth; | |||
use Stripe\Stripe; | |||
use Stripe\Customer; | |||
@@ -22,20 +24,16 @@ class UserController extends Controller | |||
$validated = $request->validate([ | |||
'name' => 'required|max:30', | |||
'email' => 'required|email|unique:users|max:255', | |||
'password' => 'required|confirmed|min:8|regex:/[a-z]/|regex:/[A-Z]/|regex:/[0-9]/' | |||
'password' => 'required|confirmed | |||
|min:8|regex:/[a-z]/|regex:/[A-Z]/|regex:/[0-9]/' | |||
]); | |||
Stripe::setApiKey(config('services.stripe.secret')); | |||
$user = new User; | |||
$user->name = $request->name; | |||
$user->email = $request->email; | |||
$user->role = "client"; | |||
$user->active = true; | |||
$user->password = Hash::make($request->password); | |||
$user->customer_id = Customer::create(['email' => | |||
$request->email, 'name' => $request->name, 'metadata' => ['user_id' | |||
=> $user->id]])->id; | |||
$user->save(); | |||
event(new Registered($user)); | |||
@@ -0,0 +1,47 @@ | |||
<?php | |||
namespace App\Mail; | |||
use Illuminate\Bus\Queueable; | |||
use Illuminate\Contracts\Queue\ShouldQueue; | |||
use Illuminate\Mail\Mailable; | |||
use Illuminate\Queue\SerializesModels; | |||
use App\Models\Ticket; | |||
class SupportTicket extends Mailable | |||
{ | |||
use Queueable, SerializesModels; | |||
/** | |||
* Create a new message instance. | |||
* | |||
* @return void | |||
*/ | |||
public $name; | |||
public $email; | |||
public $message; | |||
public $type; | |||
public $id; | |||
public function __construct(Ticket $ticket, String $message) | |||
{ | |||
$this->name = $ticket->user->name; | |||
$this->email = $ticket->user->email; | |||
$this->message = $message; | |||
$this->type = $ticket->type; | |||
$this->id = $ticket->id; | |||
} | |||
/** | |||
* Build the message. | |||
* | |||
* @return $this | |||
*/ | |||
public function build() | |||
{ | |||
return $this->view('support-ticket') | |||
->from('donotreply@trendplays.com') | |||
->subject("Ticket: $this->id, $this->type"); | |||
} | |||
} |
@@ -0,0 +1,16 @@ | |||
<?php | |||
namespace App\Models; | |||
use Illuminate\Database\Eloquent\Factories\HasFactory; | |||
use Illuminate\Database\Eloquent\Model; | |||
use App\Models\User; | |||
class Ticket extends Model | |||
{ | |||
use HasFactory; | |||
public function user() { | |||
return $this->belongsTo(User::class); | |||
} | |||
} |
@@ -8,6 +8,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable; | |||
use Illuminate\Notifications\Notifiable; | |||
use App\Models\Order; | |||
use App\Models\Transaction; | |||
use App\Models\Ticket; | |||
use Laravel\Cashier\Billable; | |||
class User extends Authenticatable implements MustVerifyEmail | |||
@@ -52,4 +53,8 @@ class User extends Authenticatable implements MustVerifyEmail | |||
return $this->hasMany(Transaction::class); | |||
} | |||
public function tickets() { | |||
return $this->hasMany(Ticket::class); | |||
} | |||
} |
@@ -23,7 +23,8 @@ class CreateOrdersTable extends Migration | |||
$table->string('note')->default(''); | |||
$table->string('message')->nullable(); | |||
$table->bigInteger('remaining')->default(0); | |||
$table->enum('status', ['processing', 'pending', 'canceled', 'refunded', 'completed', 'error']); | |||
$table->enum('status', ['processing', 'pending', 'canceled', | |||
'refunded', 'completed', 'error']); | |||
$table->string('url'); | |||
}); | |||
} | |||
@@ -0,0 +1,38 @@ | |||
<?php | |||
use Illuminate\Database\Migrations\Migration; | |||
use Illuminate\Database\Schema\Blueprint; | |||
use Illuminate\Support\Facades\Schema; | |||
class CreateTicketsTable extends Migration | |||
{ | |||
/** | |||
* Run the migrations. | |||
* | |||
* @return void | |||
*/ | |||
public function up() | |||
{ | |||
Schema::create('tickets', function (Blueprint $table) { | |||
$table->id(); | |||
$table->foreignId('user_id')->constrained(); | |||
$table->enum('type', ['order', 'service', 'credits', | |||
'payment', 'other']); | |||
$table->enum('status', ['processing', 'pending', 'abandoned', | |||
'refunded', 'completed', 'error'])->nullable(); | |||
$table->boolean('complete'); | |||
$table->string('note')->nullable(); | |||
$table->timestamps(); | |||
}); | |||
} | |||
/** | |||
* Reverse the migrations. | |||
* | |||
* @return void | |||
*/ | |||
public function down() | |||
{ | |||
Schema::dropIfExists('tickets'); | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-chat-text-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill-rule="evenodd" d="M16 8c0 3.866-3.582 7-8 7a9.06 9.06 0 0 1-2.347-.306c-.584.296-1.925.864-4.181 1.234-.2.032-.352-.176-.273-.362.354-.836.674-1.95.77-2.966C.744 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7zM4.5 5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zm0 2.5a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1h-7zm0 2.5a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1h-4z"/> | |||
</svg> |
@@ -0,0 +1,3 @@ | |||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-life-preserver" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill-rule="evenodd" d="M14.43 10.772l-2.788-1.115a4.015 4.015 0 0 1-1.985 1.985l1.115 2.788a7.025 7.025 0 0 0 3.658-3.658zM5.228 14.43l1.115-2.788a4.015 4.015 0 0 1-1.985-1.985L1.57 10.772a7.025 7.025 0 0 0 3.658 3.658zm9.202-9.202a7.025 7.025 0 0 0-3.658-3.658L9.657 4.358a4.015 4.015 0 0 1 1.985 1.985l2.788-1.115zm-8.087-.87L5.228 1.57A7.025 7.025 0 0 0 1.57 5.228l2.788 1.115a4.015 4.015 0 0 1 1.985-1.985zM8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm0-5a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/> | |||
</svg> |
@@ -1,7 +1,8 @@ | |||
<template> | |||
<div> | |||
<section class="pending-pane"> | |||
<div class="actions"><a class="new-order" href="#new-order">New</a><a class="new-order" href="#credits">Add Credits</a></div> | |||
<div class="actions"><a class="new-order" href="#new-order">New</a><a | |||
class="new-order" href="#credits">Add Credits</a></div> | |||
<h4>Pending Orders</h4> | |||
<ul> | |||
<template v-bind:key='order.id' v-for="order in orders"> | |||
@@ -11,19 +12,23 @@ | |||
<img class="chevron" src="../../images/chevron-down.svg" alt=""> | |||
</div> | |||
<div class="pending-content"> | |||
<p>ID: {{order.id}}<br>URL: {{order.url}}<br>Quantity: {{order.quantity}}<br>Note: {{order.note}}</p> | |||
<p>ID: {{order.id}}<br>URL: {{order.url}}<br>Quantity: | |||
{{order.quantity}}<br>Note: {{order.note}}</p> | |||
</div> | |||
</div> | |||
</template> | |||
</ul> | |||
</section> | |||
<div class="info-grey"><p>Orders are typically completed within 1-5 days.</p><div></div></div> | |||
<div class="info-grey"><p>Orders are typically completed within 1-5 | |||
days.</p><div></div></div> | |||
<section class="history-pane"> | |||
<h4>Order History</h4> | |||
<table> | |||
<thead><th>Date</th><th>ID</th><th>Name</th><th>Status</th> <th>Quantity</th></thead> | |||
<thead><th>Date</th><th>ID</th><th>Name</th><th>Status</th> | |||
<th>Quantity</th></thead> | |||
<tbody> | |||
<tr v-bind:key='order.id' v-for='order in orders.slice(historyPage*10-10, historyPage*10)'> | |||
<tr v-bind:key='order.id' v-for='order in | |||
orders.slice(historyPage*10-10, historyPage*10)'> | |||
<td>{{order.updated_at}}</td> | |||
<td>{{order.id}}</td> | |||
<td>{{order.service.name.length > 20 ? order.service.name.substring(0, | |||
@@ -35,9 +40,11 @@ | |||
</tr> | |||
</tbody> | |||
</table> | |||
<img @click="moveHistory(false)" class="nav-btn left" src="../../images/arrow-left-circle-fill.svg" alt=""/> | |||
<img @click="moveHistory(false)" class="nav-btn left" | |||
src="../../images/arrow-left-circle-fill.svg" alt=""/> | |||
<p class="nav-legend">{{historyPage}}/{{Math.ceil(orders.length/10)}}</p> | |||
<img @click="moveHistory(true)" class="nav-btn right" src="../../images/arrow-right-circle-fill.svg" alt=""/> | |||
<img @click="moveHistory(true)" class="nav-btn right" | |||
src="../../images/arrow-right-circle-fill.svg" alt=""/> | |||
</section> | |||
</div> | |||
</template> | |||
@@ -6,7 +6,9 @@ | |||
<section class="welcome-pane"><h3>Welcome, {{user.name}}!</h3></section> | |||
<section class="credits-pane"><img src="../../images/coin-stack.svg" alt="wallet" class="icon"/><p>Credits: {{(user.credits/100).toLocaleString('en')}}</p></section> | |||
<section class="alerts-pane"><h4>News and Announcements</h4> | |||
<p>We've just launched. Thanks for joining us.</p> | |||
<p>We've just launched. Thanks for joining us! Some features are still | |||
being tested. If you experience a delay in credits being added to your | |||
account, please wait 24 hours before contacting support@trendplays.com.</p> | |||
</section> | |||
<section class="recent-pane"><h4>Recent Activity</h4> | |||
<table> | |||
@@ -26,11 +28,14 @@ | |||
</table> | |||
</section> | |||
</div> | |||
<PastOrders :orders="orders" v-else-if="active === '#orders'" id="main"> | |||
</PastOrders> | |||
<NewOrder :preferred="user.payment_method" :token="token" :active="active" :credits="user.credits" v-else-if="active === | |||
<past-orders :orders="orders" v-else-if="active === '#orders'" id="main"> | |||
</past-orders> | |||
<new-order :preferred="user.payment_method" :token="token" :active="active" :credits="user.credits" v-else-if="active === | |||
'#new-order' || active === '#credits'" id="main" @update-user='getUser' @update-orders='getOrders'> | |||
</NewOrder> | |||
</new-order> | |||
<div id="main" v-else-if="active === '#exit'"> | |||
<section class="logout-pane"> | |||
<h3>Are you sure you want to logout?</h3> | |||
@@ -47,6 +52,8 @@ v-else-if="active === '#settings'"> | |||
'#transaction-complete' || active == '#transaction-failed'"> | |||
</transaction-end> | |||
<support v-else-if="active == '#support'" :user="user" :token="token"></support> | |||
</transition> | |||
</template> | |||
</template> | |||
@@ -57,6 +64,7 @@ import Settings from './settings.vue' | |||
import PastOrders from './orders.vue' | |||
import NewOrder from './services.vue' | |||
import TransactionEnd from './transaction-endpoint.vue' | |||
import Support from './support.vue' | |||
function getServices() { | |||
return fetch("/panel/services", { | |||
@@ -98,7 +106,7 @@ function getOrders() { | |||
export default { | |||
components: { | |||
Sidebar, Settings, PastOrders, NewOrder, | |||
TransactionEnd | |||
TransactionEnd, Support | |||
}, | |||
data() { | |||
return {active: window.location.hash, user: null, | |||
@@ -1,6 +1,6 @@ | |||
<template> | |||
<div> | |||
<h3>Settings</h3> | |||
<h2>Settings</h2> | |||
<section class="change-name-pane"> | |||
<h4>Name</h4> | |||
<input :value="user.name" name="name" id="changed_name" type="text"> | |||
@@ -8,14 +8,23 @@ | |||
<a :class="{selected: active == '#orders'}" href="/panel#orders"> | |||
<svg fill="currentColor" enable-background="new 0 0 24 24" height="512" viewBox="0 0 24 24" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m16.12 1.929-10.891 5.576-4.329-2.13 10.699-5.283c.24-.122.528-.122.78 0z"/><path d="m23.088 5.375-11.082 5.49-4.15-2.045-.6-.305 10.903-5.575.6.304z"/><path d="m11.118 12.447-.012 11.553-10.614-5.539c-.3-.158-.492-.475-.492-.816v-10.688l4.498 2.216v3.896c0 .499.408.913.9.913s.9-.414.9-.913v-2.995l.6.292z"/><path d="m23.988 6.969-11.07 5.466-.012 11.553 11.094-5.793z"/></svg> | |||
</a> | |||
<a :class="{selected: active == '#new-order' || active == '#credits'}" href="/panel#new-order"> | |||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12,2C6.486,2,2,6.486,2,12s4.486,10,10,10c5.514,0,10-4.486,10-10S17.514,2,12,2z M17,13h-4v4h-2v-4H7v-2h4V7h2v4h4V13z"></path></svg> | |||
</a> | |||
<a :class="{selected: active == '#settings'}" href="/panel#settings"> | |||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-gear-fill" viewBox="0 0 16 16"> | |||
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/> | |||
</svg> | |||
</a> | |||
<a :class="{selected: active == '#support'}" href="/panel#support"> | |||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-life-preserver" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill-rule="evenodd" d="M14.43 10.772l-2.788-1.115a4.015 4.015 0 0 1-1.985 1.985l1.115 2.788a7.025 7.025 0 0 0 3.658-3.658zM5.228 14.43l1.115-2.788a4.015 4.015 0 0 1-1.985-1.985L1.57 10.772a7.025 7.025 0 0 0 3.658 3.658zm9.202-9.202a7.025 7.025 0 0 0-3.658-3.658L9.657 4.358a4.015 4.015 0 0 1 1.985 1.985l2.788-1.115zm-8.087-.87L5.228 1.57A7.025 7.025 0 0 0 1.57 5.228l2.788 1.115a4.015 4.015 0 0 1 1.985-1.985zM8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm0-5a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/> | |||
</svg> | |||
</a> | |||
<a v-if="role == 'admin'" :class="{selected: active == '#admin'}" href="/telescope"> | |||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-key-fill" viewBox="0 0 16 16"> | |||
<path d="M3.5 11.5a3.5 3.5 0 1 1 3.163-5H14L15.5 8 14 9.5l-1-1-1 1-1-1-1 1-1-1-1 1H6.663a3.5 3.5 0 0 1-3.163 2zM2.5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/> | |||
@@ -0,0 +1,77 @@ | |||
<template> | |||
<div class="support-section" id="main"> | |||
<h2>Support</h2> | |||
<loading v-if="loading"></loading> | |||
<div v-if="!loading && !complete" id="support-form"> | |||
<label for="">Topic</label> | |||
<select id="support-topic" name="" v-model="topic"> | |||
<option value="order">Order</option> | |||
<option value="service">Service</option> | |||
<option value="credits">Credits</option> | |||
<option value="payment">Payment</option> | |||
<option value="other">Other</option> | |||
</select> | |||
<label for="">Details</label> | |||
<textarea id="" name="" cols="30" rows="10" v-model="message"></textarea> | |||
<span class="note-grey">Include any relevant information like the order number, | |||
service name, etc</span> | |||
<button @click="send">Submit</button> | |||
<p class="error-message">{{errorMessage}}</p> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import Loading from '../icons/loading.vue' | |||
function send() { | |||
this.errorMessage = '' | |||
if (!this.topic || !this.message) { | |||
this.errorMessage = 'Topic and details cannot be blank.' | |||
return | |||
} | |||
this.loading = true | |||
fetch("/panel/support", { | |||
method: 'POST', | |||
headers: {'Content-Type': 'application/json', | |||
'Accept': 'application/json', | |||
'X-XSRF-TOKEN': this.token}, | |||
body: JSON.stringify({'topic': this.topic, 'message': this.message})}). | |||
then(response => { | |||
if (response.ok) { | |||
this.complete = true | |||
} else { | |||
this.complete = false | |||
this.error = true | |||
this.errorMessage = `${response.status}: | |||
${response.statusText}` | |||
} | |||
this.loading = false | |||
}) | |||
} | |||
export default { | |||
components: {Loading}, | |||
props: ['user', 'token'], | |||
data() { | |||
return {loading: false, complete: false, error: false, errorMessage: | |||
'', topic: null, message: ''} | |||
}, | |||
methods: {send} | |||
} | |||
</script> |
@@ -56,6 +56,12 @@ input, select { | |||
margin: 10px; | |||
} | |||
.note-grey { | |||
color: grey; | |||
font-size: 0.8em; | |||
max-width: 70%; | |||
} | |||
.services-cards li { | |||
padding-bottom: 8px; | |||
&:before { | |||
@@ -757,10 +763,6 @@ main.panel { | |||
color: vars.getColor("dark-grey"); | |||
} | |||
p { | |||
margin: 0; | |||
} | |||
.welcome-pane { | |||
text-align: center; | |||
} | |||
@@ -1372,3 +1374,53 @@ main.terms { | |||
display: inline-block; | |||
} | |||
} | |||
.support-section .loading-icon { | |||
margin-top: 25%; | |||
color: vars.getColor('brand-orange'); | |||
} | |||
#support-form { | |||
margin-top: 10%; | |||
label { | |||
margin: auto; | |||
text-align: center; | |||
width: 10em; | |||
display: block; | |||
margin-bottom: 1em; | |||
} | |||
select { | |||
display: block; | |||
margin: auto; | |||
margin-bottom: 2em; | |||
width: 10em; | |||
text-align: center; | |||
background: white; | |||
} | |||
textarea { | |||
display: block; | |||
width: 90%; | |||
margin: auto; | |||
} | |||
button { | |||
display: block; | |||
font-size: 1em; | |||
margin: auto; | |||
margin-top: 2em; | |||
height: 2.3em; | |||
width: 5em; | |||
@include vars.inverting-button(black, white); | |||
} | |||
} | |||
.error-message { | |||
text-align: center; | |||
color: vars.getColor('red-alert'); | |||
margin-top: 1em; | |||
margin-bottom: 1em; | |||
} | |||
@@ -56,9 +56,8 @@ | |||
<h3>Youtube</h3> | |||
<ul> | |||
<li>Views often increase within 24 hours</li> | |||
<li>Real people</li> | |||
<li>Targeted by language</li> | |||
<li>Targeted by country</li> | |||
<li>Boost to rankings</li> | |||
<li>High quality users</li> | |||
</ul> | |||
<button>Select</button> | |||
</div> | |||
@@ -90,7 +89,7 @@ | |||
<img src="/img/tik-tok.svg" alt="twitter-icon"> | |||
<h3>TikTok</h3> | |||
<ul> | |||
<li>Real people</li> | |||
<li>Active user accounts</li> | |||
<li>Views</li> | |||
<li>Likes</li> | |||
<li>Followers</li> | |||
@@ -194,7 +193,7 @@ | |||
<div class="collapsible"><button>Is buying views illegal?</button> | |||
<div class="content"><p> | |||
No, and there is no reason why it should be. | |||
No, and there is no good reason why it should be. | |||
</p></div></div> | |||
<div class="collapsible"><button>How can I pay?</button> | |||
@@ -0,0 +1,24 @@ | |||
@extends('master') | |||
@section('title', 'Ticket') | |||
@section('head-metas') | |||
@parent | |||
@endsection | |||
@section('content') | |||
@parent | |||
<main id="ticket-email"> | |||
<h3>Name: {{$name}}</h3> | |||
<h3>Email: {{$email}}</h3> | |||
<h3>Message:</h3> | |||
<p>{{$message}}</p> | |||
</main> | |||
@endsection | |||
@section('footer') | |||
@endsection |
@@ -145,3 +145,5 @@ Route::post('/hooks/pm-transaction', | |||
Route::get('/panel/clear-paying', | |||
[UserController::class, 'clearPaying']); | |||
Route::post('/panel/support', | |||
[Ticket::class, 'send']); |