mirror of https://github.com/pixelfed/pixelfed
Add Curated Onboarding
parent
9409c569bd
commit
8dac2caf1d
@ -0,0 +1,398 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\User;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Mail\CuratedRegisterConfirmEmail;
|
||||
use App\Mail\CuratedRegisterNotifyAdmin;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
|
||||
|
||||
class CuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
|
||||
|
||||
if((bool) config_cache('pixelfed.open_registration')) {
|
||||
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
|
||||
} else {
|
||||
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
return view('auth.curated-register.index', ['step' => 1]);
|
||||
}
|
||||
|
||||
public function concierge(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->has('next') &&
|
||||
$request->session()->has('cur-reg-con.cr-id');
|
||||
return view('auth.curated-register.concierge', compact('emailConfirmed'));
|
||||
}
|
||||
|
||||
public function conciergeResponseSent(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeFormShow(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$arid = $request->session()->get('cur-reg-con.ac-id');
|
||||
$showCaptcha = config('instance.curated_registration.captcha_enabled');
|
||||
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
|
||||
$showCaptcha = $attempts && $attempts >= 2;
|
||||
} else {
|
||||
$showCaptcha = false;
|
||||
}
|
||||
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
|
||||
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
|
||||
}
|
||||
|
||||
public function conciergeFormStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$request->session()->increment('cur-reg-con-attempt');
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$attempts = $request->session()->get('cur-reg-con-attempt');
|
||||
$messages = [];
|
||||
$rules = [
|
||||
'response' => 'required|string|min:5|max:1000',
|
||||
'crid' => 'required|integer|min:1',
|
||||
'acid' => 'required|integer|min:1'
|
||||
];
|
||||
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$acid = $request->session()->get('cur-reg-con.ac-id');
|
||||
abort_if((string) $crid !== $request->input('crid'), 404);
|
||||
abort_if((string) $acid !== $request->input('acid'), 404);
|
||||
|
||||
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
|
||||
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
|
||||
}
|
||||
|
||||
$act = CuratedRegisterActivity::create([
|
||||
'register_id' => $crid,
|
||||
'reply_to_id' => $acid,
|
||||
'type' => 'user_response',
|
||||
'message' => $request->input('response'),
|
||||
'from_user' => true,
|
||||
'action_required' => true,
|
||||
]);
|
||||
|
||||
$request->session()->pull('cur-reg-con');
|
||||
$request->session()->pull('cur-reg-con-attempt');
|
||||
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$rules = [
|
||||
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'id' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'code' => 'required_if:action,email',
|
||||
'action' => 'required|string|in:email,message',
|
||||
'email' => 'required_if:action,email|email',
|
||||
'response' => 'required_if:action,message|string|min:20|max:1000',
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$action = $request->input('action');
|
||||
$sid = $request->input('sid');
|
||||
$id = $request->input('id');
|
||||
$code = $request->input('code');
|
||||
$email = $request->input('email');
|
||||
|
||||
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
|
||||
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
|
||||
|
||||
if(!hash_equals($ac->secret_code, $code)) {
|
||||
return redirect()->back()->withErrors(['code' => 'Invalid code']);
|
||||
}
|
||||
|
||||
if(!hash_equals($cr->email, $email)) {
|
||||
return redirect()->back()->withErrors(['email' => 'Invalid email']);
|
||||
}
|
||||
|
||||
$request->session()->put('cur-reg-con.email-confirmed', true);
|
||||
$request->session()->put('cur-reg-con.cr-id', $cr->id);
|
||||
$request->session()->put('cur-reg-con.ac-id', $ac->id);
|
||||
$emailConfirmed = true;
|
||||
return redirect('/auth/sign_up/concierge/form');
|
||||
}
|
||||
|
||||
public function confirmEmail(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.confirm_email');
|
||||
}
|
||||
|
||||
public function emailConfirmed(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function resendConfirmation(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.resend-confirmation');
|
||||
}
|
||||
|
||||
public function resendConfirmationProcess(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'exists:curated_registers',
|
||||
]
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
|
||||
if(!$cur) {
|
||||
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
|
||||
}
|
||||
|
||||
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->count();
|
||||
|
||||
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
|
||||
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
|
||||
}
|
||||
|
||||
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->where('created_at', '>', now()->subHours(12))
|
||||
->count();
|
||||
|
||||
if($count) {
|
||||
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
|
||||
}
|
||||
|
||||
CuratedRegisterActivity::create([
|
||||
'register_id' => $cur->id,
|
||||
'type' => 'user_resend_email_confirmation',
|
||||
'admin_only_view' => true,
|
||||
'from_admin' => false,
|
||||
'from_user' => false,
|
||||
'action_required' => false,
|
||||
]);
|
||||
|
||||
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
|
||||
return view('auth.curated-register.resent-confirmation');
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function confirmEmailHandle(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'sid' => 'required',
|
||||
'code' => 'required'
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cr = CuratedRegister::whereNull('email_verified_at')
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->find($request->input('sid'));
|
||||
if(!$cr) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
if(!hash_equals($cr->verify_code, $request->input('code'))) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
$cr->email_verified_at = now();
|
||||
$cr->save();
|
||||
|
||||
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
|
||||
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function proceed(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'step' => 'required|integer|in:1,2,3,4'
|
||||
]);
|
||||
$step = $request->input('step');
|
||||
|
||||
switch($step) {
|
||||
case 1:
|
||||
$step = 2;
|
||||
$request->session()->put('cur-step', 1);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$this->stepTwo($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 2);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$this->stepThree($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 3);
|
||||
$verifiedEmail = true;
|
||||
$request->session()->pull('cur-reg');
|
||||
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function stepTwo($request)
|
||||
{
|
||||
if($request->filled('reason')) {
|
||||
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
|
||||
}
|
||||
if($request->filled('username')) {
|
||||
$request->session()->put('cur-reg.form-username', $request->input('username'));
|
||||
}
|
||||
if($request->filled('email')) {
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
}
|
||||
$this->validate($request, [
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:curated_registers',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required|min:8',
|
||||
'password_confirmation' => 'required|same:password',
|
||||
'reason' => 'required|min:20|max:1000',
|
||||
'agree' => 'required|accepted'
|
||||
]);
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
$request->session()->put('cur-reg.form-password', $request->input('password'));
|
||||
}
|
||||
|
||||
protected function stepThree($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
]
|
||||
]);
|
||||
$cr = new CuratedRegister;
|
||||
$cr->email = $request->email;
|
||||
$cr->username = $request->session()->get('cur-reg.form-username');
|
||||
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
|
||||
$cr->ip_address = $request->ip();
|
||||
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
|
||||
$cr->verify_code = Str::random(40);
|
||||
$cr->save();
|
||||
|
||||
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\CuratedOnboarding;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Mail\CuratedRegisterNotifyAdmin;
|
||||
|
||||
class CuratedOnboardingNotifyAdminNewApplicationPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $cr;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $cr)
|
||||
{
|
||||
$this->cr = $cr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
if(!config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
config('instance.curated_registration.notify.admin.on_verify_email.bundle') ?
|
||||
$this->handleBundled() :
|
||||
$this->handleUnbundled();
|
||||
}
|
||||
|
||||
protected function handleBundled()
|
||||
{
|
||||
$cr = $this->cr;
|
||||
Storage::append('conanap.json', json_encode([
|
||||
'id' => $cr->id,
|
||||
'email' => $cr->email,
|
||||
'created_at' => $cr->created_at,
|
||||
'updated_at' => $cr->updated_at,
|
||||
]));
|
||||
}
|
||||
|
||||
protected function handleUnbundled()
|
||||
{
|
||||
$cr = $this->cr;
|
||||
if($aid = config_cache('instance.admin.pid')) {
|
||||
$admin = User::whereProfileId($aid)->first();
|
||||
if($admin && $admin->email) {
|
||||
Mail::to($admin->email)->send(new CuratedRegisterNotifyAdmin($cr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterAcceptUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-accepted',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
|
||||
class CuratedRegisterConfirmEmail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Welcome to Pixelfed! Please Confirm Your Email',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.confirm_email',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
|
||||
class CuratedRegisterNotifyAdmin extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[Requires Action]: New Curated Onboarding Application',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.admin_notify',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterNotifyAdminUserResponse extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $activity;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Curated Register Notify Admin User Response',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.admin_notify_user_response',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterRejectUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-rejected',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
|
||||
class CuratedRegisterRequestDetailsFromUser extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
public $verify;
|
||||
public $activity;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(CuratedRegister $verify, CuratedRegisterActivity $activity)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: '[Action Needed]: Additional information requested',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.request-details-from-user',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Mail\Mailables\Content;
|
||||
use Illuminate\Mail\Mailables\Envelope;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class CuratedRegisterSendMessage extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public $verify;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct($verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
*/
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return new Envelope(
|
||||
subject: 'Your ' . config('pixelfed.domain.app') . ' Registration Update',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the message content definition.
|
||||
*/
|
||||
public function content(): Content
|
||||
{
|
||||
return new Content(
|
||||
markdown: 'emails.curated-register.message-from-admin',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attachments for the message.
|
||||
*
|
||||
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
|
||||
*/
|
||||
public function attachments(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CuratedRegister extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $casts = [
|
||||
'autofollow_account_ids' => 'array',
|
||||
'admin_notes' => 'array',
|
||||
'email_verified_at' => 'datetime',
|
||||
'admin_notified_at' => 'datetime',
|
||||
'action_taken_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function adminStatusLabel()
|
||||
{
|
||||
if(!$this->email_verified_at) {
|
||||
return '<span class="border border-danger px-3 py-1 rounded text-white font-weight-bold">Unverified email</span>';
|
||||
}
|
||||
if($this->is_accepted) { return 'Approved'; }
|
||||
if($this->is_rejected) { return 'Rejected'; }
|
||||
if($this->is_awaiting_more_info ) {
|
||||
return '<span class="border border-info px-3 py-1 rounded text-white font-weight-bold">Awaiting Details</span>';
|
||||
}
|
||||
if($this->is_closed ) { return 'Closed'; }
|
||||
|
||||
return '<span class="border border-success px-3 py-1 rounded text-white font-weight-bold">Open</span>';
|
||||
}
|
||||
|
||||
public function emailConfirmUrl()
|
||||
{
|
||||
return url('/auth/sign_up/confirm?sid=' . $this->id . '&code=' . $this->verify_code);
|
||||
}
|
||||
|
||||
public function emailReplyUrl()
|
||||
{
|
||||
return url('/auth/sign_up/concierge?sid=' . $this->id . '&code=' . $this->verify_code . '&sc=' . str_random(8));
|
||||
}
|
||||
|
||||
public function adminReviewUrl()
|
||||
{
|
||||
return url('/i/admin/curated-onboarding/show/' . $this->id);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CuratedRegisterActivity extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'admin_notified_at' => 'datetime',
|
||||
'action_taken_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function application()
|
||||
{
|
||||
return $this->belongsTo(CuratedRegister::class, 'register_id');
|
||||
}
|
||||
|
||||
public function emailReplyUrl()
|
||||
{
|
||||
return url('/auth/sign_up/concierge?sid='.$this->register_id . '&id=' . $this->id . '&code=' . $this->secret_code);
|
||||
}
|
||||
|
||||
public function adminReviewUrl()
|
||||
{
|
||||
$url = '/i/admin/curated-onboarding/show/' . $this->register_id . '/?ah=' . $this->id;
|
||||
if($this->reply_to_id) {
|
||||
$url .= '&rtid=' . $this->reply_to_id;
|
||||
}
|
||||
return url($url);
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('curated_registers', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('email')->unique()->nullable()->index();
|
||||
$table->string('username')->unique()->nullable()->index();
|
||||
$table->string('password')->nullable();
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->string('verify_code')->nullable();
|
||||
$table->text('reason_to_join')->nullable();
|
||||
$table->unsignedBigInteger('invited_by')->nullable()->index();
|
||||
$table->boolean('is_approved')->default(0)->index();
|
||||
$table->boolean('is_rejected')->default(0)->index();
|
||||
$table->boolean('is_awaiting_more_info')->default(0)->index();
|
||||
$table->boolean('is_closed')->default(0)->index();
|
||||
$table->json('autofollow_account_ids')->nullable();
|
||||
$table->json('admin_notes')->nullable();
|
||||
$table->unsignedInteger('approved_by_admin_id')->nullable();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->timestamp('admin_notified_at')->nullable();
|
||||
$table->timestamp('action_taken_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('curated_registers');
|
||||
}
|
||||
};
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('curated_register_activities', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->unsignedInteger('register_id')->nullable()->index();
|
||||
$table->unsignedInteger('admin_id')->nullable();
|
||||
$table->unsignedInteger('reply_to_id')->nullable()->index();
|
||||
$table->string('secret_code')->nullable();
|
||||
$table->string('type')->nullable()->index();
|
||||
$table->string('title')->nullable();
|
||||
$table->string('link')->nullable();
|
||||
$table->text('message')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->boolean('from_admin')->default(false)->index();
|
||||
$table->boolean('from_user')->default(false)->index();
|
||||
$table->boolean('admin_only_view')->default(true);
|
||||
$table->boolean('action_required')->default(false);
|
||||
$table->timestamp('admin_notified_at')->nullable();
|
||||
$table->timestamp('action_taken_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('curated_register_activities');
|
||||
}
|
||||
};
|
@ -0,0 +1,86 @@
|
||||
@extends('admin.partial.template-full')
|
||||
|
||||
@section('section')
|
||||
</div><div class="header bg-primary pb-3 mt-n4">
|
||||
<div class="container-fluid">
|
||||
<div class="header-body">
|
||||
<div class="row align-items-center py-4">
|
||||
<div class="col-lg-8 col-12">
|
||||
<p class="display-1 text-white d-inline-block mb-0">Curated Onboarding</p>
|
||||
<p class="text-white mb-0">The ideal solution for communities seeking a balance between open registration and invite-only membership</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if((bool) config_cache('instance.curated_registration.enabled'))
|
||||
<div class="m-n2 m-lg-4">
|
||||
<div class="container-fluid mt-4">
|
||||
@include('admin.curated-register.partials.nav')
|
||||
|
||||
@if($records && $records->count())
|
||||
<div class="table-responsive rounded">
|
||||
<table class="table table-dark">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col">ID</th>
|
||||
<th scope="col">Username</th>
|
||||
@if(in_array($filter, ['all', 'open']))
|
||||
<th scope="col">Status</th>
|
||||
@endif
|
||||
<th scope="col">Reason for Joining</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($records as $record)
|
||||
<tr>
|
||||
<td class="align-middle">
|
||||
<a href="/i/admin/curated-onboarding/show/{{$record->id}}" class="font-weight-bold">
|
||||
#{{ $record->id }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<p class="font-weight-bold mb-0">
|
||||
@{{ $record->username }}
|
||||
</p>
|
||||
</td>
|
||||
@if(in_array($filter, ['all', 'open']))
|
||||
<td class="align-middle">
|
||||
{!! $record->adminStatusLabel() !!}
|
||||
</td>
|
||||
@endif
|
||||
<td class="align-middle">
|
||||
{{ str_limit($record->reason_to_join, 100) }}
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<p class="mb-0">
|
||||
{{ str_limit(\Illuminate\Support\Str::mask($record->email, '*', 5, 10), 10) }}
|
||||
</p>
|
||||
</td>
|
||||
<td class="align-middle">{{ $record->created_at->diffForHumans() }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="d-flex mt-3">
|
||||
{{ $records->links() }}
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<p class="text-center"><i class="far fa-check-circle fa-6x text-success"></i></p>
|
||||
<p class="lead text-center">No {{ request()->filled('filter') ? request()->filter : 'open' }} applications found!</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@include('admin.curated-register.partials.not-enabled')
|
||||
@endif
|
||||
@endsection
|
@ -0,0 +1,463 @@
|
||||
<template v-if="loaded">
|
||||
@if($email_verified_at === null)
|
||||
<div class="alert alert-danger mb-3">
|
||||
<p class="mb-0 font-weight-bold">Applicant has not verified their email address yet, action can not be taken at this time.</p>
|
||||
</div>
|
||||
@elseif($is_closed != true)
|
||||
<div class="d-flex justify-content-between flex-column flex-md-row mb-4" style="gap:1rem">
|
||||
<button
|
||||
class="btn btn-success bg-gradient-success rounded-pill"
|
||||
v-on:click.prevent="handleAction('approve', $event)">
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-danger bg-gradient-danger rounded-pill flex-grow-1"
|
||||
v-on:click.prevent="handleAction('reject', $event)">
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
class="btn rounded-pill px-md-5"
|
||||
:class="[ composeFormOpen ? 'btn-dark bg-gradient-dark' : 'btn-outline-dark' ]"
|
||||
v-on:click.prevent="handleAction('request', $event)">
|
||||
Request details
|
||||
</button>
|
||||
<button
|
||||
class="btn rounded-pill px-md-5"
|
||||
:class="[ messageFormOpen ? 'btn-dark bg-gradient-dark' : 'btn-outline-dark' ]"
|
||||
v-on:click.prevent="handleAction('message', $event)">
|
||||
Message
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@else
|
||||
@if($is_rejected == true)
|
||||
<p>Application was <span class="font-weight-bold text-danger">rejected</span> on {{ $action_taken_at }}</p>
|
||||
@elseif($is_approved == true)
|
||||
<p>Application was <span class="font-weight-bold text-success">approved</span> on {{ $action_taken_at }}</p>
|
||||
@else
|
||||
<p>Application was closed on {{ $action_taken_at }}</p>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="composeFormOpen">
|
||||
<div class="card">
|
||||
<div class="card-body pt-0">
|
||||
<p class="lead font-weight-bold text-center">Request Additional Details</p>
|
||||
<p class="text-muted">Use this form to request additional details. Once you press Send, we'll send the potential user an email with a special link they can visit with a form that they can provide additional details with. You can also Preview the email before it's sent.</p>
|
||||
|
||||
<div class="request-form">
|
||||
<div class="form-group">
|
||||
<label for="requestDetailsMessageInput" class="small text-muted">Your Message:</label>
|
||||
<textarea
|
||||
class="form-control text-dark"
|
||||
id="requestDetailsMessageInput"
|
||||
rows="5"
|
||||
v-model="composeMessage"
|
||||
style="white-space: pre-wrap;"
|
||||
placeholder="Enter your additional detail message here...">
|
||||
</textarea>
|
||||
<p class="help-text small text-right">
|
||||
<span>@{{ composeMessage && composeMessage.length ? composeMessage.length : 0 }}</span>
|
||||
<span>/</span>
|
||||
<span>500</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary rounded-pill btn-sm px-4"
|
||||
v-on:click.prevent="handleSend()">
|
||||
Send
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-dark rounded-pill btn-sm px-4"
|
||||
:href="previewDetailsMessageUrl"
|
||||
target="_blank">
|
||||
Preview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<transition name="fade">
|
||||
<div v-show="messageFormOpen">
|
||||
<div class="card">
|
||||
<div class="card-body pt-0">
|
||||
<p class="lead font-weight-bold text-center">Send Message</p>
|
||||
<p class="text-muted">Use this form to send a message to the applicant. Once you press Send, we'll send the potential user an email with your message. You can also Preview the email before it's sent.</p>
|
||||
|
||||
<div class="request-form">
|
||||
<div class="form-group">
|
||||
<label for="sendMessageInput" class="small text-muted">Your Message:</label>
|
||||
<textarea
|
||||
class="form-control text-dark"
|
||||
id="sendMessageInput"
|
||||
rows="5"
|
||||
v-model="messageBody"
|
||||
style="white-space: pre-wrap;"
|
||||
placeholder="Enter your message here...">
|
||||
</textarea>
|
||||
<p class="help-text small text-right">
|
||||
<span>@{{ messageBody && messageBody.length ? messageBody.length : 0 }}</span>
|
||||
<span>/</span>
|
||||
<span>500</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary rounded-pill btn-sm px-4"
|
||||
v-on:click.prevent="handleMessageSend()">
|
||||
Send
|
||||
</button>
|
||||
<a
|
||||
class="btn btn-dark rounded-pill btn-sm px-4"
|
||||
:href="previewMessageUrl"
|
||||
target="_blank">
|
||||
Preview
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="card border">
|
||||
<div class="card-body">
|
||||
<p class="text-center font-weight-bold">Activity Log</p>
|
||||
<div class="activity-log">
|
||||
<div v-if="!loaded" class="d-flex justify-content-center align-items-center py-5 my-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="activity in activities"
|
||||
class="activity-log-item"
|
||||
:key="activity.timestamp">
|
||||
<div v-if="activity.action === 'approved'" class="activity-log-item-icon bg-success">
|
||||
<i class="far fa-check fa-lg text-white"></i>
|
||||
</div>
|
||||
<div v-else-if="activity.action === 'rejected'" class="activity-log-item-icon bg-danger">
|
||||
<i class="far fa-times fa-lg text-white"></i>
|
||||
</div>
|
||||
<div v-else-if="activity.action === 'request_details'" class="activity-log-item-icon bg-lighter">
|
||||
<i class="fas fa-exclamation fa-lg text-dark"></i>
|
||||
</div>
|
||||
<div v-else class="activity-log-item-icon">
|
||||
<i class="fas fa-circle"></i>
|
||||
</div>
|
||||
<div class="activity-log-item-date">@{{ parseDate(activity.timestamp) }}<span>@{{ parseTime(activity.timestamp) }}</span></div>
|
||||
<div class="activity-log-item-content"><span class="activity-log-item-content-title">@{{ activity.title }}</span><span class="activity-log-item-content-message" v-if="activity.message">@{{ strLimit(activity.message) }}</span></div>
|
||||
<div class="d-flex" style="gap: 1rem;">
|
||||
<div v-if="activity.link">
|
||||
<a href="#" class="activity-log-item-content-link text-muted" @click.prevent="openModal(activity)">Details</a>
|
||||
</div>
|
||||
<div v-if="activity.user_response">
|
||||
<a href="#" class="activity-log-item-content-link" @click.prevent="openUserResponse(activity)">View User Response</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="card card-body d-flex justify-content-center align-items-center py-5">
|
||||
<b-spinner />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
let app = new Vue({
|
||||
el: '#panel',
|
||||
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
activities: [],
|
||||
composeFormOpen: false,
|
||||
messageFormOpen: false,
|
||||
composeMessage: null,
|
||||
messageBody: null,
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.fetchActivities();
|
||||
}, 1000)
|
||||
},
|
||||
|
||||
computed: {
|
||||
previewDetailsMessageUrl() {
|
||||
return `/i/admin/curated-onboarding/show/{{$id}}/preview-details-message?message=${encodeURIComponent(this.composeMessage)}`;
|
||||
},
|
||||
previewMessageUrl() {
|
||||
return `/i/admin/curated-onboarding/show/{{$id}}/preview-message?message=${encodeURIComponent(this.messageBody)}`;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
parseDate(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
parseTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
},
|
||||
|
||||
strLimit(str, len = 15) {
|
||||
if(str && str.length) {
|
||||
return str.slice(0, len) + (str.length > 15 ? '...' : '');
|
||||
}
|
||||
return str;
|
||||
},
|
||||
|
||||
fetchActivities() {
|
||||
axios.get('/i/admin/api/curated-onboarding/show/{{$id}}/activity-log')
|
||||
.then(res => {
|
||||
console.log(res.data);
|
||||
this.activities = res.data;
|
||||
})
|
||||
.finally(() => {
|
||||
this.loaded = true;
|
||||
})
|
||||
},
|
||||
|
||||
handleAction(action, $event) {
|
||||
$event.currentTarget?.blur();
|
||||
|
||||
switch(action) {
|
||||
case 'approve':
|
||||
this.handleApprove();
|
||||
break;
|
||||
|
||||
case 'reject':
|
||||
this.handleReject();
|
||||
break;
|
||||
|
||||
case 'request':
|
||||
this.messageFormOpen = false;
|
||||
this.composeFormOpen = !this.composeFormOpen;
|
||||
break;
|
||||
|
||||
case 'message':
|
||||
this.composeFormOpen = false;
|
||||
this.messageFormOpen = !this.messageFormOpen;
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleApprove() {
|
||||
swal({
|
||||
title: "Approve Request?",
|
||||
text: "The user application request will be approved.",
|
||||
icon: "warning",
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
}).then((willApprove) => {
|
||||
if(willApprove) {
|
||||
this.handleApproveAction();
|
||||
} else {
|
||||
swal("Approval Cancelled!", "The application approval has been cancelled. If you change your mind, you can easily approve or reject this application in the future.", "success");
|
||||
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleReject() {
|
||||
swal({
|
||||
title: "Are you sure?",
|
||||
text: "The user application request will be rejected.",
|
||||
icon: "warning",
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
})
|
||||
.then((willReject) => {
|
||||
if (willReject) {
|
||||
swal({
|
||||
title: "Choose Action",
|
||||
text: "You can provide a rejection email, or simply silently reject",
|
||||
icon: "warning",
|
||||
buttons: {
|
||||
cancel: "Cancel",
|
||||
reject: {
|
||||
text: "Reject with email",
|
||||
value: "reject-email"
|
||||
},
|
||||
silent: {
|
||||
text: "Silently Reject",
|
||||
value: "reject-silent"
|
||||
}
|
||||
},
|
||||
dangerMode: true,
|
||||
})
|
||||
.then(res => {
|
||||
if(!res) {
|
||||
swal("Rejection Cancelled!", "The application rejection has been cancelled. If you change your mind, you can easily reject this application in the future.", "success");
|
||||
} else {
|
||||
this.handleRejectAction(res);
|
||||
}
|
||||
})
|
||||
} else {
|
||||
swal("Rejection Cancelled!", "The application rejection has been cancelled. If you change your mind, you can easily reject this application in the future.", "success");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleRejectAction(action) {
|
||||
axios.post('/i/admin/api/curated-onboarding/show/{{$id}}/reject', {
|
||||
action: action
|
||||
}).then(res => {
|
||||
window.location.href = '/i/admin/curated-onboarding/home?a=rj';
|
||||
console.log(res);
|
||||
})
|
||||
},
|
||||
|
||||
handleApproveAction() {
|
||||
axios.post('/i/admin/api/curated-onboarding/show/{{$id}}/approve')
|
||||
.then(res => {
|
||||
window.location.href = '/i/admin/curated-onboarding/home?a=aj';
|
||||
})
|
||||
},
|
||||
|
||||
handlePreview() {
|
||||
axios.post('/i/admin/api/curated-onboarding/show/{{$id}}/message/preview', {
|
||||
message: this.composeMessage
|
||||
})
|
||||
.then(res => {
|
||||
console.log(res.data);
|
||||
})
|
||||
},
|
||||
|
||||
handleSend() {
|
||||
swal({
|
||||
title: "Confirm",
|
||||
text: "Are you sure you want to send this request to this user?",
|
||||
icon: "warning",
|
||||
buttons: true,
|
||||
dangerMode: true,
|
||||
}).then((hasConfirmed) => {
|
||||
if(hasConfirmed) {
|
||||
this.composeFormOpen = false;
|
||||
this.loaded = false;
|
||||
axios.post('/i/admin/api/curated-onboarding/show/{{$id}}/message/send', {
|
||||
message: this.composeMessage
|
||||
})
|
||||
.then(res => {
|
||||
this.composeMessage = null;
|
||||
swal('Successfully sent!','', 'success');
|
||||
this.fetchActivities();
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
openModal(activity) {
|
||||
swal(activity.title, activity.message)
|
||||
},
|
||||
|
||||
openUserResponse(activity) {
|
||||
swal('User Response', activity.user_response.message)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
.activity-log-item {
|
||||
border-left: 1px solid #e5e5e5;
|
||||
position: relative;
|
||||
padding: 2rem 1.5rem .5rem 2.5rem;
|
||||
font-size: .9rem;
|
||||
margin-left: 3rem;
|
||||
min-height: 5rem
|
||||
}
|
||||
.activity-log-item:last-child {
|
||||
padding-bottom: 4rem
|
||||
}
|
||||
.activity-log-item .activity-log-item-date {
|
||||
margin-bottom: .5rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
}
|
||||
.activity-log-item .activity-log-item-date span {
|
||||
color: #888;
|
||||
font-size: 85%;
|
||||
padding-left: .4rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
.activity-log-item .activity-log-item-content {
|
||||
padding: .5rem .8rem;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
.activity-log-item .activity-log-item-content span {
|
||||
display:block;
|
||||
color:#666;
|
||||
}
|
||||
.activity-log-item .activity-log-item-icon {
|
||||
line-height:2.6rem;
|
||||
position:absolute;
|
||||
left:-1.3rem;
|
||||
width:2.6rem;
|
||||
height:2.6rem;
|
||||
text-align:center;
|
||||
border-radius:50%;
|
||||
font-size:1.1rem;
|
||||
background-color:#fff;
|
||||
color:#fff
|
||||
}
|
||||
.activity-log-item .activity-log-item-icon {
|
||||
color:#e5e5e5;
|
||||
border:1px solid #e5e5e5;
|
||||
font-size:.6rem
|
||||
}
|
||||
.activity-log-item-content-title {
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
color: #000000 !important;
|
||||
}
|
||||
.activity-log-item-content-message {
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
}
|
||||
@media(min-width:992px) {
|
||||
.activity-log-item {
|
||||
margin-left:10rem
|
||||
}
|
||||
.activity-log-item .activity-log-item-date {
|
||||
position:absolute;
|
||||
left:-10rem;
|
||||
width:7.5rem;
|
||||
text-align:right
|
||||
}
|
||||
.activity-log-item .activity-log-item-date span {
|
||||
display:block
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,39 @@
|
||||
@if(request()->filled('a'))
|
||||
@if(request()->input('a') === 'rj')
|
||||
<div class="alert alert-danger">
|
||||
<p class="font-weight-bold mb-0"><i class="far fa-info-circle mr-2"></i>Successfully rejected application!</p>
|
||||
</div>
|
||||
@endif
|
||||
@if(request()->input('a') === 'aj')
|
||||
<div class="alert alert-success">
|
||||
<p class="font-weight-bold mb-0"><i class="far fa-info-circle mr-2"></i>Successfully accepted application!</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
<div class="row mb-3 justify-content-between">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->has('filter') ? '':'active'}}" href="/i/admin/curated-onboarding/home">Open Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->has('filter') && request()->filter == 'all' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=all">All Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->has('filter') && request()->filter == 'awaiting' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=awaiting">Awaiting Info</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->has('filter') && request()->filter == 'approved' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=approved">Approved Applications</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {{request()->has('filter') && request()->filter == 'rejected' ? 'active':''}}" href="/i/admin/curated-onboarding/home?filter=rejected">Rejected Applications</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@push('scripts')
|
||||
|
||||
@endpush
|
@ -0,0 +1,24 @@
|
||||
<div class="m-n2 m-lg-4">
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h1 class="text-center">Feature not enabled</h1>
|
||||
|
||||
<p class="lead">To enable this feature:
|
||||
|
||||
<ol class="lead">
|
||||
<li>Go to the <a href="/i/admin/settings" class="font-weight-bold">Settings page</a></li>
|
||||
<li>
|
||||
Under <strong>Registration Status</strong> select:
|
||||
<pre>Filtered - Anyone can apply (Curated Onboarding)</pre>
|
||||
</li>
|
||||
<li>Save the changes</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,102 @@
|
||||
@extends('admin.partial.template-full')
|
||||
|
||||
@section('section')
|
||||
</div><div class="header bg-primary pb-3 mt-n4">
|
||||
<div class="container-fluid">
|
||||
<div class="header-body">
|
||||
<div class="row align-items-center py-4">
|
||||
<div class="col-lg-8 col-12">
|
||||
<p class="mb-0">
|
||||
<a href="{{ route('admin.curated-onboarding')}}" class="text-white">
|
||||
<i class="far fa-chevron-left mr-1"></i> Back to Curated Onboarding
|
||||
</a>
|
||||
</p>
|
||||
<p class="display-3 text-white d-inline-block">Application #{{ $record->id }}</p>
|
||||
<div class="text-white mb-0 d-flex align-items-center" style="gap:1rem">
|
||||
@if($record->is_closed)
|
||||
@else
|
||||
<span class="font-weight-bold">
|
||||
<i class="far fa-circle mr-2"></i>
|
||||
Open / Awaiting Admin Action
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m-n2 m-lg-4">
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card border">
|
||||
<div class="card-header font-weight-bold bg-gradient-primary text-center text-white py-2">Details</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="list-group-item">
|
||||
<div>Username</div>
|
||||
<div>{{ $record->username }}</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div>Email</div>
|
||||
<div>{{ $record->email }}</div>
|
||||
</div>
|
||||
<div class="list-group-item">
|
||||
<div>Created</div>
|
||||
<div data-toggle="tooltip" title="{{ $record->created_at }}">{{ $record->created_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
@if($record->email_verified_at)
|
||||
<div class="list-group-item">
|
||||
<div>Email Verified</div>
|
||||
<div data-toggle="tooltip" title="{{ $record->email_verified_at }}">{{ $record->email_verified_at->diffForHumans() }}</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="list-group-item">
|
||||
<div>Email Verified</div>
|
||||
<div class="text-danger">Not yet</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border mt-3">
|
||||
<div class="card-header font-weight-bold bg-gradient-primary text-center text-white py-2">Reason for Joining</div>
|
||||
<div class="card-body">
|
||||
<blockquote>{{ $record->reason_to_join }}</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-8">
|
||||
@include('admin.curated-register.partials.activity-log', [
|
||||
'id' => $record->id,
|
||||
'is_closed' => $record->is_closed,
|
||||
'is_approved' => $record->is_approved,
|
||||
'is_rejected' => $record->is_rejected,
|
||||
'action_taken_at' => $record->action_taken_at,
|
||||
'email_verified_at' => $record->email_verified_at
|
||||
])
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
div:first-child {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,124 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 4])
|
||||
|
||||
@if ($errors->any())
|
||||
@foreach ($errors->all() as $error)
|
||||
<div class="alert alert-danger bg-danger border-danger text-white">
|
||||
<p class="lead font-weight-bold mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="mb-5"></div>
|
||||
@endif
|
||||
|
||||
@if($emailConfirmed)
|
||||
<h1 class="text-center font-weight-bold mt-4">Information Requested</h1>
|
||||
<p class="lead text-center"><span class="font-weight-light">Our admin team requests the following information from you:</span></p>
|
||||
<div class="border border-primary p-4 rounded my-3" style="border-style: dashed !important;">
|
||||
<p class="mb-0 lead">testing</p>
|
||||
</div>
|
||||
<hr class="border-dark">
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="action" value="message">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">Your Response</label>
|
||||
<textarea
|
||||
class="form-control bg-dark border-dark text-white"
|
||||
rows="4"
|
||||
id="reason"
|
||||
name="response"
|
||||
placeholder="Enter your response here, up to 1000 chars..."
|
||||
maxlength="1000"></textarea>
|
||||
<div class="help-text small text-muted d-flex justify-content-end mt-1 font-weight-bold">
|
||||
<span id="charCount" class="text-white">0</span>/<span>1000</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill px-5">Submit</button>
|
||||
</form>
|
||||
<hr class="border-dark">
|
||||
<p class="text-muted small text-center">For additional information, please see our <a href="{{ route('help.curated-onboarding') }}" style="font-weight: 600;">Curated Onboarding</a> Help Center page.</p>
|
||||
@else
|
||||
@include('auth.curated-register.partials.message-email-confirm', ['step' => 4])
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textInput = document.getElementById('reason');
|
||||
var charCount = document.getElementById('charCount');
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
|
||||
textInput.addEventListener('input', function () {
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,129 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 4])
|
||||
|
||||
@if ($errors->any())
|
||||
@foreach ($errors->all() as $error)
|
||||
<div class="alert alert-danger bg-danger border-danger text-white">
|
||||
<p class="lead font-weight-bold mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="mb-5"></div>
|
||||
@endif
|
||||
|
||||
<h1 class="text-center font-weight-bold mt-4 mb-3"><i class="far fa-info-circle mr-2"></i> Information Requested</h1>
|
||||
<p class="h5" style="line-height: 1.5;">Before we can process your application to join, our admin team have requested additional information from you. Please respond at your earliest convenience!</p>
|
||||
<hr class="border-dark">
|
||||
<p>From our Admins:</p>
|
||||
<div class="card card-body mb-1 bg-dark border border-secondary" style="border-style: dashed !important;">
|
||||
<p class="lead mb-0" style="white-space: pre; opacity: 0.8">{{ $activity->message }}</p>
|
||||
</div>
|
||||
<p class="mb-3 small text-muted">If you don't understand this request, or need additional context you should request clarification from the admin team.</p>
|
||||
{{-- <hr class="border-dark"> --}}
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="crid" value="{{ $activity->register_id }}">
|
||||
<input type="hidden" name="acid" value="{{ $activity->id }}">
|
||||
<div class="form-group">
|
||||
<label for="message">Your Response</label>
|
||||
<textarea
|
||||
class="form-control bg-dark border-dark text-white"
|
||||
rows="4"
|
||||
id="reason"
|
||||
name="response"
|
||||
placeholder="Enter your response here, up to 1000 chars..."
|
||||
maxlength="1000">{{ old('response') }}</textarea>
|
||||
<div class="help-text small text-muted d-flex justify-content-end mt-1 font-weight-bold">
|
||||
<span id="charCount" class="text-white">0</span>/<span>1000</span>
|
||||
</div>
|
||||
</div>
|
||||
@if($showCaptcha)
|
||||
<div class="d-flex justify-content-center my-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="text-center">
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill px-5">Submit my response</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr class="border-dark">
|
||||
<p class="text-muted small text-center">For additional information, please see our <a href="{{ route('help.curated-onboarding') }}" style="font-weight: 600;">Curated Onboarding</a> Help Center page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textInput = document.getElementById('reason');
|
||||
var charCount = document.getElementById('charCount');
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
|
||||
textInput.addEventListener('input', function () {
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,91 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 3])
|
||||
|
||||
@if ($errors->any())
|
||||
@foreach ($errors->all() as $error)
|
||||
<div class="alert alert-danger bg-danger border-danger text-white">
|
||||
<p class="lead font-weight-bold mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="mb-5"></div>
|
||||
@endif
|
||||
|
||||
<h1 class="text-center">Confirm Email</h1>
|
||||
<p class="lead text-center">Please confirm your email address so we can continue processing your registration application.</p>
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="sid" value={{request()->input('sid')}}>
|
||||
<input type="hidden" name="code" value={{request()->input('code')}}>
|
||||
@if(config('instance.curated_registration.captcha_enabled'))
|
||||
<div class="d-flex justify-content-center my-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="mt-3 pt-4">
|
||||
<button class="btn btn-primary rounded-pill font-weight-bold btn-block">Confirm Email Address</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,74 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 4])
|
||||
|
||||
<p class="text-center"><i class="fal fa-check-circle text-success fa-8x"></i></p>
|
||||
<h1 class="text-center font-weight-bold my-4">Email Confirmed!</h1>
|
||||
<p class="h4 text-center"><span class="font-weight-bold">Our admin team will review your application.</span></p>
|
||||
<hr class="border-dark">
|
||||
<p class="lead text-center">Most applications are processed within 24-48 hours. We will send you an email once your account is ready!</p>
|
||||
<p class="text-muted text-center">If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.</p>
|
||||
<hr class="border-dark">
|
||||
<p class="text-muted small text-center">For additional information, please see our <a href="{{ route('help.curated-onboarding') }}" style="font-weight: 600;">Curated Onboarding</a> Help Center page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,88 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => $step ?? 1])
|
||||
|
||||
@if ($errors->any())
|
||||
@foreach ($errors->all() as $error)
|
||||
<div class="alert alert-danger bg-danger border-danger text-white">
|
||||
<p class="lead font-weight-bold mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="mb-5"></div>
|
||||
@endif
|
||||
@if($step === 1)
|
||||
@include('auth.curated-register.partials.step-1')
|
||||
@elseif ($step === 2)
|
||||
@include('auth.curated-register.partials.step-2')
|
||||
@elseif ($step === 3)
|
||||
@include('auth.curated-register.partials.step-3')
|
||||
@elseif ($step === 4)
|
||||
@include('auth.curated-register.partials.step-4')
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,23 @@
|
||||
<p class="mt-5 lead text-center font-weight-bold">Please verify your email address</p>
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="action" value="email">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
name="email"
|
||||
placeholder="Your email address"
|
||||
required />
|
||||
</div>
|
||||
@if(config('instance.curated_registration.captcha_enabled'))
|
||||
<div class="d-flex justify-content-center my-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="d-flex justify-content-center">
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill px-5">Verify</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr class="border-dark">
|
||||
<p class="text-muted small text-center">For additional information, please see our <a href="{{ route('help.curated-onboarding') }}" style="font-weight: 600;">Curated Onboarding</a> Help Center page.</p>
|
@ -0,0 +1,134 @@
|
||||
<section class="step-wizard">
|
||||
<ul class="step-wizard-list">
|
||||
<li class="step-wizard-item {{ $step === 1 ? 'current-item':'' }}">
|
||||
<span class="progress-count">1</span>
|
||||
<span class="progress-label">Review Rules</span>
|
||||
</li>
|
||||
<li class="step-wizard-item {{ $step === 2 ? 'current-item':'' }}">
|
||||
<span class="progress-count">2</span>
|
||||
<span class="progress-label">Your Details</span>
|
||||
</li>
|
||||
<li class="step-wizard-item {{ $step === 3 ? 'current-item':'' }}">
|
||||
<span class="progress-count">3</span>
|
||||
<span class="progress-label">Confirm Email</span>
|
||||
</li>
|
||||
<li class="step-wizard-item {{ $step === 4 ? 'current-item':'' }}">
|
||||
<span class="progress-count">4</span>
|
||||
<span class="progress-label">Await Review</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
.step-wizard {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.step-wizard-list{
|
||||
background: transparent;
|
||||
color: #fff;
|
||||
list-style-type: none;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
padding: 20px 10px;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.step-wizard-item {
|
||||
padding: 0 10px;
|
||||
flex-basis: 0;
|
||||
-webkit-box-flex: 1;
|
||||
-ms-flex-positive:1;
|
||||
flex-grow: 1;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
min-width: 20px;
|
||||
position: relative;
|
||||
}
|
||||
@media (min-width: 600px) {
|
||||
.step-wizard-item {
|
||||
padding: 0 20px;
|
||||
min-width: 140px;
|
||||
}
|
||||
}
|
||||
.step-wizard-item + .step-wizard-item:after{
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 19px;
|
||||
background: var(--primary);
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
transform: translateX(-50%);
|
||||
z-index: -10;
|
||||
}
|
||||
.progress-count{
|
||||
height: 40px;
|
||||
width:40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-weight: 600;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index:10;
|
||||
color: transparent;
|
||||
}
|
||||
.progress-count:after{
|
||||
content: "";
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: var(--primary);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 50%;
|
||||
z-index: -10;
|
||||
}
|
||||
.progress-count:before{
|
||||
content: "";
|
||||
height: 10px;
|
||||
width: 20px;
|
||||
border-left: 3px solid #fff;
|
||||
border-bottom: 3px solid #fff;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -60%) rotate(-45deg);
|
||||
transform-origin: center center;
|
||||
}
|
||||
.progress-label{
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.current-item .progress-count:before,
|
||||
.current-item ~ .step-wizard-item .progress-count:before{
|
||||
display: none;
|
||||
}
|
||||
.current-item ~ .step-wizard-item .progress-count:after{
|
||||
height:10px;
|
||||
width:10px;
|
||||
}
|
||||
.current-item ~ .step-wizard-item .progress-label{
|
||||
opacity: 0.5;
|
||||
}
|
||||
.current-item .progress-count:after{
|
||||
background: #080e2b;
|
||||
border: 2px solid var(--primary);
|
||||
}
|
||||
.current-item .progress-count{
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,18 @@
|
||||
@php
|
||||
$rules = json_decode(config_cache('app.rules'), true)
|
||||
@endphp
|
||||
|
||||
<div class="list-group pt-4">
|
||||
@foreach($rules as $id => $rule)
|
||||
<div class="list-group-item bg-transparent border-dark d-flex align-items-center gap-1">
|
||||
<div style="display: block;width: 40px; height:40px;">
|
||||
<div class="border border-primary text-white font-weight-bold rounded-circle d-flex justify-content-center align-items-center" style="display: block;width: 40px; height:40px;">
|
||||
{{ $id + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-1">
|
||||
<p class="mb-0 flex-shrink-1 flex-wrap">{{ $rule }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
@ -0,0 +1,175 @@
|
||||
@php
|
||||
$id = str_random(14);
|
||||
@endphp
|
||||
<h1 class="text-center">Before you continue.</h1>
|
||||
@if(config_cache('app.rules') && strlen(config_cache('app.rules')) > 5)
|
||||
<p class="lead text-center">Let's go over a few basic guidelines established by the server's administrators.</p>
|
||||
|
||||
@include('auth.curated-register.partials.server-rules')
|
||||
@else
|
||||
<p class="lead text-center mt-4"><span class="opacity-5">The admins have not specified any community rules, however we suggest youreview the</span> <a href="/site/terms" target="_blank" class="text-white font-weight-bold">Terms of Use</a> <span class="opacity-5">and</span> <a href="/site/privacy" target="_blank" class="text-white font-weight-bold">Privacy Policy</a>.</p>
|
||||
@endif
|
||||
|
||||
<div class="action-btns">
|
||||
<form method="post" id="{{$id}}" class="flex-grow-1">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="1">
|
||||
<button type="button" class="btn btn-primary rounded-pill font-weight-bold btn-block flex-grow-1" onclick="onSubmit()">Accept</button>
|
||||
</form>
|
||||
|
||||
<a class="btn btn-outline-muted rounded-pill" href="/">Go back</a>
|
||||
</div>
|
||||
|
||||
<div class="small-links">
|
||||
<a href="/login">Login</a>
|
||||
<span>·</span>
|
||||
<a href="/auth/sign_up/resend-confirmation">Re-send confirmation</a>
|
||||
<span>·</span>
|
||||
<a href="{{route('help.curated-onboarding')}}" target="_blank">Help</a>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
function onSubmit() {
|
||||
@if ($errors->any())
|
||||
document.getElementById('{{$id}}').submit();
|
||||
return;
|
||||
@endif
|
||||
swal({
|
||||
text: "Please select the region you are located in",
|
||||
icon: "info",
|
||||
buttons: {
|
||||
cancel: false,
|
||||
usa: {
|
||||
text: "United States",
|
||||
className: "swal-button--cancel",
|
||||
value: "usa"
|
||||
},
|
||||
uk: {
|
||||
text: "UK",
|
||||
className: "swal-button--cancel",
|
||||
value: "uk"
|
||||
},
|
||||
eu: {
|
||||
text: "EU",
|
||||
className: "swal-button--cancel",
|
||||
value: "eu"
|
||||
},
|
||||
other: {
|
||||
text: "Other",
|
||||
className: "swal-button--cancel",
|
||||
value: "other"
|
||||
}
|
||||
},
|
||||
dangerMode: false,
|
||||
}).then((region) => {
|
||||
handleRegion(region);
|
||||
})
|
||||
}
|
||||
|
||||
function handleRegion(region) {
|
||||
if(!region) {
|
||||
return;
|
||||
}
|
||||
let minAge = 16;
|
||||
if(['usa', 'uk', 'other'].includes(region)) {
|
||||
minAge = 13;
|
||||
}
|
||||
swal({
|
||||
title: "Enter Your Date of Birth",
|
||||
text: "We require your birthdate solely to confirm that you meet our age requirement.\n\n Rest assured, this information is not stored or used for any other purpose.",
|
||||
content: {
|
||||
element: "input",
|
||||
attributes: {
|
||||
placeholder: "Enter your birthday in YYYY-MM-DD format",
|
||||
type: "date",
|
||||
min: '1900-01-01',
|
||||
max: getToday(),
|
||||
pattern: "\d{4}-\d{2}-\d{2}",
|
||||
required: true
|
||||
}
|
||||
},
|
||||
buttons: {
|
||||
cancel: false,
|
||||
confirm: {
|
||||
text: 'Confirm Birthdate',
|
||||
className: "swal-button--cancel",
|
||||
}
|
||||
}
|
||||
}).then((inputValue) => {
|
||||
if (inputValue === false) return;
|
||||
|
||||
if (inputValue === "") {
|
||||
swal("Oops!", "You need to provide your date of birth to proceed!", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
const dob = new Date(inputValue);
|
||||
if (isValidDate(dob)) {
|
||||
const age = calculateAge(dob);
|
||||
// swal(`Your age is ${age}`);
|
||||
if(age >= 120 || age <= 5) {
|
||||
swal({
|
||||
title: "Ineligible to join",
|
||||
text: "Sorry, the birth date you provided is not valid and we cannot process your request at this time.\n\nIf you entered your birthdate incorrectly you can try again, otherwise if you attempt to bypass our minimum age requirements, your account may be suspended.",
|
||||
icon: "error",
|
||||
buttons: {
|
||||
cancel: "I understand"
|
||||
}
|
||||
}).then(() => {
|
||||
window.location.href = '/'
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (age >= minAge) {
|
||||
document.getElementById('{{$id}}').submit();
|
||||
} else {
|
||||
swal({
|
||||
title: "Ineligible to join",
|
||||
text: `Sorry, you must be at least ${minAge} years old to join our service according to the laws of your country or region.`,
|
||||
icon: "error",
|
||||
buttons: {
|
||||
cancel: "I understand"
|
||||
}
|
||||
}).then(() => {
|
||||
window.location.href = '/'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
swal("Invalid date format!");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function calculateAge(dob) {
|
||||
const diff_ms = Date.now() - dob.getTime();
|
||||
console.log(diff_ms);
|
||||
const age_dt = new Date(diff_ms);
|
||||
return Math.abs(age_dt.getUTCFullYear() - 1970);
|
||||
}
|
||||
|
||||
function getToday() {
|
||||
var today = new Date();
|
||||
var dd = today.getDate();
|
||||
var mm = today.getMonth() + 1;
|
||||
var yyyy = today.getFullYear();
|
||||
|
||||
if (dd < 10) {
|
||||
dd = '0' + dd;
|
||||
}
|
||||
|
||||
if (mm < 10) {
|
||||
mm = '0' + mm;
|
||||
}
|
||||
|
||||
yyyy = yyyy - 10;
|
||||
|
||||
return yyyy + '-' + mm + '-' + dd;
|
||||
}
|
||||
|
||||
function isValidDate(d) {
|
||||
return d instanceof Date && !isNaN(d);
|
||||
}
|
||||
</script>
|
||||
@endpush
|
@ -0,0 +1,117 @@
|
||||
<h2><span style="opacity:0.5;">Let's begin setting up your account on</span> <strong style="opacity: 1">{{ config('pixelfed.domain.app') }}</strong></h2>
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="2">
|
||||
<div class="my-5 details-form">
|
||||
<div class="details-form-field">
|
||||
<label class="text-muted small font-weight-bold mb-0">Username</label>
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
placeholder="username"
|
||||
aria-label="Your username"
|
||||
aria-describedby="username-addon"
|
||||
maxlength="15"
|
||||
required
|
||||
name="username"
|
||||
value="{{ request()->session()->get('cur-reg.form-username') }}">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text bg-dark border-dark text-muted font-weight-bold" id="username-addon">@{{ config('pixelfed.domain.app') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-text small text-muted mb-0">You can use letters, numbers, and underscores with a max length of 15 chars.</p>
|
||||
</div>
|
||||
|
||||
<div class="details-form-field">
|
||||
<label class="text-muted small font-weight-bold mb-0">Email</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
placeholder="Your email address"
|
||||
name="email"
|
||||
value="{{ request()->session()->get('cur-reg.form-email') }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="details-form-field">
|
||||
<label class="text-muted small font-weight-bold mb-0">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="6"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
placeholder="Password"
|
||||
name="password"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="details-form-field">
|
||||
<label class="text-muted small font-weight-bold mb-0">Password Confirm</label>
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
minlength="6"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
placeholder="Confirm Password"
|
||||
name="password_confirmation"
|
||||
required>
|
||||
</div>
|
||||
<div class="border-top border-dark mt-3 pt-4">
|
||||
<p class="lead">
|
||||
Our moderators manually review sign-ups. To assist in the processing of your registration, please provide some information about yourself and explain why you wish to create an account on {{ config('pixelfed.domain.app') }}.
|
||||
</p>
|
||||
</div>
|
||||
<div class="details-form-field">
|
||||
<label class="text-muted small font-weight-bold mb-0">About yourself and why you'd like to join</label>
|
||||
<textarea
|
||||
class="form-control form-control-lg bg-dark text-white border-dark"
|
||||
rows="4"
|
||||
name="reason"
|
||||
maxlength="1000"
|
||||
id="reason"
|
||||
placeholder="Briefly explain why you'd like to join and optionally provide links to other accounts to help admins process your request"
|
||||
required>{{ request()->session()->get('cur-reg.form-reason') }}</textarea>
|
||||
<div class="help-text small text-muted float-right mt-1 font-weight-bold">
|
||||
<span id="charCount" class="text-white">0</span>/<span>1000</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-top border-dark mt-3 pt-4">
|
||||
<div class="details-form-field">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="agree" name="agree" required>
|
||||
<label class="custom-control-label text-muted" for="agree">I have read and agreed to our <a href="/site/terms" target="_blank" class="text-white">Terms of Use</a> and <a href="/site/privacy" target="_blank" class="text-white">Privacy Policy</a>.</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 pt-4">
|
||||
<button class="btn btn-primary rounded-pill font-weight-bold btn-block">Proceed</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var textInput = document.getElementById('reason');
|
||||
var charCount = document.getElementById('charCount');
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
|
||||
textInput.addEventListener('input', function () {
|
||||
var currentLength = textInput.value.length;
|
||||
charCount.textContent = currentLength;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@push('styles')
|
||||
<style type="text/css">
|
||||
.details-form {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,30 @@
|
||||
<h2><span style="opacity:0.5;">Confirm Your Email</h2>
|
||||
@if(isset($verifiedEmail))
|
||||
<div class="alert alert-success bg-success border-success p-4 text-center mt-5">
|
||||
<p class="text-center text-white mb-4"><i class="far fa-envelope-open fa-4x"></i></p>
|
||||
<p class="lead font-weight-bold text-white mb-0">Please check your email inbox, we sent an email confirmation with a link that you need to visit.</p>
|
||||
</div>
|
||||
@else
|
||||
<p class="lead">Please confirm your email address is correct, we will send a verification e-mail with a special verification link that you need to visit before proceeding.</p>
|
||||
<form method="post">
|
||||
@csrf
|
||||
<input type="hidden" name="step" value="3">
|
||||
<div class="details-form-field">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
placeholder="Your email address"
|
||||
name="email"
|
||||
value="{{ request()->session()->get('cur-reg.form-email') }}"
|
||||
required>
|
||||
</div>
|
||||
@if(config('instance.curated_registration.captcha_enabled'))
|
||||
<div class="d-flex justify-content-center my-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="mt-3 pt-4">
|
||||
<button class="btn btn-primary rounded-pill font-weight-bold btn-block">My email is correct</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
@ -0,0 +1,2 @@
|
||||
<h2><span>Processing your membership request</span></h2>
|
||||
<p class="lead">We will send you an email once your account is ready!</p>
|
@ -0,0 +1,112 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 3])
|
||||
|
||||
@if ($errors->any())
|
||||
@foreach ($errors->all() as $error)
|
||||
<div class="alert alert-danger bg-danger border-danger text-white">
|
||||
<p class="lead font-weight-bold mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {!! $error !!}</p>
|
||||
</div>
|
||||
@endforeach
|
||||
<div class="mb-5"></div>
|
||||
@endif
|
||||
|
||||
<h1 class="text-center">Resend Confirmation</h1>
|
||||
<p class="lead text-center">Please confirm your email address so we verify your registration application to re-send your email verification email.</p>
|
||||
|
||||
<form method="post">
|
||||
@csrf
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="email"
|
||||
class="form-control form-control-lg bg-dark border-dark text-white"
|
||||
name="email"
|
||||
placeholder="Your email address"
|
||||
required />
|
||||
</div>
|
||||
@if(config('instance.curated_registration.captcha_enabled'))
|
||||
<div class="d-flex justify-content-center my-3">
|
||||
{!! Captcha::display() !!}
|
||||
</div>
|
||||
@endif
|
||||
<div class="d-flex justify-content-center">
|
||||
<button class="btn btn-primary font-weight-bold rounded-pill px-5">Verify</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="col-12 col-md-7 mt-5">
|
||||
<div class="small-links">
|
||||
<a href="/">Home</a>
|
||||
<span>·</span>
|
||||
<a href="/login">Login</a>
|
||||
<span>·</span>
|
||||
<a href="{{route('help.curated-onboarding')}}" target="_blank">Help</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,87 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 3])
|
||||
|
||||
<div class="alert alert-muted bg-transparent border-muted p-4 text-center mt-5">
|
||||
<p class="text-center text-success mb-4"><i class="far fa-envelope-open fa-4x"></i></p>
|
||||
<p class="h2 font-weight-bold text-white">Please check your email inbox</p>
|
||||
<hr style="opacity: 0.2">
|
||||
<p class="lead text-white">We sent a confirmation link to your email that you need to verify before we can process your registration application.</p>
|
||||
<p class="text-muted mb-0">The verification link expires after 24 hours.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-7 mt-5">
|
||||
<div class="small-links">
|
||||
<a href="/">Home</a>
|
||||
<span>·</span>
|
||||
<a href="/login">Login</a>
|
||||
<span>·</span>
|
||||
<a href="{{route('help.curated-onboarding')}}" target="_blank">Help</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,79 @@
|
||||
@extends('layouts.blank')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="col-12 col-md-7">
|
||||
<div class="logo">
|
||||
<img src="/img/pixelfed-icon-color.svg" width="40" height="40" alt="Pixelfed Logo">
|
||||
<p class="font-weight-bold mb-0">Pixelfed</p>
|
||||
</div>
|
||||
|
||||
@include('auth.curated-register.partials.progress-bar', ['step' => 4])
|
||||
|
||||
<p class="text-center"><i class="fal fa-check-circle text-success fa-8x"></i></p>
|
||||
<h1 class="text-center font-weight-bold my-4">Succesfully Sent Response!</h1>
|
||||
<p class="h4 text-center"><span class="font-weight-bold">Our admin team will review your application.</span></p>
|
||||
<hr class="border-dark">
|
||||
<p class="lead text-center">Most applications are processed within 24-48 hours. We will send you an email once your account is ready!</p>
|
||||
<p class="text-muted text-center">If we need any additional information, we will send you an automated request with a link that you can visit and provide further details to help process your application.</p>
|
||||
<hr class="border-dark">
|
||||
<p class="text-muted small text-center">For additional information, please see our <a href="{{ route('help.curated-onboarding') }}" style="font-weight: 600;">Curated Onboarding</a> Help Center page.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@push('styles')
|
||||
<link href="{{ mix('css/landing.css') }}" rel="stylesheet">
|
||||
<style type="text/css">
|
||||
.gap-1 {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.opacity-5 {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
margin-top: 50px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
p {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
margin: 3rem auto 1rem auto;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
|
||||
form {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.small-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
margin-bottom: 5rem;
|
||||
gap: 1rem;
|
||||
|
||||
a {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@endpush
|
@ -0,0 +1,34 @@
|
||||
@component('mail::message')
|
||||
# [#{{$verify->id}}] New Curated Onboarding Application
|
||||
|
||||
Hello admin,
|
||||
|
||||
**Please review this new onboarding application.**
|
||||
|
||||
<x-mail::panel>
|
||||
<p>
|
||||
<small>
|
||||
Username: <strong>{{ $verify->username }}</strong>
|
||||
</small>
|
||||
<br>
|
||||
<small>
|
||||
Email: <strong>{{ $verify->email }}</strong>
|
||||
</small>
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<small><strong>*The user provided the following reason to join:*</strong></small>
|
||||
<p style="font-size:9pt;">{!!str_limit(nl2br($verify->reason_to_join), 300)!!}</p>
|
||||
</x-mail::panel>
|
||||
|
||||
<x-mail::button :url="$verify->adminReviewUrl()" color="success">
|
||||
<strong>Review Onboarding Application</strong>
|
||||
</x-mail::button>
|
||||
|
||||
Thanks,<br>
|
||||
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
|
||||
<br>
|
||||
<hr>
|
||||
<p style="font-size:10pt;">This is an automated message, please be aware that replies to this email cannot be monitored or responded to.</p>
|
||||
@endcomponent
|
@ -0,0 +1,21 @@
|
||||
@component('mail::message')
|
||||
# New Curated Onboarding Response ({{ '#' . $activity->id}})
|
||||
|
||||
Hello,
|
||||
|
||||
You have a new response from a curated onboarding application from **{{$activity->application->email}}**.
|
||||
|
||||
<x-mail::panel>
|
||||
<p style="white-space: pre-wrap;">{!! $activity->message !!}</p>
|
||||
</x-mail::panel>
|
||||
|
||||
<x-mail::button :url="$activity->adminReviewUrl()" color="success">
|
||||
<strong>Review Onboarding Response</strong>
|
||||
</x-mail::button>
|
||||
|
||||
Thanks,<br>
|
||||
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
|
||||
<br>
|
||||
<hr>
|
||||
<p style="font-size:10pt;">This is an automated message, please be aware that replies to this email cannot be monitored or responded to.</p>
|
||||
@endcomponent
|
@ -0,0 +1,21 @@
|
||||
@component('mail::message')
|
||||
# Action Needed: Confirm Your Email to Activate Your Pixelfed Account
|
||||
|
||||
Hello **{{'@'.$verify->username}}**,
|
||||
|
||||
Please confirm your email address so we can process your new registration application.
|
||||
|
||||
<x-mail::button :url="$verify->emailConfirmUrl()" color="success">
|
||||
<strong>Confirm Email Address</strong>
|
||||
</x-mail::button>
|
||||
|
||||
|
||||
<p style="font-size:10pt;">If you did not create this account, please disregard this email. This link expires after 24 hours.</p>
|
||||
<br>
|
||||
|
||||
Thanks,<br>
|
||||
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
|
||||
<br>
|
||||
<hr>
|
||||
<p style="font-size:10pt;">This is an automated message, please be aware that replies to this email cannot be monitored or responded to.</p>
|
||||
@endcomponent
|
@ -0,0 +1,21 @@
|
||||
@component('mail::message')
|
||||
# New Message from {{config('pixelfed.domain.app')}}
|
||||
|
||||
Hello,
|
||||
|
||||
You recently applied to join our Pixelfed community using the @**{{ $verify->username }}** username.
|
||||
|
||||
The admins have a message for you:
|
||||
|
||||
<x-mail::panel>
|
||||
<p style="white-space: pre-wrap;">{{ $verify->message }}</p>
|
||||
</x-mail::panel>
|
||||
|
||||
Please do not respond to this email, any replies will not be seen by our admin team.
|
||||
|
||||
Thanks,<br>
|
||||
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
|
||||
<br>
|
||||
<hr>
|
||||
<p style="font-size:10pt;">This is an automated message on behalf of our admin team, please be aware that replies to this email cannot be monitored or responded to.</p>
|
||||
@endcomponent
|
@ -0,0 +1,22 @@
|
||||
@component('mail::message')
|
||||
# Action Needed: Additional information requested
|
||||
|
||||
Hello **{{'@'.$verify->username}}**
|
||||
|
||||
To help us process your registration application, we require more information.
|
||||
|
||||
Our onboarding team have requested the following details:
|
||||
|
||||
@component('mail::panel')
|
||||
<p style="white-space: pre-wrap;">{!! $activity->message !!}</p>
|
||||
@endcomponent
|
||||
<x-mail::button :url="$activity->emailReplyUrl()" color="success">
|
||||
<strong>Reply with your response</strong>
|
||||
</x-mail::button>
|
||||
|
||||
<p style="font-size:10pt;">Please respond promptly, your application will be automatically removed 7 days after your last interaction.</p>
|
||||
<br>
|
||||
|
||||
Thanks,<br>
|
||||
<a href="{{ config('app.url') }}">{{ config('pixelfed.domain.app') }}</a>
|
||||
@endcomponent
|
Loading…
Reference in New Issue