Merge pull request #1363 from pixelfed/frontend-ui-refactor

Add Contact Site Page
pull/1392/head
daniel 6 years ago committed by GitHub
commit 08a75c7143
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,18 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Contact extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function adminUrl()
{
return url('/i/admin/contact/show/' . $this->id);
}
}

@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers\Admin;
use Cache, DB;
use Illuminate\Http\Request;
use App\{Contact, Like, Media, Page, Profile, Report, Status, User};
trait AdminSupportController
{
}

@ -23,7 +23,8 @@ use App\Http\Controllers\Admin\{
AdminInstanceController,
AdminReportController,
AdminMediaController,
AdminSettingsController
AdminSettingsController,
AdminSupportController
};
use App\Util\Lexer\PrettyNumber;
use Illuminate\Validation\Rule;
@ -101,7 +102,7 @@ class AdminController extends Controller
$col = $request->query('col') ?? 'id';
$dir = $request->query('dir') ?? 'desc';
$stats = $this->collectUserStats($request);
$users = User::withCount('statuses')->orderBy($col, $dir)->paginate(10);
$users = User::withCount('statuses')->orderBy($col, $dir)->simplePaginate(10);
return view('admin.users.home', compact('users', 'stats'));
}
@ -115,7 +116,7 @@ class AdminController extends Controller
public function statuses(Request $request)
{
$statuses = Status::orderBy('id', 'desc')->paginate(10);
$statuses = Status::orderBy('id', 'desc')->simplePaginate(10);
return view('admin.statuses.home', compact('statuses'));
}
@ -207,11 +208,11 @@ class AdminController extends Controller
$order = $request->input('order') ?? 'desc';
$limit = $request->input('limit') ?? 12;
if($search) {
$profiles = Profile::select('id','username')->where('username','like', "%$search%")->orderBy('id','desc')->paginate($limit);
$profiles = Profile::select('id','username')->where('username','like', "%$search%")->orderBy('id','desc')->simplePaginate($limit);
} else if($filter && $order) {
$profiles = Profile::select('id','username')->withCount(['likes','statuses','followers'])->orderBy($filter, $order)->paginate($limit);
$profiles = Profile::select('id','username')->withCount(['likes','statuses','followers'])->orderBy($filter, $order)->simplePaginate($limit);
} else {
$profiles = Profile::select('id','username')->orderBy('id','desc')->paginate($limit);
$profiles = Profile::select('id','username')->orderBy('id','desc')->simplePaginate($limit);
}
return view('admin.profiles.home', compact('profiles'));

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Auth;
use App\Contact;
class ContactController extends Controller
{
public function show(Request $request)
{
return view('site.contact');
}
public function store(Request $request)
{
abort_if(!Auth::check(), 403);
$this->validate($request, [
'message' => 'required|string|min:5|max:500',
'request_response' => 'string|max:3'
]);
$message = $request->input('message');
$request_response = $request->input('request_response') == 'on' ? true : false;
$user = Auth::user();
$contact = Contact::whereUserId($user->id)
->whereDate('created_at', '>', now()->subDays(1))
->count();
if($contact >= 2) {
return redirect()->back()->with('error', 'You have recently sent a message. Please try again later.');
}
$contact = new Contact;
$contact->user_id = $user->id;
$contact->response_requested = $request_response;
$contact->message = $message;
$contact->save();
return redirect()->back()->with('status', 'Success - Your message has been sent to admins.');
}
}

@ -0,0 +1,37 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use App\Contact;
class ContactAdmin extends Mailable
{
use Queueable, SerializesModels;
protected $contact;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct(Contact $contact)
{
$this->contact = $contact;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$contact = $this->contact;
return $this->markdown('emails.contact.admin')->with(compact('contact'));
}
}

