1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(ui) Trainings settings + style cleanup

This commit is contained in:
vincent 2023-01-06 18:35:57 +01:00 committed by Sylvain
parent 47229d9fff
commit 17c70e0c81
10 changed files with 244 additions and 35 deletions

View File

@ -22,12 +22,13 @@ interface FormInputProps<TFieldValues, TInputType> extends FormComponent<TFieldV
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
nullable?: boolean,
ariaLabel?: string,
maxLength?: number
}
/**
* This component is a template for an input component to use within React Hook Form
*/
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel }: FormInputProps<TFieldValues, TInputType>) => {
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps<TFieldValues, TInputType>) => {
/**
* Debounced (ie. temporised) version of the 'on change' callback.
*/
@ -70,7 +71,8 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
step={step}
disabled={typeof disabled === 'function' ? disabled(id) : disabled}
placeholder={placeholder}
accept={accept} />
accept={accept}
maxLength={maxLength} />
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
{addOn && addOnAction && <button aria-label={addOnAriaLabel} type="button" onClick={addOnAction} className={`addon ${addOnClassName || ''} is-btn`}>{addOn}</button>}
{addOn && !addOnAction && <span aria-label={addOnAriaLabel} className={`addon ${addOnClassName || ''}`}>{addOn}</span>}

View File

@ -0,0 +1,164 @@
import * as React from 'react';
import { useState } from 'react';
import { IApplication } from '../../models/application';
import { Loader } from '../base/loader';
import { react2angular } from 'react2angular';
import { ErrorBoundary } from '../base/error-boundary';
import { useTranslation } from 'react-i18next';
import { FabAlert } from '../base/fab-alert';
import { useForm, SubmitHandler } from 'react-hook-form';
import { FormRichText } from '../form/form-rich-text';
import { FormSwitch } from '../form/form-switch';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
declare const Application: IApplication;
interface TrainingsSettingsProps {
onError: (message: string) => void,
onSuccess: (message: string) => void,
}
/**
* Trainings settings
*/
export const TrainingsSettings: React.FC<TrainingsSettingsProps> = () => {
const { t } = useTranslation('admin');
const { register, control, formState, handleSubmit } = useForm();
// regular expression to validate the input fields
const urlRegex = /^(https?:\/\/)([^.]+)\.(.{2,30})(\/.*)*\/?$/;
const [isActiveAutoCancellation, setIsActiveAutoCancellation] = useState<boolean>(false);
const [isActiveTextBlock, setIsActiveTextBlock] = useState<boolean>(false);
const [isActiveCta, setIsActiveCta] = useState<boolean>(false);
/**
* Callback triggered when the auto cancellation switch has changed.
*/
const toggleAutoCancellation = (value: boolean) => {
setIsActiveAutoCancellation(value);
};
/**
* Callback triggered when the text block switch has changed.
*/
const toggleTextBlockSwitch = (value: boolean) => {
setIsActiveTextBlock(value);
};
/**
* Callback triggered when the CTA switch has changed.
*/
const toggleTextBlockCta = (value: boolean) => {
setIsActiveCta(value);
};
/**
* Callback triggered when the CTA label has changed.
*/
const handleCtaLabelChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
console.log('cta label:', event.target.value);
};
/**
* Callback triggered when the cta url has changed.
*/
const handleCtaUrlChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
console.log('cta url:', event.target.value);
};
/**
* Callback triggered when the form is submitted: save the settings
*/
const onSubmit: SubmitHandler<any> = (data) => {
console.log(data);
};
return (
<div className="trainings-settings">
<header>
<h2>{t('app.admin.trainings_settings.title')}</h2>
</header>
<form onSubmit={handleSubmit(onSubmit)} className="trainings-settings-content">
<div className="setting-section">
<p className="section-title">{t('app.admin.trainings_settings.automatic_cancellation')}</p>
<FabAlert level="warning">
{t('app.admin.trainings_settings.automatic_cancellation_info')}
</FabAlert>
<FormSwitch id="active_auto_cancellation" control={control}
onChange={toggleAutoCancellation} formState={formState}
defaultValue={isActiveAutoCancellation}
label={t('app.admin.trainings_settings.automatic_cancellation_switch')} />
{isActiveAutoCancellation && <>
<FormInput id="auto_cancellation_threshold"
type="number"
register={register}
rules={{ required: isActiveAutoCancellation, min: 0 }}
step={1}
formState={formState}
label={t('app.admin.trainings_settings.automatic_cancellation_threshold')} />
<FormInput id="auto_cancellation_deadline"
type="number"
register={register}
rules={{ required: isActiveAutoCancellation, min: 1 }}
step={1}
formState={formState}
label={t('app.admin.trainings_settings.automatic_cancellation_deadline')} />
</>}
</div>
<div className="setting-section">
<p className="section-title">{t('app.admin.trainings_settings.automatic_cancellation')}</p>
<FabAlert level="warning">
{t('app.admin.trainings_settings.generic_text_block_info')}
</FabAlert>
<FormSwitch id="active_text_block" control={control}
onChange={toggleTextBlockSwitch} formState={formState}
defaultValue={isActiveTextBlock}
label={t('app.admin.trainings_settings.generic_text_block_switch')} />
<FormRichText id="text_block"
control={control}
limit={280}
disabled={!isActiveTextBlock} />
{isActiveTextBlock && <>
<FormSwitch id="active_cta" control={control}
onChange={toggleTextBlockCta} formState={formState}
label={t('app.admin.trainings_settings.cta_switch')} />
{isActiveCta && <>
<FormInput id="cta_label"
register={register}
rules={{ required: true }}
onChange={handleCtaLabelChange}
maxLength={40}
label={t('app.admin.trainings_settings.cta_label')} />
<FormInput id="cta_url"
register={register}
rules={{ required: true, pattern: urlRegex }}
onChange={handleCtaUrlChange}
label={t('app.admin.trainings_settings.cta_url')} />
</>}
</>}
</div>
<FabButton type='submit' className='save-btn'>{t('app.admin.trainings_settings.save')}</FabButton>
</form>
</div>
);
};
const TrainingsSettingsWrapper: React.FC<TrainingsSettingsProps> = (props) => {
return (
<Loader>
<ErrorBoundary>
<TrainingsSettings {...props} />
</ErrorBoundary>
</Loader>
);
};
Application.Components.component('trainingsSettings', react2angular(TrainingsSettingsWrapper, ['onError', 'onSuccess']));

