Ver código fonte

Create support section

master
Immanuel Onyeka 3 anos atrás
pai
commit
42d00f60b9
19 arquivos alterados com 355 adições e 31 exclusões
  1. +1
    -1
      app/Http/Controllers/BillingController.php
  2. +35
    -0
      app/Http/Controllers/Ticket.php
  3. +4
    -6
      app/Http/Controllers/UserController.php
  4. +47
    -0
      app/Mail/SupportTicket.php
  5. +16
    -0
      app/Models/Ticket.php
  6. +5
    -0
      app/Models/User.php
  7. +2
    -1
      database/migrations/2021_05_19_185302_create_orders_table.php
  8. +38
    -0
      database/migrations/2021_06_22_153337_create_tickets_table.php
  9. +3
    -0
      resources/images/chat-text-fill.svg
  10. +3
    -0
      resources/images/life-preserver.svg
  11. +14
    -7
      resources/js/panel/orders.vue
  12. +14
    -6
      resources/js/panel/panel.vue
  13. +1
    -1
      resources/js/panel/settings.vue
  14. +9
    -0
      resources/js/panel/sidebar.vue
  15. +77
    -0
      resources/js/panel/support.vue
  16. +56
    -4
      resources/scss/main.scss
  17. +4
    -5
      resources/views/home.blade.php
  18. +24
    -0
      resources/views/support-ticket.blade.php
  19. +2
    -0
      routes/web.php

+ 1
- 1
app/Http/Controllers/BillingController.php Ver arquivo

@@ -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;


+ 35
- 0
app/Http/Controllers/Ticket.php Ver arquivo

@@ -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;
}
}

+ 4
- 6
app/Http/Controllers/UserController.php Ver arquivo

@@ -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));


+ 47
- 0
app/Mail/SupportTicket.php Ver arquivo

@@ -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");
}
}

+ 16
- 0
app/Models/Ticket.php Ver arquivo

@@ -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);
}
}

+ 5
- 0
app/Models/User.php Ver arquivo

@@ -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);
}

}

+ 2
- 1
database/migrations/2021_05_19_185302_create_orders_table.php Ver arquivo

@@ -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');
});
}


+ 38
- 0
database/migrations/2021_06_22_153337_create_tickets_table.php Ver arquivo

@@ -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');
}
}

+ 3
- 0
resources/images/chat-text-fill.svg Ver arquivo

@@ -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>

+ 3
- 0
resources/images/life-preserver.svg Ver arquivo

@@ -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>

+ 14
- 7
resources/js/panel/orders.vue Ver arquivo

@@ -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>


+ 14
- 6
resources/js/panel/panel.vue Ver arquivo

@@ -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
- 1
resources/js/panel/settings.vue Ver arquivo

@@ -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">


+ 9
- 0
resources/js/panel/sidebar.vue Ver arquivo

@@ -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"/>


+ 77
- 0
resources/js/panel/support.vue Ver arquivo

@@ -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
- 4
resources/scss/main.scss Ver arquivo

@@ -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;
}


+ 4
- 5
resources/views/home.blade.php Ver arquivo

@@ -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>


+ 24
- 0
resources/views/support-ticket.blade.php Ver arquivo

@@ -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

+ 2
- 0
routes/web.php Ver arquivo

@@ -145,3 +145,5 @@ Route::post('/hooks/pm-transaction',
Route::get('/panel/clear-paying',
[UserController::class, 'clearPaying']);

Route::post('/panel/support',
[Ticket::class, 'send']);

Carregando…
Cancelar
Salvar