@ -0,0 +1,37 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateContactsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('user_id')->unsigned()->index();
$table->boolean('response_requested')->default(false);
$table->text('message');
$table->text('response');
$table->timestamp('read_at')->nullable();
$table->timestamp('responded_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('contacts');
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -9,8 +9,8 @@
"/js/developers.js": "/js/developers.js?id=1359f11c7349301903f8",
"/js/discover.js": "/js/discover.js?id=2dc2dc703a3a625df1a5",
"/js/loops.js": "/js/loops.js?id=0677173fdad43d0687ba",
"/js/profile.js": "/js/profile.js?id=257d02b221142438d173",
"/js/profile.js": "/js/profile.js?id=461dd489765de3334344",
"/js/search.js": "/js/search.js?id=27e8be8bfef6be586d25",
"/js/status.js": "/js/status.js?id=fb2f77026b548814adc3",
"/js/timeline.js": "/js/timeline.js?id=64ff836536915aaafd15"
"/js/status.js": "/js/status.js?id=51ffe173848e6d23ad2c",
"/js/timeline.js": "/js/timeline.js?id=31e326128f44acc066fa"
}

@ -18,8 +18,8 @@
<source :src="media.url" :type="media.mime">
</video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
<div v-else-if="media.type == 'Image'" slot="img" :title="media.description" :class="media.filter_class">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" loading="lazy">
</div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>
@ -42,8 +42,8 @@
<source :src="media.url" :type="media.mime">
</video>
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" :title="media.description" loading="lazy">
<div v-else-if="media.type == 'Image'" slot="img" :class="media.filter_class" :title="media.description">
<img class="d-block img-fluid w-100" :src="media.url" :alt="media.description" loading="lazy">
</div>
<p v-else class="text-center p-0 font-weight-bold text-white">Error: Problem rendering preview.</p>

@ -13,8 +13,8 @@
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" :title="img.description" loading="lazy">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;" :title="img.description">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" :alt="img.description" loading="lazy">
</div>
</b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">
@ -31,9 +31,9 @@
background="#ffffff"
:interval="0"
>
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id" :alt="img.description" :title="img.description">
<b-carousel-slide v-for="(img, index) in status.media_attachments" :key="img.id" :title="img.description">
<div slot="img" :class="img.filter_class + ' d-block mx-auto text-center'" style="max-height: 600px;">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" loading="lazy">
<img class="img-fluid" style="max-height: 600px;" :src="img.url" loading="lazy" :alt="img.description">
</div>
</b-carousel-slide>
<span class="badge badge-dark box-shadow" style="position: absolute;top:10px;right:10px;">

@ -5,14 +5,14 @@
<p class="mb-0 lead font-weight-bold">{{ status.spoiler_text ? status.spoiler_text : 'CW / NSFW / Hidden Media'}}</p>
<p class="font-weight-light">(click to show)</p>
</summary>
<div class="max-hide-overflow" :class="status.media_attachments[0].filter_class" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
<div class="max-hide-overflow" :class="status.media_attachments[0].filter_class" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy" :alt="status.media_attachments[0].description">
</div>
</details>
</div>
<div v-else>
<div :class="status.media_attachments[0].filter_class" :alt="status.media_attachments[0].description" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy">
<div :class="status.media_attachments[0].filter_class" :title="status.media_attachments[0].description">
<img class="card-img-top" :src="status.media_attachments[0].url" loading="lazy" :alt="status.media_attachments[0].description">
</div>
</div>
</template>

@ -85,7 +85,7 @@
</ul>
<hr>
<div class="d-flex justify-content-center">
{{$media->links()}}
{{$media->appends(['layout'=>request()->layout])->links()}}
</div>
@else
<div class="profile-timeline mt-5 row">
@ -99,7 +99,7 @@
</div>
<hr>
<div class="d-flex justify-content-center">
{{$media->links()}}
{{$media->appends(['layout'=>request()->layout])->links()}}
</div>
@endif
@endsection

@ -0,0 +1,23 @@
@component('mail::message')
# New Support Message
<br>
@if($contact->response_requested)
**This user has requested a response from you.**
@endif
<br>
[**{{$contact->user->username}}**]({{$contact->user->url()}}) has sent the following message:
@component('mail::panel')
{{ $contact->message }}
@endcomponent
@component('mail::button', ['url' => $contact->adminUrl(), 'color' => 'primary'])
View Message
@endcomponent
@endcomponent

