mirror of
https://gitlab.linphone.org/BC/public/flexisip-account-manager.git
synced 2026-05-01 16:26:24 +00:00
FIX FLEXIAPI-441 add color picker and logo uploader to space settings
This commit is contained in:
parent
0669b0d965
commit
6cea9fbd0b
15 changed files with 474 additions and 116 deletions
|
|
@ -14,6 +14,7 @@ Clone the repository, install the dependencies and generate a key.
|
|||
|
||||
composer install --no-dev
|
||||
php artisan key:generate
|
||||
php artisan storage:link
|
||||
|
||||
# 1.b Packages setup
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use App\Rules\Domain;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class SpaceController extends Controller
|
||||
{
|
||||
|
|
@ -64,7 +65,7 @@ class SpaceController extends Controller
|
|||
|
||||
$request->merge(['full_host' => $fullHost]);
|
||||
$request->validate([
|
||||
'host' => 'nullable|regex:/'. Space::HOST_REGEX . '/',
|
||||
'host' => 'nullable|regex:/' . Space::HOST_REGEX . '/',
|
||||
'full_host' => ['required', 'unique:spaces,host', new Domain()],
|
||||
]);
|
||||
|
||||
|
|
@ -113,7 +114,46 @@ class SpaceController extends Controller
|
|||
|
||||
public function configurationUpdate(Request $request, Space $space)
|
||||
{
|
||||
$space = $this->setConfiguration($request, $space);
|
||||
$request->validate([
|
||||
'newsletter_registration_address' => 'nullable|email',
|
||||
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
|
||||
'logo' => ['nullable', 'image', 'mimes:png', 'max:2048'],
|
||||
'theme_hue' => 'nullable|integer|min:0|max:360',
|
||||
]);
|
||||
|
||||
if ($request->logo_delete == 1 && $space->logo) {
|
||||
Storage::disk('public')->delete($space->LOGO_PATH);
|
||||
$space->logo = null;
|
||||
$space->save();
|
||||
}
|
||||
|
||||
if ($request->hasFile('logo')) {
|
||||
if ($space->logo) {
|
||||
Storage::disk('public')->delete($space->LOGO_PATH);
|
||||
}
|
||||
$filename = $request->file('logo')->hashName();
|
||||
$request->file('logo')->storeAs('img', $filename, 'public');
|
||||
$space->logo = $filename;
|
||||
}
|
||||
|
||||
$space->theme_hue = $request->get('theme_hue');
|
||||
$space->copyright_text = $request->get('copyright_text');
|
||||
$space->intro_registration_text = $request->get('intro_registration_text');
|
||||
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
|
||||
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
|
||||
|
||||
if ($space->accounts()->count() == 0) {
|
||||
$space->account_realm = $request->get('account_realm');
|
||||
}
|
||||
|
||||
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
|
||||
$space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all');
|
||||
$space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header');
|
||||
|
||||
$space->public_registration = getRequestBoolean($request, 'public_registration');
|
||||
$space->phone_registration = getRequestBoolean($request, 'phone_registration');
|
||||
$space->intercom_features = getRequestBoolean($request, 'intercom_features');
|
||||
|
||||
$space->save();
|
||||
|
||||
return redirect()->route('admin.spaces.configuration', $space);
|
||||
|
|
@ -152,33 +192,6 @@ class SpaceController extends Controller
|
|||
return redirect()->route('admin.spaces.show', $space);
|
||||
}
|
||||
|
||||
private function setConfiguration(Request $request, Space $space)
|
||||
{
|
||||
$request->validate([
|
||||
'newsletter_registration_address' => 'nullable|email',
|
||||
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)]
|
||||
]);
|
||||
|
||||
$space->copyright_text = $request->get('copyright_text');
|
||||
$space->intro_registration_text = $request->get('intro_registration_text');
|
||||
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
|
||||
$space->account_proxy_registrar_address = $request->get('account_proxy_registrar_address');
|
||||
|
||||
if ($space->accounts()->count() == 0) {
|
||||
$space->account_realm = $request->get('account_realm');
|
||||
}
|
||||
|
||||
$space->custom_provisioning_entries = $request->get('custom_provisioning_entries');
|
||||
$space->custom_provisioning_overwrite_all = getRequestBoolean($request, 'custom_provisioning_overwrite_all');
|
||||
$space->provisioning_use_linphone_provisioning_header = getRequestBoolean($request, 'provisioning_use_linphone_provisioning_header');
|
||||
|
||||
$space->public_registration = getRequestBoolean($request, 'public_registration');
|
||||
$space->phone_registration = getRequestBoolean($request, 'phone_registration');
|
||||
$space->intercom_features = getRequestBoolean($request, 'intercom_features');
|
||||
|
||||
return $space;
|
||||
}
|
||||
|
||||
private function setAppConfiguration(Request $request, Space $space)
|
||||
{
|
||||
$request->validate([
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class SpaceController extends Controller
|
|||
'max_accounts' => 'nullable|integer',
|
||||
'expire_at' => 'nullable|date|after_or_equal:today',
|
||||
'custom_provisioning_entries' => ['nullable', new Ini(Space::FORBIDDEN_KEYS)],
|
||||
'theme_hue' => 'nullable|integer|min:0|max:360',
|
||||
]);
|
||||
|
||||
$space = new Space;
|
||||
|
|
@ -57,6 +58,7 @@ class SpaceController extends Controller
|
|||
$space->max_accounts = $request->get('max_accounts', 0);
|
||||
$space->name = $request->get('name');
|
||||
$space->newsletter_registration_address = $request->get('newsletter_registration_address');
|
||||
$space->theme_hue = $request->get('theme_hue');
|
||||
$this->setRequestBoolean($request, $space, 'assistant_disable_qr_code');
|
||||
$this->setRequestBoolean($request, $space, 'assistant_hide_create_account');
|
||||
$this->setRequestBoolean($request, $space, 'assistant_hide_third_party_account');
|
||||
|
|
@ -76,6 +78,7 @@ class SpaceController extends Controller
|
|||
$this->setRequestBoolean($request, $space, 'public_registration');
|
||||
$this->setRequestBoolean($request, $space, 'super');
|
||||
$this->setRequestBoolean($request, $space, 'web_panel');
|
||||
|
||||
$space->save();
|
||||
|
||||
return $space->refresh();
|
||||
|
|
@ -125,6 +128,7 @@ class SpaceController extends Controller
|
|||
'public_registration' => 'required|boolean',
|
||||
'super' => 'required|boolean',
|
||||
'web_panel' => 'required|boolean',
|
||||
'theme_hue' => 'nullable|integer|min:0|max:360',
|
||||
]);
|
||||
|
||||
$space = Space::where('domain', $domain)->firstOrFail();
|
||||
|
|
@ -170,6 +174,7 @@ class SpaceController extends Controller
|
|||
$space->provisioning_use_linphone_provisioning_header = $request->get('provisioning_use_linphone_provisioning_header');
|
||||
$space->public_registration = $request->get('public_registration');
|
||||
$space->web_panel = $request->get('web_panel');
|
||||
$space->theme_hue = $request->get('theme_hue');
|
||||
|
||||
$space->save();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -75,6 +76,7 @@ class Space extends Model
|
|||
|
||||
public const HOST_REGEX = '[\w\-]+';
|
||||
public const DOMAIN_REGEX = '(?=^.{4,253}$)(^((?!-)[a-z0-9-]{1,63}(?<!-)\.)+[a-z]{2,63}$)';
|
||||
public const LOGO_PATH = 'img';
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
|
|
@ -130,6 +132,11 @@ class Space extends Model
|
|||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
public function getLogoPathAttribute(): string
|
||||
{
|
||||
return self::LOGO_PATH . '/' . $this->attributes['logo'];
|
||||
}
|
||||
|
||||
public function isFull(): bool
|
||||
{
|
||||
return $this->max_accounts > 0 && ($this->accounts()->count() >= $this->max_accounts);
|
||||
|
|
@ -197,4 +204,11 @@ class Space extends Model
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function themeHue(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn($value) => $value ?? 22
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
flexiapi/app/bootstrap/cache/.gitignore
vendored
Executable file
2
flexiapi/app/bootstrap/cache/.gitignore
vendored
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('spaces', function (Blueprint $table) {
|
||||
$table->integer('theme_hue')->nullable();
|
||||
$table->string('logo')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('spaces', function (Blueprint $table) {
|
||||
$table->dropColumn('theme_hue');
|
||||
$table->dropColumn('logo');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -121,6 +121,7 @@
|
|||
"Files": "Fichiers",
|
||||
"From": "Depuis",
|
||||
"Hello":"Bonjour",
|
||||
"Hex" : "Hex",
|
||||
"Host": "Hôte",
|
||||
"I accept the Privacy Policy": "J'accepte la Politique de Confidentialité",
|
||||
"I accept the Terms and Conditions": "J'accepte les Conditions Générales",
|
||||
|
|
@ -186,6 +187,7 @@
|
|||
"Phone Countries": "Numéros Internationaux",
|
||||
"Phone number": "Numéro de téléphone",
|
||||
"Phone registration": "Inscription par téléphone",
|
||||
"Platform customization": "Customisation de la plateforme",
|
||||
"Please enter the new email that you would like to link to your account.": "Veuillez entre l'adresse email que vous souhaitez lier à votre compte.",
|
||||
"Please enter the new phone number that you would like to link to your account.": "Veuillez entrer le numéro de téléphone que vous souhaitez lier à votre compte.",
|
||||
"Priority rule": "Rêgle prioritaire",
|
||||
|
|
@ -301,8 +303,9 @@
|
|||
"You can now continue your registration process in the application": "Vous pouvez maintenant continuer le processus d'inscription dans l'application",
|
||||
"You didn't receive the code?": "Vous n'avez pas reçu le code ?",
|
||||
"Your account recovery code":"Votre code de récupération de compte",
|
||||
"Your color will be snapped to the nearest 500 shade" : "La couleur que vous entrez sera automatiquement remplacée par sa variante 500 la plus proche.",
|
||||
"Your password was updated properly.": "Votre mot de passe a été mis à jour.",
|
||||
"Your password" : "Votre mot de passe",
|
||||
"Your space :space is expiring in :count days": "Votre espace :space expire dans :count jours",
|
||||
"Your space has expired. Access to your interface is now disabled, and your users can no longer benefit from the service. To reactivate your space, please contact your account manager.": "Votre espace est arrivé à expiration. L’accès à votre interface est désormais désactivé, et vos utilisateurs ne peuvent plus bénéficier du service. Pour réactiver votre espace, veuillez contacter votre responsable de compte."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
flexiapi/public/css/form.css
vendored
122
flexiapi/public/css/form.css
vendored
|
|
@ -1,5 +1,6 @@
|
|||
/** Forms **/
|
||||
|
||||
form div input[type="file"]::file-selector-button,
|
||||
.btn {
|
||||
display: inline-block;
|
||||
background-color: var(--main-5);
|
||||
|
|
@ -157,7 +158,6 @@ form .disabled:not(a) {
|
|||
form label {
|
||||
color: var(--second-6);
|
||||
font-size: 1.5rem;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
|
@ -183,12 +183,21 @@ form div .btn.oppose {
|
|||
right: 0;
|
||||
}
|
||||
|
||||
form div input,
|
||||
form div input:not([type=range]):not([type=button]):not([type=file]):not(.btn),
|
||||
form div textarea,
|
||||
form div select {
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--grey-1);
|
||||
border-radius: 3rem;
|
||||
border: 0.063rem solid var(--grey-2);
|
||||
font-size: 1.5rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
form div input[type=file] {
|
||||
color: var(--second-6);
|
||||
background-color: var(--grey-1);
|
||||
border-radius: 3rem;
|
||||
border: 1px solid var(--grey-2);
|
||||
font-size: 1.5rem;
|
||||
resize: vertical;
|
||||
|
|
@ -261,6 +270,7 @@ input[type=number].digit {
|
|||
height: 5rem;
|
||||
-moz-appearance: textfield;
|
||||
-webkit-appearance: textfield;
|
||||
appearance: textfield;
|
||||
border-radius: 1.5rem;
|
||||
background-color: white;
|
||||
padding: 0.5rem;
|
||||
|
|
@ -270,8 +280,8 @@ input[type=number].digit {
|
|||
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
|
|
@ -282,7 +292,7 @@ form div input[type=checkbox] {
|
|||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
form div input:not([type=checkbox]):not([type=radio]):not([type="number"].digit):not(.btn),
|
||||
form div input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type="number"].digit):not(.btn),
|
||||
form div textarea,
|
||||
form div select {
|
||||
margin-top: 2.5rem;
|
||||
|
|
@ -338,6 +348,86 @@ form div textarea:invalid:not(:placeholder-shown)+label {
|
|||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* Image and Logo Picker */
|
||||
|
||||
form div .picker {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 11rem;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
form canvas,
|
||||
form .picker .color-div {
|
||||
aspect-ratio: 1/1;
|
||||
height: 100%;
|
||||
border: 1px solid var(--grey-2);
|
||||
border-radius: 1.25rem;
|
||||
}
|
||||
|
||||
form .picker canvas {
|
||||
background-color: var(--grey-1);
|
||||
}
|
||||
|
||||
form .picker .color-div {
|
||||
background-color: var(--main-5);
|
||||
}
|
||||
|
||||
form .picker .color-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form .picker .color-input {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
form .picker .color-input i {
|
||||
font-size: 2.5rem;
|
||||
padding-top: 2rem;
|
||||
|
||||
}
|
||||
|
||||
form .picker .color-input i:hover {
|
||||
font-size: 2.5rem;
|
||||
padding-top: 2rem;
|
||||
color: var(--main-5);
|
||||
}
|
||||
|
||||
|
||||
form input[type=range].color {
|
||||
appearance: none;
|
||||
margin-top: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 1.5rem;
|
||||
border-radius: 3rem;
|
||||
overflow: visible;
|
||||
background: linear-gradient(to right,
|
||||
hsl(0, 100%, 50%),
|
||||
hsl(60, 100%, 50%),
|
||||
hsl(120, 100%, 50%),
|
||||
hsl(180, 100%, 50%),
|
||||
hsl(240, 100%, 50%),
|
||||
hsl(300, 100%, 50%),
|
||||
hsl(360, 100%, 50%));
|
||||
}
|
||||
|
||||
form input[type=range]::-webkit-slider-thumb,
|
||||
form input[type=range]::-moz-range-thumb {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
border: 0.25rem solid var(--grey-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Checkbox element */
|
||||
|
||||
form div.checkbox {
|
||||
|
|
@ -346,11 +436,11 @@ form div.checkbox {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
form div.checkbox > div {
|
||||
margin-right: 5rem;
|
||||
form div.checkbox>div {
|
||||
margin-right: 5rem;
|
||||
}
|
||||
|
||||
div.checkbox > input[type="checkbox"] {
|
||||
div.checkbox>input[type="checkbox"] {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
|
@ -378,7 +468,7 @@ div.checkbox:has(> input[type="checkbox"]:checked)::before {
|
|||
background-color: var(--color-green);
|
||||
}
|
||||
|
||||
div.checkbox > input[type="checkbox"] + label {
|
||||
div.checkbox>input[type="checkbox"]+label {
|
||||
display: block;
|
||||
background-color: white;
|
||||
width: 2rem;
|
||||
|
|
@ -393,28 +483,28 @@ div.checkbox > input[type="checkbox"] + label {
|
|||
transition: right 0.3s ease;
|
||||
}
|
||||
|
||||
div.checkbox:hover > input[type="checkbox"] + label {
|
||||
div.checkbox:hover>input[type="checkbox"]+label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.checkbox > input[type="checkbox"]:checked + label {
|
||||
div.checkbox>input[type="checkbox"]:checked+label {
|
||||
right: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
/* Telephony subselect */
|
||||
|
||||
div.select[data-value] ~ div.togglable {
|
||||
div.select[data-value]~div.togglable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.select[data-value=voicemail] ~ div.togglable.voicemail,
|
||||
div.select[data-value=contact] ~ div.togglable.contact,
|
||||
div.select[data-value=sip_uri] ~ div.togglable.sip_uri {
|
||||
div.select[data-value=voicemail]~div.togglable.voicemail,
|
||||
div.select[data-value=contact]~div.togglable.contact,
|
||||
div.select[data-value=sip_uri]~div.togglable.sip_uri {
|
||||
display: block;
|
||||
}
|
||||
|
||||
form section.block div.checkbox:has(input:not(:checked)) ~ div {
|
||||
form section.block div.checkbox:has(input:not(:checked))~div {
|
||||
opacity: 0.25;
|
||||
pointer-events: none;
|
||||
}
|
||||
10
flexiapi/public/css/style.css
vendored
10
flexiapi/public/css/style.css
vendored
|
|
@ -32,7 +32,8 @@ body {
|
|||
--main-2: hsl(from var(--original) calc(h + 11) s calc(l + 29.80));
|
||||
--main-3: hsl(from var(--original) calc(h + 8) s calc(l + 20));
|
||||
--main-4: hsl(from var(--original) calc(h + 3) s calc(l + 12.35));
|
||||
--main-5: hsl(from var(--original) calc(h + 0) s calc(l + 0)); /* original */
|
||||
--main-5: hsl(from var(--original) calc(h + 0) s calc(l + 0));
|
||||
/* original */
|
||||
--main-6: hsl(from var(--original) calc(h - 3) s calc(l - 7.25));
|
||||
--main-7: hsl(from var(--original) calc(h - 7) s calc(l - 14.12));
|
||||
|
||||
|
|
@ -285,7 +286,7 @@ header nav a#menu:after {
|
|||
}
|
||||
|
||||
body.show_menu header nav a#menu:after {
|
||||
font-family: 'Phosphor'
|
||||
font-family: 'Phosphor';
|
||||
|
||||
header nav a#logo {
|
||||
position: absolute;
|
||||
|
|
@ -300,9 +301,8 @@ header nav a#logo::before {
|
|||
width: 3rem;
|
||||
height: 3rem;
|
||||
padding: 1rem;
|
||||
background-image: url('../img/logo.svg');
|
||||
background-color: var(--main-5);
|
||||
background-size: 3rem;
|
||||
background-image: var(--space-logo, url('../img/logo.svg'));
|
||||
background-size: 5rem;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
|
|
|
|||
|
|
@ -1,44 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="287.47433"
|
||||
height="292.50049"
|
||||
viewBox="0 0 287.47433 292.50049"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg2705"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2709" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2707"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.4609375"
|
||||
inkscape:cx="144.27119"
|
||||
inkscape:cy="147.52542"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1148"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2705" />
|
||||
<path
|
||||
d="m 24.974336,57.579473 c 0,0 -29,40 -24.5,58.000997 -0.155,-0.698 19,-21.000997 19,-21.000997 1,5.499997 47.5,19.000997 47.5,19.000997 6.174,3.935 8.302,6.761 9.5,13 v 24 c 16.063,9.994 30.818004,13.31 66.500004,15.5 37.913,-1.616 53.572,-4.974 67.5,-15.5 0,0 -1.5,-18.501 0,-25.001 1.5,-6.5 4.829,-10.217 10,-11.999 19.293,-4.414 28.816,-9.243 47,-18.500997 l 20,20.500997 c -3.039,-22.539997 -8.262,-35.015997 -24,-56.999997 -12.494,-14.278 -20.505,-21.766 -37,-34 -17.932,-10.596 -27.882,-14.6409998 -45.5,-19.4999998 -30.58,-6.262 -47.438,-6.243 -77,0 -18.113004,5.4609998 -27.141004,9.5809998 -40.500004,19.4999998 0,0 -50.5,-29.0009998 -57,-24.00099984 -6.5,5.00000004 18.5,56.99999984 18.5,56.99999984 z"
|
||||
fill="#ffffff"
|
||||
id="path2701" />
|
||||
<path
|
||||
d="m 59.500336,212.50047 c -23.228,-10.168 -35.814,-17.849 -57.5,-35 5.214,21.587 10.076,32.953 22,52 12.706,17.195 20.513,24.868 35.5,35.5 16.208,11.124 25.928,15.987 44.500004,22 16.684,4.01 25.987,5.288 42.5,5.5 16.837,-0.843 25.574,-2.678 40,-6 15.912,-6.341 24.018,-10.339 37.5,-18 22.355,12.62 50,27 54,21 4,-6 -18,-54.5 -18,-54.5 14.673,-21.015 20.675,-35.167 26.5,-59 -20.086,15.757 -33.135,24.951 -58,36.5 -30.758,12.132 -48.672,16.028 -82,17 -38.827,-0.677 -57.775004,-4.519 -87.000004,-17 z"
|
||||
fill="#ffffff"
|
||||
id="path2703" />
|
||||
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_361_1459)">
|
||||
<rect width="100" height="100" rx="20" fill="#FE5E00"/>
|
||||
<rect width="170" height="170" fill="#FE5E00"/>
|
||||
<path d="M27.8723 32.7597C25.4582 36.0095 23.4114 41.2328 23.0002 43.5646C22.9712 43.4345 26.8489 39.6619 26.8489 39.6619C29.0866 40.8597 35.6873 43.2063 35.6873 43.2063C36.836 43.9405 37.232 44.4676 37.455 45.6314V50.1085C40.4438 51.9729 43.1892 52.5916 49.8288 53C56.8833 52.6985 59.7969 52.0722 62.3886 50.1085C62.3886 50.1085 62.1095 46.6574 62.3886 45.4448C62.6677 44.2323 63.2871 43.5388 64.2493 43.2063C67.8391 42.383 69.6111 41.4822 72.9947 39.7552L77 43.4713C76.4418 40.9529 75.1788 37.0473 72.2504 32.9462C69.9255 30.2829 68.4349 28.8859 65.3657 26.6037C62.029 24.6272 60.1824 23.783 56.9043 22.8766C51.6012 21.5521 46.8564 21.8319 42.5768 23.0258C38.9484 24.038 37.3405 24.7013 34.8548 26.5515C34.8548 26.5515 25.6395 21.1938 24.43 22.1265C23.2205 23.0593 27.8723 32.7597 27.8723 32.7597Z" fill="white"/>
|
||||
<path d="M33.9831 62.8886C29.6288 60.9696 27.0652 59.6127 23 56.3758C23.7619 60.0404 25.8059 64.0808 27.3148 66.0971C29.6153 69.3426 31.1736 70.8483 33.9831 72.8549C36.8273 74.8864 38.8434 75.8141 42.3248 76.949C45.4523 77.7058 47.1767 78.0758 50.2917 77.987C53.5864 77.8931 55.1896 77.7052 57.7899 76.8547C60.6476 75.9199 62.4129 75.1682 64.8286 73.6122C69.0192 75.9939 74.1923 78.5532 74.9421 77.4209C75.6919 76.2885 71.7412 67.2754 71.7412 67.2754C73.9339 64.4566 76.1638 60.4162 77 56C73.2347 58.9737 70.3242 60.709 65.663 62.8886C59.8973 65.1784 56.5392 65.9135 50.2917 66.0971C43.0134 65.9692 39.4614 65.2441 33.9831 62.8886Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_361_1459">
|
||||
<rect width="100" height="100" rx="20" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.8 KiB |
179
flexiapi/public/scripts/colorPicker.js
vendored
Normal file
179
flexiapi/public/scripts/colorPicker.js
vendored
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
let colorPicker = {
|
||||
init() {
|
||||
let hue = document.getElementById("theme_hue").value;
|
||||
document.getElementById('hex-color').value = this.hslToHex(hue, 100, 50);
|
||||
document.getElementById('color-div').style.backgroundColor = this.hslToHex(hue, 100, 50);
|
||||
},
|
||||
|
||||
onSliderChange(input) {
|
||||
const group = input.closest('.color-picker-group');
|
||||
const hue = input.value;
|
||||
|
||||
group.querySelector('input[type=hidden]').value = hue;
|
||||
group.querySelector('input[type=text]').value = this.hslToHex(hue, 100, 50);
|
||||
group.querySelector('.color-div').style.backgroundColor = this.hslToHex(hue, 100, 50);
|
||||
},
|
||||
|
||||
onInputChange(input) {
|
||||
const hex = input.value.trim();
|
||||
if (!hex.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = input.closest('.color-picker-group');
|
||||
const colorHsl = this.hexToHsl(hex);
|
||||
group.querySelector('input[type=hidden]').value = colorHsl.h;
|
||||
group.querySelector('input[type=range]').value = colorHsl.h;
|
||||
group.querySelector('input[type=text]').value = this.hslToHex(colorHsl.h, 100, 50);
|
||||
group.querySelector('.color-div').style.backgroundColor = this.hslToHex(colorHsl.h, 100, 50);
|
||||
},
|
||||
|
||||
onReset(i, hue) {
|
||||
const color = this.hslToHex(hue, 100, 50);
|
||||
const group = i.closest('.color-picker-group');
|
||||
group.querySelector('input[type=hidden]').value = hue;
|
||||
group.querySelector('input[type=range]').value = hue;
|
||||
group.querySelector('input[type=text]').value = color;
|
||||
group.querySelector('.color-div').style.backgroundColor = color;
|
||||
},
|
||||
|
||||
hslToHex(h, s, l) {
|
||||
s /= 100;
|
||||
l /= 100;
|
||||
|
||||
const k = n => (n + h / 30) % 12;
|
||||
const a = s * Math.min(l, 1 - l);
|
||||
const f = n => Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))));
|
||||
|
||||
const toHex = v => v.toString(16).padStart(2, '0');
|
||||
return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`;
|
||||
},
|
||||
|
||||
hexToHsl(hex) {
|
||||
hex = hex.replace(/^#/, '');
|
||||
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
||||
|
||||
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(hex.slice(4, 6), 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
let h, s;
|
||||
let l = (max + min) / 2;
|
||||
|
||||
if (d === 0) {
|
||||
h = s = 0;
|
||||
} else {
|
||||
s = d / (l > 0.5 ? 2 - max - min : max + min);
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
h: Math.round(h * 360),
|
||||
s: Math.round(s * 100),
|
||||
l: Math.round(l * 100),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let imagePicker = {
|
||||
init() {
|
||||
document.querySelectorAll('.image-picker-group').forEach(group => {
|
||||
const canvas = group.querySelector('canvas');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width;
|
||||
canvas.height = rect.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const existingImage = canvas.dataset.existing;
|
||||
|
||||
if (existingImage) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = "anonymous";
|
||||
img.onload = () => this.drawImageProp(ctx, img);
|
||||
img.src = existingImage;
|
||||
} else {
|
||||
this.setPlaceHolder(canvas);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onLoad(input) {
|
||||
const file = input.files[0];
|
||||
if (!file) return;
|
||||
if (!file.type.match('image.*')) {
|
||||
console.log("Error: not an image");
|
||||
imgInput.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
|
||||
const group = input.closest('.image-picker-group');
|
||||
const canvas = group.querySelector('canvas');
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.drawImageProp(ctx, img);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
const croppedFile = new File([blob], 'logo.png', { type: 'image/png' });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(croppedFile);
|
||||
input.files = dataTransfer.files;
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
group.querySelector('input[name=logo_delete]').value = 0;
|
||||
group.querySelector('input[type=button]').style.display = 'block';
|
||||
},
|
||||
|
||||
drawImageProp(ctx, img) {
|
||||
const canvas = ctx.canvas;
|
||||
const scale = Math.max(canvas.width / img.width, canvas.height / img.height);
|
||||
|
||||
const nw = img.width * scale;
|
||||
const nh = img.height * scale;
|
||||
|
||||
const x = (canvas.width - nw) / 2;
|
||||
const y = (canvas.height - nh) / 2;
|
||||
|
||||
ctx.drawImage(img, x, y, nw, nh);
|
||||
},
|
||||
|
||||
setPlaceHolder(canvas) {
|
||||
const ctx = canvas.getContext("2d");
|
||||
const style = getComputedStyle(document.querySelector('body'));
|
||||
ctx.fillStyle = style.getPropertyValue('--second-5').trim();
|
||||
ctx.font = `${canvas.width / 6}px ${style.getPropertyValue('font-family')}`;
|
||||
ctx.textBaseline = "middle";
|
||||
|
||||
const text = "Image";
|
||||
const textWidth = ctx.measureText(text).width;
|
||||
ctx.fillText(text, (canvas.width - textWidth) / 2, canvas.height / 2);
|
||||
},
|
||||
|
||||
onDelete(input) {
|
||||
const group = input.closest('.image-picker-group');
|
||||
const canvas = group.querySelector('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
this.setPlaceHolder(canvas);
|
||||
group.querySelector('input[name=logo_delete]').value = 1;
|
||||
group.querySelector('input[type=button]').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
colorPicker.init();
|
||||
imagePicker.init();
|
||||
});
|
||||
|
|
@ -17,12 +17,38 @@
|
|||
@include('admin.space.head')
|
||||
@include('admin.space.tabs')
|
||||
|
||||
<form method="POST"
|
||||
action="{{ route('admin.spaces.configuration.update', $space) }}"
|
||||
accept-charset="UTF-8">
|
||||
<form method="POST" action="{{ route('admin.spaces.configuration.update', $space) }}" accept-charset="UTF-8"
|
||||
enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('put')
|
||||
|
||||
<h3 class="large">{{ __('Platform customization') }}</h3>
|
||||
|
||||
<div class="image-picker-group">
|
||||
<div class="picker">
|
||||
<canvas @if($space->logo) data-existing="{{ asset('storage/img/' . $space->logo) }}" @endif> </canvas>
|
||||
<input name="logo" type="file" accept="image/*" onchange="imagePicker.onLoad(this)">
|
||||
<input class="btn secondary" type="button" value="{{ __('Delete') }}" onclick="imagePicker.onDelete(this)" @if(!$space->logo) style="display:none;" @endif>
|
||||
<input type="hidden" name="logo_delete" value="0">
|
||||
@include('parts.errors', ['name' => 'logo'])
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-picker-group">
|
||||
<input id="theme_hue" name="theme_hue" type="hidden" value="{{$space->theme_hue}}">
|
||||
<div class="picker">
|
||||
<div class="color-div"></div>
|
||||
<div class="color-selector">
|
||||
<div class="color-input">
|
||||
<input id="hex-color"type="text" value="" onblur="colorPicker.onInputChange(this)">
|
||||
<label for="color-input">{{ __('Hex') }}</label>
|
||||
<i class="ph ph-arrow-counter-clockwise" onclick="colorPicker.onReset(this, {{$space->theme_hue}})"></i>
|
||||
</div>
|
||||
<input class="color" type="range" min="0" max="360" value="{{$space->theme_hue}}" oninput="colorPicker.onSliderChange(this)">
|
||||
</div>
|
||||
</div>
|
||||
<span class="supporting"> {{ __('Your color will be snapped to the nearest 500 shade') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="large">
|
||||
<textarea name="copyright_text" id="copyright_text">{{ $space->copyright_text }}</textarea>
|
||||
<label for="copyright_text">{{ __('Copyright text') }}</label>
|
||||
|
|
@ -37,21 +63,24 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<input name="newsletter_registration_address" id="newsletter_registration_address" placeholder="email@server.tld" type="email" value="{{ $space->newsletter_registration_address }}">
|
||||
<input name="newsletter_registration_address" id="newsletter_registration_address"
|
||||
placeholder="email@server.tld" type="email" value="{{ $space->newsletter_registration_address }}">
|
||||
<label for="newsletter_registration_address">{{ __('Newsletter registration email address') }}</label>
|
||||
<span class="supporting">{{ __('An email will be sent to this email when someone join the newsletter') }}</span>
|
||||
@include('parts.errors', ['name' => 'newsletter_registration_address'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="account_proxy_registrar_address" id="account_proxy_registrar_address" placeholder="server.tld" value="{{ $space->account_proxy_registrar_address }}">
|
||||
<input name="account_proxy_registrar_address" id="account_proxy_registrar_address" placeholder="server.tld"
|
||||
value="{{ $space->account_proxy_registrar_address }}">
|
||||
<label for="account_proxy_registrar_address">Account proxy registrar address</label>
|
||||
<span class="supporting">Will be used for informational purpose in the user panel and communication emails</span>
|
||||
@include('parts.errors', ['name' => 'account_proxy_registrar_address'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input name="account_realm" @if ($space->accounts()->count() > 0)disabled @endif id="account_realm" placeholder="server.tld" value="{{ $space->account_realm }}">
|
||||
<input name="account_realm" @if ($space->accounts()->count() > 0) disabled @endif id="account_realm"
|
||||
placeholder="server.tld" value="{{ $space->account_realm }}">
|
||||
<label for="account_realm">Account realm</label>
|
||||
<span class="supporting">A custom realm for the Space accounts</span>
|
||||
@include('parts.errors', ['name' => 'account_realm'])
|
||||
|
|
@ -63,31 +92,55 @@
|
|||
<textarea style="min-height: 200px;" name="custom_provisioning_entries" id="custom_provisioning_entries">{{ $space->custom_provisioning_entries }}</textarea>
|
||||
<label for="custom_provisioning_entries">{{ __('Custom entries') }}</label>
|
||||
<span class="supporting">{{ __('In ini format, will complete the other settings.') }}</span>
|
||||
<span class="supporting">{{ __('Use ; to comment, key="value" to declare a complex string.') }} <a target="_blank" href="https://cheatsheets.zip/ini.html">{{ __('Checkout the cheatsheets to know how to format things correctly.') }}</a></span>
|
||||
<span class="supporting">
|
||||
{{ __('Use ; to comment, key="value" to declare a complex string.') }}
|
||||
<a target="_blank" href="https://cheatsheets.zip/ini.html">{{ __('Checkout the cheatsheets to know how to format things correctly.') }}</a>
|
||||
</span>
|
||||
@include('parts.errors', ['name' => 'custom_provisioning_entries'])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'custom_provisioning_overwrite_all', 'label' => __('Allow client settings to be overwritten by the provisioning ones')])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'custom_provisioning_overwrite_all',
|
||||
'label' => __('Allow client settings to be overwritten by the provisioning ones'),
|
||||
])
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'provisioning_use_linphone_provisioning_header', 'label' => 'Enforce X-Linphone-Provisioning header'])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'provisioning_use_linphone_provisioning_header',
|
||||
'label' => 'Enforce X-Linphone-Provisioning header',
|
||||
])
|
||||
</div>
|
||||
|
||||
<h3 class="large">{{ __('Features') }}</h3>
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'public_registration', 'label' => __('Public registration')])
|
||||
</div>
|
||||
<div
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'phone_registration', 'label' => __('Phone registration')])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'public_registration',
|
||||
'label' => __('Public registration'),
|
||||
])
|
||||
</div>
|
||||
<div>
|
||||
@include('parts.form.toggle', ['object' => $space, 'key' => 'intercom_features', 'label' => __('Intercom features')])
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'phone_registration',
|
||||
'label' => __('Phone registration'),
|
||||
])
|
||||
</div>
|
||||
<div>
|
||||
@include('parts.form.toggle', [
|
||||
'object' => $space,
|
||||
'key' => 'intercom_features',
|
||||
'label' => __('Intercom features'),
|
||||
])
|
||||
</div>
|
||||
|
||||
<div class="large">
|
||||
<input class="btn" type="submit" value="{{ __('Update') }}">
|
||||
</div>
|
||||
</form>
|
||||
<script src="{{ asset('scripts/colorPicker.js') }}"></script>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ JSON parameters:
|
|||
* `public_registration` boolean, the public registration switch
|
||||
* `super` boolean, set the domain as a Super Domain
|
||||
* `web_panel` boolean, the web panel switch
|
||||
* `theme_hue` integer, the hue component of an HSL color (e.g. `hsl(theme_hue, 100%, 50%)`), between 0 and 360
|
||||
|
||||
### `PUT /spaces/{domain}`
|
||||
<span class="badge badge-error">Super Admin</span>
|
||||
|
|
@ -88,6 +89,7 @@ JSON parameters:
|
|||
* `public_registration` **required**, boolean, the public registration switch
|
||||
* `super` **required**, boolean, set the domain as a Super Domain
|
||||
* `web_panel` **required**, boolean, the web panel switch
|
||||
* `theme_hue` **required**, integer, the hue component of an HSL color (e.g. `hsl(theme_hue, 100%, 50%)`), between 0 and 360
|
||||
|
||||
### `DELETE /spaces/{domain}`
|
||||
<span class="badge badge-error">Super Admin</span>
|
||||
|
|
|
|||
|
|
@ -9,15 +9,15 @@
|
|||
<title>{{ space()->name }}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/style.css') }}">
|
||||
|
||||
@php
|
||||
$space = space();
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
body {
|
||||
@if (space()?->theme_hue) --hue: {{ $space->theme_hue }}; @endif
|
||||
@if (space()?->logo) --space-logo: url('{{ asset('storage/img/' . $space->logo) }}'); @endif
|
||||
}
|
||||
</style>
|
||||
@if (space()?->custom_theme && file_exists(public_path('css/' . space()?->host . '.style.css')))
|
||||
<link rel="stylesheet" type="text/css" href="{{ asset('css/' . space()?->host . '.style.css') }}">
|
||||
@endif
|
||||
|
||||
<script src="{{ asset('scripts/utils.js') }}"></script>
|
||||
<script src="{{ asset('scripts/chart.js') }}"></script>
|
||||
<script src="{{ asset('scripts/chartjs-plugin-datalabels@2.0.0') }}"></script>
|
||||
|
|
@ -26,8 +26,9 @@
|
|||
<body class="@if (isset($welcome) && $welcome) welcome @endif">
|
||||
<header>
|
||||
<nav>
|
||||
<a id="logo" href="{{ route('account.home') }}"><span
|
||||
class="on_desktop">{{ space()->name }}</span></a>
|
||||
<a id="logo" href="{{ route('account.home') }}">
|
||||
<span class="on_desktop">{{ space()->name }}</span>
|
||||
</a>
|
||||
|
||||
@if (!isset($welcome) || $welcome == false)
|
||||
<a id="menu" class="on_mobile" href="#"
|
||||
|
|
|
|||
|
|
@ -130,6 +130,8 @@ if ! test -f %{env_config_file}; then
|
|||
php artisan key:generate
|
||||
fi
|
||||
|
||||
php artisan storage:link
|
||||
|
||||
# Link it once more
|
||||
ln -sf %{env_config_file} %{env_symlink_file}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue