1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

(ui) Update machines layout

This commit is contained in:
vincent 2023-01-18 17:40:47 +01:00 committed by Sylvain
parent 5fd2d3fc79
commit 49a06a9176
13 changed files with 315 additions and 96 deletions

View File

@ -117,8 +117,12 @@ export const MachineCategoriesList: React.FC<MachineCategoriesListProps> = ({ on
return ( return (
<div className="machine-categories-list"> <div className="machine-categories-list">
<h3 className="machines-categories">{t('app.admin.machine_categories_list.machine_categories')}</h3> <header>
<FabButton onClick={addMachineCategory} className="is-secondary" >{t('app.admin.machine_categories_list.add_a_machine_category')}</FabButton> <h2>{t('app.admin.machine_categories_list.machine_categories')}</h2>
<div className='grpBtn'>
<FabButton className="main-action-btn" onClick={addMachineCategory}>{t('app.admin.machine_categories_list.add_a_machine_category')}</FabButton>
</div>
</header>
<MachineCategoryModal isOpen={modalIsOpen} <MachineCategoryModal isOpen={modalIsOpen}
machines={machines} machines={machines}
machineCategory={machineCategory} machineCategory={machineCategory}

View File

@ -18,6 +18,19 @@ export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onFilterChange
const defaultValue = { value: true, label: t('app.public.machines_filters.status_enabled') }; const defaultValue = { value: true, label: t('app.public.machines_filters.status_enabled') };
const categoryDefaultValue = { value: null, label: t('app.public.machines_filters.all_machines') }; const categoryDefaultValue = { value: null, label: t('app.public.machines_filters.all_machines') };
// Styles the React-select component
const customStyles = {
control: base => ({
...base,
width: '20ch',
border: 'none',
backgroundColor: 'transparent'
}),
indicatorSeparator: () => ({
display: 'none'
})
};
/** /**
* Provides boolean options in the react-select format (yes/no/all) * Provides boolean options in the react-select format (yes/no/all)
*/ */
@ -56,21 +69,23 @@ export const MachinesFilters: React.FC<MachinesFiltersProps> = ({ onFilterChange
return ( return (
<div className="machines-filters"> <div className="machines-filters">
<div className="filter-item"> <div className="filter-item">
<label htmlFor="status">{t('app.public.machines_filters.show_machines')}</label> <p>{t('app.public.machines_filters.show_machines')}</p>
<Select defaultValue={defaultValue} <Select defaultValue={defaultValue}
id="status" id="status"
className="status-select" className="status-select"
onChange={handleStatusSelected} onChange={handleStatusSelected}
options={buildBooleanOptions()}/> options={buildBooleanOptions()}
styles={customStyles}/>
</div> </div>
{machineCategories.length > 0 && {machineCategories.length > 0 &&
<div className="filter-item"> <div className="filter-item">
<label htmlFor="category">{t('app.public.machines_filters.filter_by_machine_category')}</label> <p>{t('app.public.machines_filters.filter_by_machine_category')}</p>
<Select defaultValue={categoryDefaultValue} <Select defaultValue={categoryDefaultValue}
id="machine_category" id="machine_category"
className="category-select" className="category-select"
onChange={handleCategorySelected} onChange={handleCategorySelected}
options={buildCategoriesOptions()}/> options={buildCategoriesOptions()}
styles={customStyles}/>
</div> </div>
} }
</div> </div>

View File

@ -12,6 +12,8 @@ import { MachinesFilters } from './machines-filters';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import { EditorialBlock } from '../base/editorial-block';
import { CalendarBlank } from 'phosphor-react';
declare const Application: IApplication; declare const Application: IApplication;
@ -96,30 +98,14 @@ export const MachinesList: React.FC<MachinesListProps> = ({ onError, onSuccess,
}); });
}; };
/**
* Go to store
*/
const linkToStore = (): void => {
window.location.href = '/#!/store';
};
// TODO: Conditionally display the store ad
return ( return (
<div className="machines-list"> <div className="machines-list">
{/* TODO: Condition to display editorial block */}
{false &&
<EditorialBlock />
}
<MachinesFilters onFilterChangedBy={handleFilterChangedBy} machineCategories={machineCategories}/> <MachinesFilters onFilterChangedBy={handleFilterChangedBy} machineCategories={machineCategories}/>
<div className="all-machines"> <div className="all-machines">
{false &&
<div className='store-ad' onClick={() => linkToStore}>
<div className='content'>
<h3>{t('app.public.machines_list.store_ad.title')}</h3>
<p>{t('app.public.machines_list.store_ad.buy')}</p>
<p className='sell'>{t('app.public.machines_list.store_ad.sell')}</p>
</div>
<FabButton icon={<i className="fa fa-cart-plus fa-lg" />} className="cta" onClick={linkToStore}>
{t('app.public.machines_list.store_ad.link')}
</FabButton>
</div>
}
{machines && machines.map(machine => { {machines && machines.map(machine => {
return <MachineCard key={machine.id} return <MachineCard key={machine.id}
user={user} user={user}

View File

@ -0,0 +1,153 @@
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 { 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 MachinesSettingsProps {
onError: (message: string) => void,
onSuccess: (message: string) => void,
}
/**
* Machines settings
*/
export const MachinesSettings: React.FC<MachinesSettingsProps> = () => {
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 [isActiveAuthorizationValidity, setIsActiveAuthorizationValidity] = useState<boolean>(false);
const [isActiveTextBlock, setIsActiveTextBlock] = useState<boolean>(false);
const [isActiveValidationRule, setIsActiveValidationRule] = 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 authorisation validity switch has changed.
*/
const toggleAuthorizationValidity = (value: boolean) => {
setIsActiveAuthorizationValidity(value);
};
/**
* Callback triggered when the authorisation validity switch has changed.
*/
const toggleValidationRule = (value: boolean) => {
setIsActiveValidationRule(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="machines-settings">
<header>
<h2>{t('app.admin.machines_settings.title')}</h2>
<FabButton onClick={handleSubmit(onSubmit)} className='save-btn is-main'>{t('app.admin.machines_settings.save')}</FabButton>
</header>
<form className="machines-settings-content">
<div className="settings-section">
<header>
<p className="title">{t('app.admin.machines_settings.generic_text_block')}</p>
<p className="description">{t('app.admin.machines_settings.generic_text_block_info')}</p>
</header>
<div className="content">
<FormSwitch id="active_text_block" control={control}
onChange={toggleTextBlockSwitch} formState={formState}
defaultValue={isActiveTextBlock}
label={t('app.admin.machines_settings.generic_text_block_switch')} />
<FormRichText id="text_block"
control={control}
heading
limit={280}
disabled={!isActiveTextBlock} />
{isActiveTextBlock && <>
<FormSwitch id="active_cta" control={control}
onChange={toggleTextBlockCta} formState={formState}
label={t('app.admin.machines_settings.cta_switch')} />
{isActiveCta && <>
<FormInput id="cta_label"
register={register}
rules={{ required: true }}
onChange={handleCtaLabelChange}
maxLength={40}
label={t('app.admin.machines_settings.cta_label')} />
<FormInput id="cta_url"
register={register}
rules={{ required: true, pattern: urlRegex }}
onChange={handleCtaUrlChange}
label={t('app.admin.machines_settings.cta_url')} />
</>}
</>}
</div>
</div>
</form>
</div>
);
};
const MachinesSettingsWrapper: React.FC<MachinesSettingsProps> = (props) => {
return (
<Loader>
<ErrorBoundary>
<MachinesSettings {...props} />
</ErrorBoundary>
</Loader>
);
};
Application.Components.component('machinesSettings', react2angular(MachinesSettingsWrapper, ['onError', 'onSuccess']));

View File

@ -62,11 +62,12 @@
@import "modules/invoices/vat-settings-modal"; @import "modules/invoices/vat-settings-modal";
@import "modules/layout/header-page"; @import "modules/layout/header-page";
@import "modules/machines/machine-card"; @import "modules/machines/machine-card";
@import "modules/machines/machine-categories";
@import "modules/machines/machine-form"; @import "modules/machines/machine-form";
@import "modules/machines/machines-filters"; @import "modules/machines/machines-filters";
@import "modules/machines/machines-list"; @import "modules/machines/machines-list";
@import "modules/machines/machines-settings";
@import "modules/machines/required-training-modal"; @import "modules/machines/required-training-modal";
@import "modules/machines/machine-categories";
@import "modules/payment-schedule/payment-schedule-dashboard"; @import "modules/payment-schedule/payment-schedule-dashboard";
@import "modules/payment-schedule/payment-schedule-summary"; @import "modules/payment-schedule/payment-schedule-summary";
@import "modules/payment-schedule/payment-schedules-list"; @import "modules/payment-schedule/payment-schedules-list";

View File

@ -1,4 +1,15 @@
.machine-categories-list { .machine-categories-list {
max-width: 1200px;
margin: 0 auto;
padding-bottom: 6rem;
display: flex;
flex-direction: column;
& > header {
padding-bottom: 0;
@include header;
gap: 2.4rem;
}
.buttons { .buttons {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -1,36 +1,40 @@
.machines-filters { .machines-filters {
margin: 1.5em 0; max-width: 1600px;
width: 100%;
margin: 0 auto;
padding: 0.8rem 2.4rem;
display: flex; display: flex;
justify-content: space-between; flex-wrap: wrap;
background-color: var(--gray-soft);
border-radius: var(--border-radius);
@media (min-width: 1024px) {
justify-content: flex-end;
& > *:not(:first-child) {
&::before {
content: "";
margin: 0 2rem;
width: 1px;
height: 2rem;
background-color: var(--gray-hard-darkest);
}
}
}
.filter-item { .filter-item {
&:first-child { display: flex;
padding-right: 20px; align-items: center;
} p { margin: 0 0.8rem 0 0; }
& {
display: block;
width: 50%;
}
& > label {
white-space: nowrap;
line-height: 2em;
}
& > * {
display: inline-block;
}
.status-select, .category-select {
width: 100%;
}
} }
} }
@media screen and (max-width: 720px){ //@media screen and (max-width: 720px){
.machines-filters { // .machines-filters {
display: block; // display: block;
.filter-item { // .filter-item {
padding-right: 0 !important; // padding-right: 0 !important;
display: block; // display: block;
width: 100%; // width: 100%;
} // }
} // }
} //}

View File

@ -1,6 +1,24 @@
.machines-list { .machines-list {
display: flex;
flex-direction: column;
gap: 1.6rem;
&-container {
max-width: 1600px;
margin: 0 auto;
padding-bottom: 6rem;
display: flex;
flex-direction: column;
& > header {
padding-bottom: 0;
@include header;
gap: 2.4rem;
}
}
.all-machines { .all-machines {
max-width: 1600px; max-width: 1600px;
width: 100%;
margin: 0 auto; margin: 0 auto;
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));

View File

@ -0,0 +1,21 @@
.machines-settings {
max-width: 1200px;
margin: 0 auto;
padding-bottom: 6rem;
display: flex;
flex-direction: column;
& > header {
padding-bottom: 0;
@include header($sticky: true);
gap: 2.4rem;
}
&-content {
display: flex;
flex-direction: column;
gap: 3.2rem;
.settings-section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}
}

View File

@ -6,47 +6,44 @@
<div class="center"> <div class="center">
<h1 translate>{{ 'app.admin.machines.the_fablab_s_machines' }}</h1> <h1 translate>{{ 'app.admin.machines.the_fablab_s_machines' }}</h1>
</div> </div>
<div class="right">
<div class="grpBtn wrapper">
<a ng-if="isAuthorized('admin')"
role="button"
class="btn btn-lg btn-warning bg-white b-2x rounded m-t-xs"
ui-sref="app.admin.machines_new"
title="{{'app.public.machines_list.add_a_machine' | translate}}">
<i class="fas fa-plus"></i>
</a>
<a ng-if="isAuthorized(['admin', 'manager'])"
role="button"
ui-sref="app.admin.calendar"
class="btn btn-lg btn-default rounded b-2x m-t-xs"
title="{{'app.public.machines_list.new_availability' | translate}}">
<i class="fa fa-calendar-check-o" aria-hidden="true"></i>
</a>
</div>
</div>
</div> </div>
<section class="m-lg admin-machines-manage"> <section class="m-lg admin-machines-manage">
<div class="row"> <uib-tabset justified="true" active="tabs.active">
<div> <!--<uib-tab heading="{{ 'app.admin.machines.machines_settings' | translate }}" index="1" select="selectTab()">
<uib-tabset justified="true" active="tabs.active"> <machines-settings on-error="onError" on-success="on-success"></machines-settings>
</uib-tab>-->
<uib-tab heading="{{ 'app.admin.machines.machines_settings' | translate }}" index="1" select="selectTab()"> <uib-tab heading="{{ 'app.admin.machines.all_machines' | translate }}" index="0" select="selectTab()">
<machines-settings on-error="onError" on-success="on-success"></machines-settings> <div class="machines-list-container">
</uib-tab> <header>
<h2 translate>{{ 'app.admin.machines.all_machines' }}</h2>
<div class="grpBtn">
<a ng-if="isAuthorized(['admin', 'manager'])"
role="button"
ui-sref="app.admin.calendar"
class="fab-button"
title="{{'app.public.machines_list.new_availability' | translate}}">
<i class="far fa-calendar" aria-hidden="true"></i>
</a>
<a ng-if="isAuthorized('admin')"
role="button"
class="fab-button is-main"
ui-sref="app.admin.machines_new"
title="{{'app.public.machines_list.add_a_machine' | translate}}"
translate>
{{ 'app.admin.machines.add_a_machine' }}
</a>
</div>
</header>
<ng-include src="'/admin/machines/machines.html'"></ng-include>
</div>
</uib-tab>
<uib-tab heading="{{ 'app.admin.machines.all_machines' | translate }}" index="0" select="selectTab()"> <uib-tab heading="{{ 'app.admin.machines.manage_machines_categories' | translate }}" index="2" select="selectTab()">
<ng-include src="'/admin/machines/machines.html'"></ng-include> <ng-include src="'/admin/machines/categories.html'"></ng-include>
</uib-tab> </uib-tab>
<uib-tab heading="{{ 'app.admin.machines.manage_machines_categories' | translate }}" index="2" select="selectTab()"> </uib-tabset>
<ng-include src="'/admin/machines/categories.html'"></ng-include>
</uib-tab>
</uib-tabset>
</div>
</div>
</section> </section>

View File

@ -1,5 +1,4 @@
<section class="m-lg" <section ui-tour="machines"
ui-tour="machines"
ui-tour-backdrop="true" ui-tour-backdrop="true"
ui-tour-template-url="'/shared/tour-step-template.html'" ui-tour-template-url="'/shared/tour-step-template.html'"
ui-tour-use-hotkeys="true" ui-tour-use-hotkeys="true"

View File

@ -4,8 +4,18 @@ en:
machines: machines:
the_fablab_s_machines: "The FabLab's machines" the_fablab_s_machines: "The FabLab's machines"
all_machines: "All machines" all_machines: "All machines"
add_a_machine: "Add a new machine"
manage_machines_categories: "Manage machines categories" manage_machines_categories: "Manage machines categories"
machines_settings: "Settings" machines_settings: "Settings"
machines_settings:
title: "Settings"
generic_text_block: "Editorial text block"
generic_text_block_info: "Displays an editorial block above the list of machines visible to members."
generic_text_block_switch: "Display editorial block"
cta_switch: "Display a button"
cta_label: "Button label"
cta_url: "url"
save: "Save"
machine_categories_list: machine_categories_list:
machine_categories: "Machines categories" machine_categories: "Machines categories"
add_a_machine_category: "Add a machine category" add_a_machine_category: "Add a machine category"

View File

@ -229,11 +229,11 @@ en:
sell: "If you also want to sell your creations, please let us know." sell: "If you also want to sell your creations, please let us know."
link: "To the store" link: "To the store"
machines_filters: machines_filters:
show_machines: "Show machines" show_machines: "Show machines:"
status_enabled: "Enabled" status_enabled: "Enabled"
status_disabled: "Disabled" status_disabled: "Disabled"
status_all: "All" status_all: "All"
filter_by_machine_category: "Filter by category" filter_by_machine_category: "Filter by category:"
all_machines: "All machines" all_machines: "All machines"
machine_card: machine_card:
book: "Book" book: "Book"