@ -0,0 +1,49 @@
@extends('site.partial.template')
@section('section')
<div class="title">
<h3 class="font-weight-bold">Contact</h3>
</div>
<hr>
<section>
@auth
<form method="POST">
@csrf
<div class="form-group">
<label for="input1" class="font-weight-bold">Message</label>
<textarea class="form-control" id="input1" name="message" rows="6" placeholder=""></textarea>
<span class="form-text text-muted text-right msg-counter">0/500</span>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="input2" name="request_response">
<label class="form-check-label" for="input2">Request response from admins</label>
</div>
<button type="submit" class="btn btn-primary font-weight-bold py-0">Submit</button>
</form>
@else
<p class="lead">
@if(filter_var(config('instance.email'), FILTER_VALIDATE_EMAIL) == true)
You can contact the admins by sending an email to {{config('instance.email')}}.
@else
The admins have not listed any public email. Please log in to send a message.
@endif
</p>
@endauth
</section>
@endsection
@auth
@push('styles')
<meta name="csrf-token" content="{{ csrf_token() }}">
@endpush
@push('scripts')
<script type="text/javascript">
$('#input1').on('keyup change paste', function(el) {
let len = el.target.value.length;
$('.msg-counter').text(len + '/500');
});
</script>
@endpush
@endauth

@ -10,10 +10,24 @@
@include('site.partial.sidebar')
<div class="col-12 col-md-9 p-5">
@if (session('status'))
<div class="alert alert-success">
<div class="alert alert-success font-weight-bold">
{{ session('status') }}
</div>
@endif
@if (session('error'))
<div class="alert alert-danger font-weight-bold">
{{ session('error') }}
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger font-weight-bold">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@yield('section')
</div>
</div>

@ -0,0 +1,19 @@
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-{{ $color ?? 'primary' }}" target="_blank">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>

@ -0,0 +1,11 @@
<tr>
<td>
<table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="content-cell" align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>

@ -0,0 +1,7 @@
<tr>
<td class="header">
<a href="{{ $url }}">
{{ $slot }}
</a>
</td>
</tr>

@ -0,0 +1,54 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<style>
@media only screen and (max-width: 600px) {
.inner-body {
width: 100% !important;
}
.footer {
width: 100% !important;
}
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
}
}
</style>
<table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
{{ $header ?? '' }}
<!-- Email Body -->
<tr>
<td class="body" width="100%" cellpadding="0" cellspacing="0">
<table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
<!-- Body content -->
<tr>
<td class="content-cell">
{{ Illuminate\Mail\Markdown::parse($slot) }}
{{ $subcopy ?? '' }}
</td>
</tr>
</table>
</td>
</tr>
{{ $footer ?? '' }}
</table>
</td>
</tr>
</table>
</body>
</html>

@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent

@ -0,0 +1,13 @@
<table class="panel" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-content">
<table width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td class="panel-item">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>
</td>
</tr>
</table>

@ -0,0 +1,7 @@
<table class="promotion" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

@ -0,0 +1,13 @@
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td align="center">
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<a href="{{ $url }}" class="button button-green" target="_blank">{{ $slot }}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>

@ -0,0 +1,7 @@
<table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
{{ Illuminate\Mail\Markdown::parse($slot) }}
</td>
</tr>
</table>

@ -0,0 +1,3 @@
<div class="table">
{{ Illuminate\Mail\Markdown::parse($slot) }}
</div>

