Set the baseURL for ajax requests (#373)
* Set the baseURL for ajax requests * Use async/await on AJAX calls * Add mix_public() cache generated asset cache busting * Move storage container into separate class * Fix some styling
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Exceptions\SettingNotFound;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
|
||||
if (!function_exists('in_mask')) {
|
||||
@@ -152,14 +153,29 @@ if (!function_exists('setting')) {
|
||||
* set
|
||||
*/
|
||||
if (!function_exists('public_asset')) {
|
||||
function public_asset($path, array $parameters = [], $secure = null)
|
||||
function public_asset($path, array $parameters = [])
|
||||
{
|
||||
$publicBaseUrl = app()->publicUrlPath();
|
||||
$path = $publicBaseUrl.$path;
|
||||
|
||||
$path = str_replace('//', '/', $path);
|
||||
|
||||
return url($path, $parameters, $secure);
|
||||
return url($path, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Call mix() and then prepend the proper public URL
|
||||
*/
|
||||
if (!function_exists('mix_public')) {
|
||||
function mix_public($path, array $parameters = [])
|
||||
{
|
||||
try {
|
||||
$path = mix($path);
|
||||
} catch (Exception $e) {
|
||||
}
|
||||
|
||||
return public_asset($path, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,11 +187,11 @@ if (!function_exists('show_datetime')) {
|
||||
* Format the a Carbon date into the datetime string
|
||||
* but convert it into the user's timezone
|
||||
*
|
||||
* @param \Carbon\Carbon $date
|
||||
* @param Carbon $date
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function show_datetime(\Carbon\Carbon $date = null)
|
||||
function show_datetime(Carbon $date = null)
|
||||
{
|
||||
if ($date === null) {
|
||||
return '-';
|
||||
@@ -202,7 +218,7 @@ if (!function_exists('show_date')) {
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function show_date(\Carbon\Carbon $date)
|
||||
function show_date(Carbon $date)
|
||||
{
|
||||
$timezone = 'UTC';
|
||||
if (Auth::check()) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"/assets/frontend/js/app.js": "/assets/frontend/js/app.js?id=3d3acfdac1df77a540f0",
|
||||
"/assets/frontend/js/app.js": "/assets/frontend/js/app.js?id=4ef5a26ff476bb0a9fcf",
|
||||
"/assets/frontend/css/now-ui-kit.css": "/assets/frontend/css/now-ui-kit.css?id=c4987da93365a82d32b6",
|
||||
"/assets/admin/css/vendor.min.css": "/assets/admin/css/vendor.min.css?id=da87041e81048759bd41",
|
||||
"/assets/admin/js/app.js": "/assets/admin/js/app.js?id=5801ddaddcb650cc967c",
|
||||
"/assets/installer/js/app.js": "/assets/installer/js/app.js?id=38a4df797a9982e20988",
|
||||
"/assets/admin/js/app.js": "/assets/admin/js/app.js?id=918b9ff17affe36bab4c",
|
||||
"/assets/installer/js/app.js": "/assets/installer/js/app.js?id=964075bbd7379552bc05",
|
||||
"/assets/fonts/glyphicons-halflings-regular.woff2": "/assets/fonts/glyphicons-halflings-regular.woff2?id=349344e92fb16221dd56",
|
||||
"/assets/admin/fonts/glyphicons-halflings-regular.woff2": "/assets/admin/fonts/glyphicons-halflings-regular.woff2?id=349344e92fb16221dd56",
|
||||
"/assets/admin/img/clear.png": "/assets/admin/img/clear.png?id=63b3af84650a0145d61a",
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Lookup an airport from the server
|
||||
* @param icao
|
||||
* @param callback
|
||||
*
|
||||
* @param {String} icao
|
||||
*/
|
||||
export default (icao, callback) => {
|
||||
export default async (icao) => {
|
||||
let params = {
|
||||
method: 'GET',
|
||||
url: '/api/airports/' + icao + '/lookup',
|
||||
};
|
||||
|
||||
console.log('Looking airport up');
|
||||
axios(params)
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
callback(response.data);
|
||||
});
|
||||
const response = await axios(params);
|
||||
console.log('lookup raw response: ', response);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/**
|
||||
* Lookup an airport from the server
|
||||
* @param fromICAO
|
||||
* @param toICAO
|
||||
* @param callback
|
||||
*
|
||||
* @param {String} fromICAO
|
||||
* @param {String} toICAO
|
||||
*/
|
||||
export default (fromICAO, toICAO, callback) => {
|
||||
export default async (fromICAO, toICAO) => {
|
||||
let params = {
|
||||
method: 'GET',
|
||||
url: '/api/airports/' + fromICAO + '/distance/' + toICAO,
|
||||
};
|
||||
|
||||
console.log('Calcuating airport distance');
|
||||
axios(params)
|
||||
.then(response => {
|
||||
console.log(response);
|
||||
callback(response.data);
|
||||
});
|
||||
const response = await axios(params);
|
||||
console.log('distance raw response: ', response);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
18
resources/js/bootstrap.js
vendored
18
resources/js/bootstrap.js
vendored
@@ -2,12 +2,18 @@
|
||||
* Bootstrap any Javascript libraries required
|
||||
*/
|
||||
|
||||
|
||||
window.axios = require('axios');
|
||||
|
||||
import Storage from "./storage";
|
||||
|
||||
/**
|
||||
* Container for phpVMS specific functions
|
||||
*/
|
||||
window.phpvms = {};
|
||||
window.phpvms = {
|
||||
config: {},
|
||||
Storage,
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure Axios with both the csrf token and the API key
|
||||
@@ -15,17 +21,17 @@ window.phpvms = {};
|
||||
|
||||
const base_url = document.head.querySelector('meta[name="base-url"]');
|
||||
if(base_url) {
|
||||
window.axios.default.baseURL = base_url;
|
||||
console.log(`baseURL=${base_url.content}`);
|
||||
window.phpvms.config.base_url = base_url.content;
|
||||
window.axios.default.baseURL = base_url.content;
|
||||
}
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
const token = document.head.querySelector('meta[name="csrf-token"]');
|
||||
|
||||
if (token) {
|
||||
window.phpvms.config.csrf_token = token.content;
|
||||
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content
|
||||
/*window.jquery.ajaxSetup({
|
||||
'X-CSRF-TOKEN': token.content
|
||||
})*/
|
||||
} else {
|
||||
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token')
|
||||
}
|
||||
@@ -33,8 +39,10 @@ if (token) {
|
||||
const api_key = document.head.querySelector('meta[name="api-key"]');
|
||||
if (api_key) {
|
||||
window.axios.defaults.headers.common['x-api-key'] = api_key.content;
|
||||
window.phpvms.config.user_api_key = api_key.content;
|
||||
window.PHPVMS_USER_API_KEY = api_key.content
|
||||
} else {
|
||||
window.phpvms.config.user_api_key = false;
|
||||
window.PHPVMS_USER_API_KEY = false;
|
||||
console.error('API Key not found!')
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
|
||||
require('./../bootstrap');
|
||||
|
||||
// Import the bids functionality
|
||||
import {addBid, removeBid} from './bids';
|
||||
window.phpvms.bids = {
|
||||
addBid,
|
||||
removeBid,
|
||||
};
|
||||
|
||||
// Import the mapping function
|
||||
window.phpvms.map = require('../maps/index');
|
||||
|
||||
41
resources/js/frontend/bids.js
Normal file
41
resources/js/frontend/bids.js
Normal file
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Add a bid to a flight
|
||||
*
|
||||
* @param {String} flight_id
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function addBid(flight_id) {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: '/api/user/bids',
|
||||
data: {
|
||||
'_method': 'POST',
|
||||
'flight_id': flight_id
|
||||
}
|
||||
};
|
||||
|
||||
return axios(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a bid from a given flight
|
||||
*
|
||||
* @param {String} flight_id
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function removeBid(flight_id) {
|
||||
const params = {
|
||||
method: 'POST',
|
||||
url: '/api/user/bids',
|
||||
data: {
|
||||
'_method': 'DELETE',
|
||||
'flight_id': flight_id
|
||||
}
|
||||
};
|
||||
|
||||
return axios(params);
|
||||
}
|
||||
77
resources/js/storage.js
Normal file
77
resources/js/storage.js
Normal file
@@ -0,0 +1,77 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Simple browser storage interface
|
||||
*/
|
||||
export default class Storage {
|
||||
constructor(name, default_value) {
|
||||
this.name = name;
|
||||
|
||||
// Read the object from storage; if it doesn't exist, set
|
||||
// it to the default value
|
||||
const st = window.localStorage.getItem(this.name);
|
||||
if (!st) {
|
||||
console.log('Nothing found in storage, starting from default');
|
||||
this.data = default_value;
|
||||
} else {
|
||||
console.log('Found in storage: ', st);
|
||||
this.data = JSON.parse(st);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save to local storage
|
||||
*/
|
||||
save() {
|
||||
window.localStorage.setItem(this.name, JSON.stringify(this.data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list from a given key
|
||||
*
|
||||
* @param {String} key
|
||||
*
|
||||
* @returns {Array|*}
|
||||
*/
|
||||
getList(key) {
|
||||
if (!(key in this.data)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.data[key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add `value` to a given `key`
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {*} value
|
||||
*/
|
||||
addToList(key, value) {
|
||||
if (!(key in this.data)) {
|
||||
this.data[key] = [];
|
||||
}
|
||||
|
||||
const index = this.data[key].indexOf(value);
|
||||
if (index === -1) {
|
||||
this.data[key].push(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `value` from the given `key`
|
||||
*
|
||||
* @param {String} key
|
||||
* @param {*} value
|
||||
*/
|
||||
removeFromList(key, value) {
|
||||
if (!(key in this.data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.data[key].indexOf(value);
|
||||
if (index !== -1) {
|
||||
this.data[key].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
@section('scripts')
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
function setEditable() {
|
||||
const csrf_token = $('meta[name="csrf-token"]').attr('content');
|
||||
const api_key = $('meta[name="api-key"]').attr('content');
|
||||
@@ -81,26 +83,32 @@ $(document).ready(function() {
|
||||
}
|
||||
});
|
||||
|
||||
$('a.airport_data_lookup').click(function(e) {
|
||||
$('a.airport_data_lookup').click(async function(e) {
|
||||
e.preventDefault();
|
||||
const icao = $("input#airport_icao").val();
|
||||
if(icao === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
phpvms.airport_lookup(icao, function(response) {
|
||||
_.forEach(response.data, function(value, key) {
|
||||
if(key === 'city') {
|
||||
key = 'location';
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await phpvms.airport_lookup(icao);
|
||||
} catch (e) {
|
||||
console.log('Error looking up airport!', e);
|
||||
return;
|
||||
}
|
||||
|
||||
$("#" + key).val(value);
|
||||
_.forEach(response.data, function (value, key) {
|
||||
if (key === 'city') {
|
||||
key = 'location';
|
||||
}
|
||||
|
||||
if(key === 'tz') {
|
||||
$("#timezone").val(value);
|
||||
$("#timezone").trigger('change');
|
||||
}
|
||||
});
|
||||
$("#" + key).val(value);
|
||||
|
||||
if (key === 'tz') {
|
||||
$("#timezone").val(value);
|
||||
$("#timezone").trigger('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
<link href='https://fonts.googleapis.com/css?family=Muli:400,300' rel='stylesheet' type='text/css'/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700,300" rel="stylesheet" type="text/css"/>
|
||||
|
||||
<link rel="stylesheet" href="{{ public_asset('/assets/global/css/vendor.css') }}"/>
|
||||
<link rel="stylesheet" href="{{ public_asset('/assets/admin/css/vendor.css') }}"/>
|
||||
<link rel="stylesheet" href="{{ public_asset('/assets/admin/css/admin.css') }}"/>
|
||||
<link rel="stylesheet" href="{{ mix_public('/assets/global/css/vendor.css') }}"/>
|
||||
<link rel="stylesheet" href="{{ mix_public('/assets/admin/css/vendor.css') }}"/>
|
||||
<link rel="stylesheet" href="{{ mix_public('/assets/admin/css/admin.css') }}"/>
|
||||
|
||||
<style type="text/css">
|
||||
@yield('css')
|
||||
@@ -91,47 +91,10 @@
|
||||
</body>
|
||||
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
|
||||
<script defer src="{{ public_asset('/assets/admin/js/vendor.js') }}"></script>
|
||||
<script defer src="{{ public_asset('/assets/admin/js/app.js') }}"></script>
|
||||
<script defer src="{{ mix_public('/assets/admin/js/vendor.js') }}"></script>
|
||||
<script defer src="{{ mix_public('/assets/admin/js/app.js') }}"></script>
|
||||
|
||||
<script>
|
||||
const getStorage = function(key) {
|
||||
const st = window.localStorage.getItem(key);
|
||||
// console.log('storage: ', key, st);
|
||||
if(!st) {
|
||||
return {
|
||||
"menu": [],
|
||||
};
|
||||
}
|
||||
|
||||
return JSON.parse(st);
|
||||
};
|
||||
|
||||
const saveStorage = function(key, obj) {
|
||||
// console.log('save: ', key, obj);
|
||||
window.localStorage.setItem(key, JSON.stringify(obj));
|
||||
};
|
||||
|
||||
const addItem = function(obj, item) {
|
||||
if (!obj) { obj = []; }
|
||||
const index = obj.indexOf(item);
|
||||
if(index === -1) {
|
||||
obj.push(item);
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
const removeItem = function (obj, item) {
|
||||
if (!obj) { obj = []; }
|
||||
const index = obj.indexOf(item);
|
||||
if (index !== -1) {
|
||||
obj.splice(index, 1);
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize any plugins on the page
|
||||
*/
|
||||
@@ -144,14 +107,17 @@ const initPlugins = () => {
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
initPlugins();
|
||||
|
||||
let storage = getStorage('phpvms.admin');
|
||||
//let storage = getStorage('phpvms.admin');
|
||||
const storage = new phpvms.Storage('phpvms.admin', {
|
||||
"menu": [],
|
||||
});
|
||||
|
||||
// see what menu items should be open
|
||||
for(let idx = 0; idx < storage.menu.length; idx++) {
|
||||
const id = storage.menu[idx];
|
||||
const menu = storage.getList('menu');
|
||||
for (const id of menu) {
|
||||
console.log('found '+id);
|
||||
const elem = $(".collapse#" + id);
|
||||
elem.addClass("in").trigger("show.bs.collapse");
|
||||
|
||||
@@ -160,26 +126,26 @@ $(document).ready(function () {
|
||||
caret.removeClass("pe-7s-angle-right");
|
||||
}
|
||||
|
||||
$(".collapse").on("hide.bs.collapse", function () {
|
||||
// console.log('hiding');
|
||||
$(".collapse").on("hide.bs.collapse", function() {
|
||||
const id = $(this).attr('id');
|
||||
const elem = $("a." + id + " b");
|
||||
elem.removeClass("pe-7s-angle-down");
|
||||
elem.addClass("pe-7s-angle-right");
|
||||
|
||||
removeItem(storage.menu, id);
|
||||
saveStorage("phpvms.admin", storage);
|
||||
// console.log('hiding ' + id);
|
||||
storage.removeFromList('menu', id);
|
||||
storage.save();
|
||||
});
|
||||
|
||||
$(".collapse").on("show.bs.collapse", function () {
|
||||
// console.log('showing');
|
||||
$(".collapse").on("show.bs.collapse", function() {
|
||||
const id = $(this).attr('id');
|
||||
const caret = $("a." + id + " b");
|
||||
caret.addClass("pe-7s-angle-down");
|
||||
caret.removeClass("pe-7s-angle-right");
|
||||
|
||||
addItem(storage.menu, id);
|
||||
saveStorage("phpvms.admin", storage);
|
||||
// console.log('showing ' + id);
|
||||
storage.addToList('menu', id);
|
||||
storage.save();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -96,7 +96,7 @@ $(document).ready(function () {
|
||||
setFieldsEditable();
|
||||
});
|
||||
|
||||
$('a.airport_distance_lookup').click(function (e) {
|
||||
$('a.airport_distance_lookup').click(async function (e) {
|
||||
e.preventDefault();
|
||||
const fromIcao = $("select#dpt_airport_id option:selected").val();
|
||||
const toIcao = $("select#arr_airport_id option:selected").val();
|
||||
@@ -105,10 +105,16 @@ $(document).ready(function () {
|
||||
}
|
||||
|
||||
console.log(`Calculating from ${fromIcao} to ${toIcao}`);
|
||||
phpvms.calculate_distance(fromIcao, toIcao, function (response) {
|
||||
console.log('Calculate distance:', response);
|
||||
$("#distance").val(response.data.distance.nmi);
|
||||
});
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await phpvms.calculate_distance(fromIcao, toIcao);
|
||||
} catch (e) {
|
||||
console.log('Error calculating distance:', e);
|
||||
return;
|
||||
}
|
||||
|
||||
$("#distance").val(response.data.distance.nmi);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
<meta content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0, shrink-to-fit=no' name='viewport'/>
|
||||
|
||||
<title>@yield('title') - {{ config('app.name') }}</title>
|
||||
|
||||
{{-- Start of required lines block. DON'T REMOVE THESE LINES! They're required or might break things --}}
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="api-key" content="{{ Auth::check() ? Auth::user()->api_key: '' }}">
|
||||
<meta name="base-url" content="{!! url('') !!}">
|
||||
<meta name="api-key" content="{!! Auth::check() ? Auth::user()->api_key: '' !!}">
|
||||
<meta name="csrf-token" content="{!! csrf_token() !!}">
|
||||
{{-- End the required lines block --}}
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="{{ public_asset('/assets/img/favicon.png') }}"/>
|
||||
@@ -18,7 +20,7 @@
|
||||
<link href="{{ public_asset('/assets/frontend/css/styles.css') }}" rel="stylesheet"/>
|
||||
|
||||
{{-- Start of the required files in the head block --}}
|
||||
<link href="{{ public_asset('/assets/global/css/vendor.css') }}" rel="stylesheet"/>
|
||||
<link href="{{ mix_public('/assets/global/css/vendor.css') }}" rel="stylesheet"/>
|
||||
<style type="text/css">
|
||||
@yield('css')
|
||||
</style>
|
||||
@@ -82,8 +84,8 @@
|
||||
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"></script>
|
||||
|
||||
{{-- Start of the required tags block. Don't remove these or things will break!! --}}
|
||||
<script src="{{ public_asset('/assets/global/js/vendor.js') }}"></script>
|
||||
<script src="{{ public_asset('/assets/frontend/js/app.js') }}"></script>
|
||||
<script src="{{ mix_public('/assets/global/js/vendor.js') }}"></script>
|
||||
<script src="{{ mix_public('/assets/frontend/js/app.js') }}"></script>
|
||||
@yield('scripts')
|
||||
|
||||
{{--
|
||||
|
||||
@@ -1,41 +1,26 @@
|
||||
@section('scripts')
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("button.save_flight").click(function (e) {
|
||||
$("button.save_flight").click(async function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = $(this);
|
||||
const class_name = btn.attr('x-saved-class'); // classname to use is set on the element
|
||||
const flight_id = btn.attr('x-id');
|
||||
|
||||
let params = {
|
||||
url: '{{ url('/api/user/bids') }}',
|
||||
data: {
|
||||
'flight_id': btn.attr('x-id')
|
||||
}
|
||||
};
|
||||
if (!btn.hasClass(class_name)) {
|
||||
await phpvms.bids.addBid(flight_id);
|
||||
|
||||
if (btn.hasClass(class_name)) {
|
||||
params.method = 'DELETE';
|
||||
console.log('successfully saved flight');
|
||||
btn.addClass(class_name);
|
||||
alert('@lang("flights.bidadded")');
|
||||
} else {
|
||||
params.method = 'POST';
|
||||
await phpvms.bids.removeBid(flight_id);
|
||||
|
||||
console.log('successfully removed flight');
|
||||
btn.removeClass(class_name);
|
||||
alert('@lang("flights.bidremoved")');
|
||||
}
|
||||
|
||||
axios(params).then(response => {
|
||||
console.log('save bid response', response);
|
||||
|
||||
if(params.method === 'DELETE') {
|
||||
console.log('successfully removed flight');
|
||||
btn.removeClass(class_name);
|
||||
alert('@lang("flights.bidremoved")');
|
||||
} else {
|
||||
console.log('successfully saved flight');
|
||||
btn.addClass(class_name);
|
||||
alert('@lang("flights.bidadded")');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving bid status', params, error);
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user