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:
parent
0fe4f13110
commit
0c456b153e
12
app/frontend/src/javascript/api/api-client.ts
Normal file
12
app/frontend/src/javascript/api/api-client.ts
Normal 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;
|
17
app/frontend/src/javascript/api/custom-asset.ts
Normal file
17
app/frontend/src/javascript/api/custom-asset.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
|
49
app/frontend/src/javascript/components/fab-modal.tsx
Normal file
49
app/frontend/src/javascript/components/fab-modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
20
app/frontend/src/javascript/components/loader.tsx
Normal file
20
app/frontend/src/javascript/components/loader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
37
app/frontend/src/javascript/lib/wrap-promise.ts
Normal file
37
app/frontend/src/javascript/lib/wrap-promise.ts
Normal 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;
|
9
app/frontend/src/javascript/models/custom-asset.ts
Normal file
9
app/frontend/src/javascript/models/custom-asset.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface CustomAsset {
|
||||
id: number,
|
||||
name: string,
|
||||
custom_asset_file_attributes: {
|
||||
id: number,
|
||||
attachment: string
|
||||
attachment_url: string
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
89
app/frontend/src/stylesheets/modules/fab-modal.scss
Normal file
89
app/frontend/src/stylesheets/modules/fab-modal.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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==
|
||||
|
Loading…
Reference in New Issue
Block a user