@ -0,0 +1,292 @@
/* Base */
body,
body *:not(html):not(style):not(br):not(tr):not(code) {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
box-sizing: border-box;
}
body {
background-color: #f8fafc;
color: #74787e;
height: 100%;
hyphens: auto;
line-height: 1.4;
margin: 0;
-moz-hyphens: auto;
-ms-word-break: break-all;
width: 100% !important;
-webkit-hyphens: auto;
-webkit-text-size-adjust: none;
word-break: break-all;
word-break: break-word;
}
p,
ul,
ol,
blockquote {
line-height: 1.4;
text-align: left;
}
a {
color: #3869d4;
}
a img {
border: none;
}
/* Typography */
h1 {
color: #3d4852;
font-size: 19px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h2 {
color: #3d4852;
font-size: 16px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
h3 {
color: #3d4852;
font-size: 14px;
font-weight: bold;
margin-top: 0;
text-align: left;
}
p {
color: #3d4852;
font-size: 16px;
line-height: 1.5em;
margin-top: 0;
text-align: left;
}
p.sub {
font-size: 12px;
}
img {
max-width: 100%;
}
/* Layout */
.wrapper {
background-color: #f8fafc;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.content {
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
/* Header */
.header {
padding: 25px 0;
text-align: center;
}
.header a {
color: #bbbfc3;
font-size: 19px;
font-weight: bold;
text-decoration: none;
text-shadow: 0 1px 0 white;
}
/* Body */
.body {
background-color: #ffffff;
border-bottom: 1px solid #edeff2;
border-top: 1px solid #edeff2;
margin: 0;
padding: 0;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.inner-body {
background-color: #ffffff;
margin: 0 auto;
padding: 0;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
/* Subcopy */
.subcopy {
border-top: 1px solid #edeff2;
margin-top: 25px;
padding-top: 25px;
}
.subcopy p {
font-size: 12px;
}
/* Footer */
.footer {
margin: 0 auto;
padding: 0;
text-align: center;
width: 570px;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 570px;
}
.footer p {
color: #aeaeae;
font-size: 12px;
text-align: center;
}
/* Tables */
.table table {
margin: 30px auto;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.table th {
border-bottom: 1px solid #edeff2;
padding-bottom: 8px;
margin: 0;
}
.table td {
color: #74787e;
font-size: 15px;
line-height: 18px;
padding: 10px 0;
margin: 0;
}
.content-cell {
padding: 35px;
}
/* Buttons */
.action {
margin: 30px auto;
padding: 0;
text-align: center;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.button {
border-radius: 3px;
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.16);
color: #fff;
display: inline-block;
text-decoration: none;
-webkit-text-size-adjust: none;
}
.button-blue,
.button-primary {
background-color: #3490dc;
border-top: 10px solid #3490dc;
border-right: 18px solid #3490dc;
border-bottom: 10px solid #3490dc;
border-left: 18px solid #3490dc;
}
.button-green,
.button-success {
background-color: #38c172;
border-top: 10px solid #38c172;
border-right: 18px solid #38c172;
border-bottom: 10px solid #38c172;
border-left: 18px solid #38c172;
}
.button-red,
.button-error {
background-color: #e3342f;
border-top: 10px solid #e3342f;
border-right: 18px solid #e3342f;
border-bottom: 10px solid #e3342f;
border-left: 18px solid #e3342f;
}
/* Panels */
.panel {
margin: 0 0 21px;
}
.panel-content {
background-color: #f1f5f8;
padding: 16px;
}
.panel-item {
padding: 0;
}
.panel-item p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
/* Promotions */
.promotion {
background-color: #ffffff;
border: 2px dashed #9ba2ab;
margin: 0;
margin-bottom: 25px;
margin-top: 25px;
padding: 24px;
width: 100%;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
}
.promotion h1 {
text-align: center;
}
.promotion p {
font-size: 15px;
text-align: center;
}

@ -0,0 +1 @@
{{ $slot }}: {{ $url }}

@ -0,0 +1 @@
[{{ $slot }}]({{ $url }})

@ -0,0 +1,9 @@
{!! strip_tags($header) !!}
{!! strip_tags($slot) !!}
@isset($subcopy)
{!! strip_tags($subcopy) !!}
@endisset
{!! strip_tags($footer) !!}

@ -0,0 +1,27 @@
@component('mail::layout')
{{-- Header --}}
@slot('header')
@component('mail::header', ['url' => config('app.url')])
{{ config('app.name') }}
@endcomponent
@endslot
{{-- Body --}}
{{ $slot }}
{{-- Subcopy --}}
@isset($subcopy)
@slot('subcopy')
@component('mail::subcopy')
{{ $subcopy }}
@endcomponent
@endslot
@endisset
{{-- Footer --}}
@slot('footer')
@component('mail::footer')
© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.')
@endcomponent
@endslot
@endcomponent
Loading…
Cancel
Save