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

use FabModal component to display ful schedule

This commit is contained in:
Sylvain 2020-11-09 15:17:38 +01:00
parent 0fe4f13110
commit 0c456b153e
16 changed files with 324 additions and 37 deletions

View File

@ -0,0 +1,12 @@
import axios, { AxiosInstance } from 'axios'
const token: HTMLMetaElement = document.querySelector('[name="csrf-token"]');
const client: AxiosInstance = axios.create({
headers: {
common: {
'X-CSRF-Token': token?.content || 'no-csrf-token'
}
}
})
export default client;

View File

@ -0,0 +1,17 @@
import apiClient from './api-client';
import { AxiosResponse } from 'axios';
import { CustomAsset } from '../models/custom-asset';
import wrapPromise, { IWrapPromise } from '../lib/wrap-promise';
export default class CustomAssetAPI {
async get (name: string): Promise<CustomAsset> {
const res: AxiosResponse = await apiClient.get(`/api/custom_assets/${name}`);
return res?.data?.custom_asset;
}
static get (name: string): IWrapPromise<CustomAsset> {
const api = new CustomAssetAPI();
return wrapPromise(api.get(name));
}
}

View File

@ -0,0 +1,49 @@
/**
* This component is a modal dialog that can wraps the application style
*/
import React from 'react';
import Modal from 'react-modal';
import { useTranslation } from 'react-i18next';
import { Loader } from './loader';
import CustomAsset from '../api/custom-asset';
Modal.setAppElement('body');
interface FabModalProps {
title: string,
isOpen: boolean,
toggleModal: () => void
}
const blackLogoFile = CustomAsset.get('logo-black-file');
export const FabModal: React.FC<FabModalProps> = ({ title, isOpen, toggleModal, children }) => {
const { t } = useTranslation('shared');
const blackLogo = blackLogoFile.read();
return (
<Modal isOpen={isOpen}
className="fab-modal"
overlayClassName="fab-modal-overlay"
onRequestClose={toggleModal}>
<div className="fab-modal-header">
<Loader>
<img src={blackLogo.custom_asset_file_attributes.attachment_url}
alt={blackLogo.custom_asset_file_attributes.attachment}
className="modal-logo" />
</Loader>
<h1>{ title }</h1>
</div>
<div className="fab-modal-content">
{children}
</div>
<div className="fab-modal-footer">
<Loader>
<button className="close-modal-btn" onClick={toggleModal}>{t('app.shared.buttons.close')}</button>
</Loader>
</div>
</Modal>
);
}

View File

@ -0,0 +1,20 @@
/**
* This component is a wrapper that display a loader while the children components have their rendering suspended
*/
import React, { Suspense } from 'react';
export const Loader: React.FC = ({children }) => {
const loading = (
<div className="fa-3x">
<i className="fas fa-circle-notch fa-spin" />
</div>
);
return (
<Suspense fallback={loading}>
{children}
</Suspense>
);
}

View File

@ -2,15 +2,16 @@
* This component displays a summary of the monthly payment schedule for the current cart, with a subscription
*/
import React, { useState, Suspense } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Modal from 'react-modal';
import { react2angular } from 'react2angular';
import moment from 'moment';
import { IApplication } from '../models/application';
import '../lib/i18n';
import { IFilterService } from 'angular';
import { PaymentSchedule } from '../models/payment-schedule';
import { Loader } from './loader';
import { FabModal } from './fab-modal';
declare var Application: IApplication;
@ -71,33 +72,27 @@ const PaymentScheduleSummary: React.FC<PaymentScheduleSummaryProps> = ({ schedul
</span>
</li>
</ul>}
<a className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.cart.view_full_schedule')}</a>
{/* TODO, create a component FabModal and put this inside */}
<Modal isOpen={modal}
className="full-schedule-modal"
onRequestClose={toggleFullScheduleModal}>
<button className="view-full-schedule" onClick={toggleFullScheduleModal}>{t('app.shared.cart.view_full_schedule')}</button>
<FabModal title={t('app.shared.cart.your_payment_schedule')} isOpen={modal} toggleModal={toggleFullScheduleModal}>
<ul className="full-schedule">
{schedule.items.map(item => (
<li>
<li key={String(item.due_date)}>
<span className="schedule-item-date">{formatDate(item.due_date)}</span>
<span> </span>
<span className="schedule-item-price">{formatPrice(item.price)}</span>
</li>
))}
</Modal>
</ul>
</FabModal>
</div>
</div>
);
}
const PaymentScheduleSummaryWrapper: React.FC<PaymentScheduleSummaryProps> = ({ schedule, $filter }) => {
const loading = (
<div className="fa-3x">
<i className="fas fa-circle-notch fa-spin" />
</div>
);
return (
<Suspense fallback={loading}>
<Loader>
<PaymentScheduleSummary schedule={schedule} $filter={$filter} />
</Suspense>
</Loader>
);
}