View File

@ -1,7 +1,8 @@
@import "variables/animations";
@import "variables/colors";
@import "variables/typography";
@import "variables/decoration";
@import "variables/layout";
@import "variables/typography";
@import "app.functions";
@import "bootstrap_and_overrides";
@ -139,6 +140,7 @@
@import "modules/supporting-documents/supporting-documents-type-form";
@import "modules/supporting-documents/supporting-documents-types-list";
@import "modules/trainings/training-form";
@import "modules/trainings/trainings-settings";
@import "modules/user/avatar";
@import "modules/user/avatar-input";
@import "modules/user/change-password";

View File

@ -13,12 +13,6 @@
}
}
@mixin grid-col($col-count) {
width: 100%;
display: grid;
grid-template-columns: repeat($col-count, minmax(0, 1fr));
}
.back-btn {
margin: 2.4rem 0;
padding: 0.4rem 0.8rem;
@ -28,7 +22,7 @@
border-radius: var(--border-radius-sm);
color: var(--gray-soft-lightest);
i { margin-right: 0.8rem; }
&:hover {
color: var(--gray-soft-lightest);
background-color: var(--gray-hard-lightest);
@ -47,27 +41,6 @@
}
}
@mixin header {
padding: 2.4rem 0;
display: flex;
justify-content: space-between;
align-items: center;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
h2 {
margin: 0;
@include title-lg;
color: var(--gray-hard-darkest) !important;
}
h3 {
margin: 0;
@include text-lg(600);
color: var(--gray-hard-darkest) !important;
}
}
// Custom scrollbar
.u-scrollbar {
&::-webkit-scrollbar-track
@ -75,13 +48,13 @@
border-radius: 6px;
background-color: #d9d9d9;
}
&::-webkit-scrollbar
{
width: 12px;
background-color: #ffffff;
}
&::-webkit-scrollbar-thumb
{
border-radius: 6px;

View File

@ -8,6 +8,10 @@
header {
@include header();
grid-column: 2 / -2;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
}
.fab-alert {
grid-column: 2 / -2;

View File

@ -9,6 +9,10 @@
@include header();
padding-bottom: 0;
grid-column: 1 / -1;
.grpBtn {
display: flex;
& > *:not(:first-child) { margin-left: 2.4rem; }
}
}
}

