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:
parent
47229d9fff
commit
17c70e0c81
@ -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>}
|
||||
|
@ -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']));
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -9,6 +9,10 @@
|
||||
@include header();
|
||||
padding-bottom: 0;
|
||||
grid-column: 1 / -1;
|
||||
.grpBtn {
|
||||
display: flex;
|
||||
& > *:not(:first-child) { margin-left: 2.4rem; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
5
app/frontend/src/stylesheets/variables/layout.scss
Normal file
5
app/frontend/src/stylesheets/variables/layout.scss
Normal file
@ -0,0 +1,5 @@
|
||||
@mixin grid-col($col-count) {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat($col-count, minmax(0, 1fr));
|
||||
}
|
@ -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">
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user