View File

@ -2,7 +2,7 @@
* This component is a "card" publicly presenting the details of a plan
*/
import React, { Suspense } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { IFilterService } from 'angular';
@ -11,6 +11,7 @@ import _ from 'lodash'
import { IApplication } from '../models/application';
import { Plan } from '../models/plan';
import { User, UserRole } from '../models/user';
import { Loader } from './loader';
import '../lib/i18n';
declare var Application: IApplication;
@ -122,15 +123,10 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan,
}
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => {
const loading = (
<div className="fa-3x">
<i className="fas fa-circle-notch fa-spin" />
</div>
);
return (
<Suspense fallback={loading}>
<Loader>
<PlanCard plan={plan} user={user} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} $filter={$filter} />
</Suspense>
</Loader>
);
}

View File

@ -3,11 +3,12 @@
* or with a one time payment
*/
import React, { Suspense } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import Switch from 'react-switch';
import { IApplication } from '../models/application';
import { Loader } from './loader';
import '../lib/i18n';
declare var Application: IApplication;
@ -26,22 +27,17 @@ const SelectSchedule: React.FC<SelectScheduleProps> = ({ show, selected, onChang
<div className="select-schedule">
{show && <div className={className}>
<label htmlFor="payment_schedule">{ t('app.shared.cart.monthly_payment') }</label>
<Switch checked={selected} id="payment_schedule" onChange={onChange} className="schedule-switch"></Switch>
<Switch checked={selected} id="payment_schedule" onChange={onChange} className="schedule-switch" />
</div>}
</div>
);
}
const SelectScheduleWrapper: React.FC<SelectScheduleProps> = ({ show, selected, onChange, className }) => {
const loading = (
<div className="fa-3x">
<i className="fas fa-circle-notch fa-spin" />
</div>
);
return (
<Suspense fallback={loading}>
<Loader>
<SelectSchedule show={show} selected={selected} onChange={onChange} className={className} />
</Suspense>
</Loader>
);
}

View File

@ -1,3 +1,6 @@
/**
* This is a compatibility wrapper to allow usage of react-switch inside of the angular.js app
*/
import Switch from 'react-switch';
import { react2angular } from 'react2angular';
import { IApplication } from '../models/application';

View File

@ -0,0 +1,37 @@
/**
* This function wraps a Promise to make it compatible with react Suspense
*/
export interface IWrapPromise<T> {
read: () => T
}
function wrapPromise(promise: Promise<any>): IWrapPromise<any> {
let status: string = 'pending';
let response: any;
const suspender: Promise<any> = promise.then(
(res) => {
status = 'success'
response = res
},
(err) => {
status = 'error'
response = err
},
);
const read = (): any => {
switch (status) {
case 'pending':
throw suspender
case 'error':
throw response
default:
return response
}
};
return { read };
}
export default wrapPromise;

View File

@ -0,0 +1,9 @@
export interface CustomAsset {
id: number,
name: string,
custom_asset_file_attributes: {
id: number,
attachment: string
attachment_url: string
}
}

View File

@ -21,5 +21,7 @@
@import "modules/signup";
@import "modules/stripe";
@import "modules/tour";
@import "modules/fab-modal";
@import "modules/payment-schedule-summary";
@import "app.responsive";

