diff --git a/app/frontend/src/javascript/api/api-client.ts b/app/frontend/src/javascript/api/api-client.ts new file mode 100644 index 000000000..d7766794f --- /dev/null +++ b/app/frontend/src/javascript/api/api-client.ts @@ -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; diff --git a/app/frontend/src/javascript/api/custom-asset.ts b/app/frontend/src/javascript/api/custom-asset.ts new file mode 100644 index 000000000..a069ab0ba --- /dev/null +++ b/app/frontend/src/javascript/api/custom-asset.ts @@ -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 { + const res: AxiosResponse = await apiClient.get(`/api/custom_assets/${name}`); + return res?.data?.custom_asset; + } + + static get (name: string): IWrapPromise { + const api = new CustomAssetAPI(); + return wrapPromise(api.get(name)); + } +} + diff --git a/app/frontend/src/javascript/components/fab-modal.tsx b/app/frontend/src/javascript/components/fab-modal.tsx new file mode 100644 index 000000000..5fabfe7f4 --- /dev/null +++ b/app/frontend/src/javascript/components/fab-modal.tsx @@ -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 = ({ title, isOpen, toggleModal, children }) => { + const { t } = useTranslation('shared'); + const blackLogo = blackLogoFile.read(); + + return ( + +
+ + {blackLogo.custom_asset_file_attributes.attachment} + +

{ title }

+
+
+ {children} +
+
+ + + +
+
+ ); +} + diff --git a/app/frontend/src/javascript/components/loader.tsx b/app/frontend/src/javascript/components/loader.tsx new file mode 100644 index 000000000..ff933f86d --- /dev/null +++ b/app/frontend/src/javascript/components/loader.tsx @@ -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 = ( +
+ +
+ ); + return ( + + {children} + + ); +} + diff --git a/app/frontend/src/javascript/components/payment-schedule-summary.tsx b/app/frontend/src/javascript/components/payment-schedule-summary.tsx index 4416e4843..2b31400ba 100644 --- a/app/frontend/src/javascript/components/payment-schedule-summary.tsx +++ b/app/frontend/src/javascript/components/payment-schedule-summary.tsx @@ -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 = ({ schedul } - {t('app.shared.cart.view_full_schedule')} - {/* TODO, create a component FabModal and put this inside */} - + + +
    {schedule.items.map(item => ( -
  • +
  • {formatDate(item.due_date)} {formatPrice(item.price)}
  • ))} - +
+
); } const PaymentScheduleSummaryWrapper: React.FC = ({ schedule, $filter }) => { - const loading = ( -
- -
- ); return ( - + - + ); } diff --git a/app/frontend/src/javascript/components/plan-card.tsx b/app/frontend/src/javascript/components/plan-card.tsx index f4b8a78ec..9fb38cdb6 100644 --- a/app/frontend/src/javascript/components/plan-card.tsx +++ b/app/frontend/src/javascript/components/plan-card.tsx @@ -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 = ({ plan, user, operator, onSelectPlan, } const PlanCardWrapper: React.FC = ({ plan, user, operator, onSelectPlan, isSelected, $filter }) => { - const loading = ( -
- -
- ); return ( - + - + ); } diff --git a/app/frontend/src/javascript/components/select-schedule.tsx b/app/frontend/src/javascript/components/select-schedule.tsx index 6bb754063..982fc05e4 100644 --- a/app/frontend/src/javascript/components/select-schedule.tsx +++ b/app/frontend/src/javascript/components/select-schedule.tsx @@ -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 = ({ show, selected, onChang
{show &&
- +
}
); } const SelectScheduleWrapper: React.FC = ({ show, selected, onChange, className }) => { - const loading = ( -
- -
- ); return ( - + - + ); } diff --git a/app/frontend/src/javascript/components/switch.ts b/app/frontend/src/javascript/components/switch.ts index 660eece53..baeba811a 100644 --- a/app/frontend/src/javascript/components/switch.ts +++ b/app/frontend/src/javascript/components/switch.ts @@ -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'; diff --git a/app/frontend/src/javascript/lib/wrap-promise.ts b/app/frontend/src/javascript/lib/wrap-promise.ts new file mode 100644 index 000000000..663211b96 --- /dev/null +++ b/app/frontend/src/javascript/lib/wrap-promise.ts @@ -0,0 +1,37 @@ +/** + * This function wraps a Promise to make it compatible with react Suspense + */ +export interface IWrapPromise { + read: () => T +} + +function wrapPromise(promise: Promise): IWrapPromise { + let status: string = 'pending'; + let response: any; + + const suspender: Promise = 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; diff --git a/app/frontend/src/javascript/models/custom-asset.ts b/app/frontend/src/javascript/models/custom-asset.ts new file mode 100644 index 000000000..21db65406 --- /dev/null +++ b/app/frontend/src/javascript/models/custom-asset.ts @@ -0,0 +1,9 @@ +export interface CustomAsset { + id: number, + name: string, + custom_asset_file_attributes: { + id: number, + attachment: string + attachment_url: string + } +} diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index e64345818..c6a1d9f83 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -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"; diff --git a/app/frontend/src/stylesheets/modules/fab-modal.scss b/app/frontend/src/stylesheets/modules/fab-modal.scss new file mode 100644 index 000000000..9c11568fc --- /dev/null +++ b/app/frontend/src/stylesheets/modules/fab-modal.scss @@ -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; + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/payment-schedule-summary.scss b/app/frontend/src/stylesheets/modules/payment-schedule-summary.scss index 6e165dba4..93aaa2f8f 100644 --- a/app/frontend/src/stylesheets/modules/payment-schedule-summary.scss +++ b/app/frontend/src/stylesheets/modules/payment-schedule-summary.scss @@ -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; } } diff --git a/app/themes/casemate/style.scss.erb b/app/themes/casemate/style.scss.erb index 256549446..72b84aea9 100644 --- a/app/themes/casemate/style.scss.erb +++ b/app/themes/casemate/style.scss.erb @@ -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; } diff --git a/package.json b/package.json index 99676a169..e20c1db22 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 857753222..c7eb35284 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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==