View File

@ -0,0 +1,37 @@
.trainings-settings {
max-width: 1600px;
margin: 0 auto;
padding-bottom: 6rem;
@include grid-col(12);
gap: 3.2rem;
align-items: flex-start;
header {
@include header();
padding-bottom: 0;
grid-column: 2 / -2;
}
&-content {
grid-column: 2 / -2;
@include grid-col(2);
gap: 2.4rem 3.2rem;
.setting-section { grid-column: 1 / -1; }
@media (min-width: 1024px) {
.setting-section { grid-column: span 1; }
}
.section-title { @include title-base; }
.save-btn {
grid-column: 1 / -1;
justify-self: flex-start;
background-color: var(--main);
color: var(--gray-soft-lightest);
border: none;
&:hover {
background-color: var(--main);
color: var(--gray-soft-lightest);
opacity: 0.75;
}
}
}
}

View File

@ -0,0 +1,5 @@
@mixin grid-col($col-count) {
width: 100%;
display: grid;
grid-template-columns: repeat($col-count, minmax(0, 1fr));
}

View File

@ -33,6 +33,9 @@
<div class="col-md-12">
<uib-tabset justified="true" active="tabs.active">
<uib-tab heading="{{ 'app.admin.trainings.trainings_settings' | translate }}" index="1" class="manage-trainings">
<trainings-settings on-error="onError" on-success="on-success"></trainings-settings>
</uib-tab>
<uib-tab heading="{{ 'app.admin.trainings.trainings' | translate }}" index="0" class="manage-trainings">
<div class="m-t m-b">
<button type="button" class="btn btn-warning" ui-sref="app.admin.trainings_new" ng-show="isAuthorized('admin')">
@ -78,7 +81,7 @@
</table>
</uib-tab>
<uib-tab heading="{{ 'app.admin.trainings.trainings_monitoring' | translate }}" class="post-tracking" index="1">
<uib-tab heading="{{ 'app.admin.trainings.trainings_monitoring' | translate }}" class="post-tracking" index="2">
<div class="m-lg">
<label for="training_select" translate>{{ 'app.admin.trainings.select_a_training' }}</label>
<select ng-options="training as training.name for training in trainings" ng-model="monitoring.training" class="form-control" ng-change="selectTrainingToMonitor()" name="training_select">

View File

@ -418,11 +418,26 @@ en:
status_enabled: "Enabled"
status_disabled: "Disabled"
status_all: "All"
trainings_settings: "Settings"
#create a new training
trainings_new:
add_a_new_training: "Add a new training"
beware_when_creating_a_training_its_reservation_prices_are_initialized_to_zero: "Beware, when creating a training, its reservation prices are initialized at zero."
dont_forget_to_change_them_before_creating_slots_for_this_training: "Don't forget to change them before creating slots for this training."
trainings_settings:
title: "Settings"
automatic_cancellation: "Trainings automatic cancellation"
automatic_cancellation_info: "If this setting is activated, a minimum number of participants will be required to maintain a session. You will be notified by email if a session is cancelled. If the wallet is enabled, credit notes and refunds will be automatic, otherwise you will have to generate credit notes and make refunds manually."
automatic_cancellation_switch: "Activate automatic cancellation of the trainings"
automatic_cancellation_threshold: "Minimum number of registrations to maintain a session"
automatic_cancellation_deadline: "Deadline, in hours, before automatic cancellation"
generic_text_block: "Generic text block"
generic_text_block_info: "Displays an editorial block above the list of trainings visible to members."
generic_text_block_switch: "Display editorial block"
cta_switch: "Display a button"
cta_label: "Button label"
cta_url: "url"
save: "Save"
#events tracking and management
events:
events_monitoring: "Events monitoring"
@ -2278,7 +2293,7 @@ en:
newest: "Newest first"
oldest: "Oldest first"
store_settings:
title: 'Settings'
title: "Settings"
withdrawal_instructions: 'Product withdrawal instructions'
withdrawal_info: "This text is displayed on the checkout page to inform the client about the products withdrawal method"
store_hidden_title: "Store publicly available"