View File

@ -0,0 +1,89 @@
@keyframes slideInFromTop {
0% { transform: translate(0, -25%); }
100% { transform: translate(0, 0); }
}
@keyframes fadeIn {
0% { opacity: 0; }
100% { opacity: 0.9; }
}
.fab-modal-overlay {
z-index: 1050;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.9);
animation: 0.15s linear fadeIn;
}
.fab-modal {
animation: 0.3s ease-out slideInFromTop;
position: relative;
top: 90px;
width: 340px;
margin: auto;
opacity: 1;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
outline: 0;
.fab-modal-header {
padding: 8px;
border-bottom: 1px solid #e5e5e5;
.modal-logo {
position: absolute;
top: -70px;
left: 0;
right: 0;
margin: 0 auto;
max-height: 44px;
}
h1 {
margin: 25px 0 20px 0;
font-weight: bold;
text-transform: uppercase;
text-align: center;
}
}
.fab-modal-content {
position: relative;
padding: 15px;
}
.fab-modal-footer {
padding: 15px;
text-align: right;
border-top: 1px solid #e5e5e5;
.close-modal-btn {
color: black;
background-color: #fbfbfb;
margin-bottom: 0;
margin-left: 5px;
display: inline-block;
font-weight: normal;
text-align: center;
white-space: nowrap;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
background-image: none;
border: 1px solid #c9c9c9;
padding: 6px 12px;
font-size: 16px;
line-height: 1.5;
border-radius: 4px;
&:hover {
background-color: #f2f2f2;
}
}
}
}

View File

@ -1,6 +1,58 @@
.payment-schedule-summary {
.full-schedule-modal {
@extend .modal;
@extend .fade;
h4 {
margin-left: 2em;
}
ul {
list-style: none;
padding-left: 2em;
li {
position: relative;
margin-bottom: 0.75em;
.schedule-item-info {
display: block;
border-bottom: 1px solid #ddd;
margin-right: 2em;
}
.schedule-item-price {
position: absolute;
top: 0;
right: 2em;
}
.schedule-item-date {
display: block;
font-size: 0.8em;
}
}
}
.view-full-schedule {
margin-left: 1em;
font-size: 0.8em;
border: 0;
background: transparent;
margin-bottom: 2em;
&:before {
content: '\f06e';
font-family: 'Font Awesome 5 Free';
font-weight: 900;
margin-right: 1em;
}
}
}
.full-schedule {
list-style: none;
li {
border-bottom: 1px solid #ddd;
margin-right: 3em;
}
.schedule-item-price {
color: #5a5a5a;
float: right;
}
}

View File

@ -103,6 +103,7 @@ a.reinit-filters:hover,
a.project-author:hover,
a.dsq-brlink:hover,
.widget .widget-content a:hover,
.payment-schedule-summary .view-full-schedule:hover,
.alert a:hover,
.about-fablab a:hover,
a.collected-infos:hover {
@ -166,6 +167,7 @@ h5:after {
}
.modal-header h1,
.fab-modal-header h1,
.custom-invoice .modal-header h1 {
color: $primary;
}

View File

@ -73,6 +73,7 @@
"angular-ui-tour": "https://github.com/sleede/angular-ui-tour.git#master",
"angular-unsavedchanges": "0.2",
"angular-xeditable": "0.10",
"axios": "^0.21.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"bootstrap-sass": "3.4.1",
"checklist-model": "0.2",

View File

@ -1905,6 +1905,13 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.1.tgz#e1e82e4f3e999e2cfd61b161280d16a111f86428"
integrity sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==
axios@^0.21.0:
version "0.21.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.0.tgz#26df088803a2350dff2c27f96fef99fe49442aca"
integrity sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==
dependencies:
follow-redirects "^1.10.0"
babel-loader@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.1.0.tgz#c611d5112bd5209abe8b9fa84c3e4da25275f1c3"
@ -4094,7 +4101,7 @@ flush-write-stream@^1.0.0:
inherits "^2.0.3"
readable-stream "^2.3.6"
follow-redirects@^1.0.0:
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==