app: Add modals and begin implementing donations
This patch adds a mechanism to open a modal, and roughs out a donation flow using the 'braintree' payments provider.
This commit is contained in:
@@ -21,6 +21,10 @@
|
|||||||
"repository": "github:phosphor-icons/phosphor-home",
|
"repository": "github:phosphor-icons/phosphor-home",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/braintree-web": "^3.75.3",
|
||||||
|
"@types/braintree-web-drop-in": "^1.22.3",
|
||||||
|
"braintree-web": "^3.78.2",
|
||||||
|
"braintree-web-drop-in": "^1.30.1",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.2",
|
||||||
"framer-motion": "^3.10.0",
|
"framer-motion": "^3.10.0",
|
||||||
"fuse.js": "^6.4.1",
|
"fuse.js": "^6.4.1",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ button {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.main-button,
|
||||||
button.main-button {
|
button.main-button {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
padding: 0 48px 0 40px;
|
padding: 0 48px 0 40px;
|
||||||
@@ -72,19 +73,29 @@ button.main-button {
|
|||||||
margin: 0 24px 24px 0;
|
margin: 0 24px 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.main-button:active,
|
||||||
button.main-button:active {
|
button.main-button:active {
|
||||||
transform: translate(4px, 4px);
|
transform: translate(4px, 4px);
|
||||||
box-shadow: 0 0 0 0 black;
|
box-shadow: 0 0 0 0 black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.main-button:focus,
|
||||||
button.main-button:focus {
|
button.main-button:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.main-button:disabled,
|
||||||
|
button.main-button:disabled {
|
||||||
|
border: 2px solid gray;
|
||||||
|
box-shadow: 4px 4px 0 0 gray;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* button.main-button:not(:last-child) {
|
/* button.main-button:not(:last-child) {
|
||||||
margin: 0 24px 24px 0;
|
margin: 0 24px 24px 0;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
input.main-button,
|
||||||
button.main-button svg {
|
button.main-button svg {
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
}
|
}
|
||||||
@@ -114,3 +125,55 @@ a.main-link:after {
|
|||||||
a.main-link:hover:after {
|
a.main-link:hover:after {
|
||||||
width: 0%;
|
width: 0%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.text-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.text-button svg {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: space-around;
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 50px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid black;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button label:hover {
|
||||||
|
background-color: #E0E0E0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button input[type="radio"]:checked + label {
|
||||||
|
color: white;
|
||||||
|
background-color: black;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button label input {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import React, { Suspense } from "react";
|
|||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import Header from "../Header/Header";
|
import Header from "../Header/Header";
|
||||||
|
import Modal from "../Modal/Modal";
|
||||||
import Toolbar from "../Toolbar/Toolbar";
|
import Toolbar from "../Toolbar/Toolbar";
|
||||||
import IconGrid from "../IconGrid/IconGrid";
|
import IconGrid from "../IconGrid/IconGrid";
|
||||||
import Footer from "../Footer/Footer";
|
import Footer from "../Footer/Footer";
|
||||||
@@ -10,6 +11,7 @@ import Notice from "../Notice/Notice";
|
|||||||
import useIconParameters from "../../hooks/useIconParameters";
|
import useIconParameters from "../../hooks/useIconParameters";
|
||||||
|
|
||||||
const errorFallback = <Notice message="Search error" />;
|
const errorFallback = <Notice message="Search error" />;
|
||||||
|
const paymentFallback = <Notice message="Could not connect to payments" />;
|
||||||
const waitingFallback = <Notice type="none" message="" />;
|
const waitingFallback = <Notice type="none" message="" />;
|
||||||
|
|
||||||
const App: React.FC<any> = () => {
|
const App: React.FC<any> = () => {
|
||||||
@@ -26,6 +28,9 @@ const App: React.FC<any> = () => {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</main>
|
</main>
|
||||||
|
<Suspense fallback={paymentFallback}>
|
||||||
|
<Modal />
|
||||||
|
</Suspense>
|
||||||
<Footer />
|
<Footer />
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { OutboundLink } from "react-ga";
|
import { OutboundLink } from "react-ga";
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
import { ArrowElbowDownRight } from "phosphor-react";
|
import { ArrowElbowDownRight } from "phosphor-react";
|
||||||
|
|
||||||
import { iconCount } from "../../lib/icons";
|
import { iconCount } from "../../lib/icons";
|
||||||
|
import { modalOpenAtom } from "../../state/atoms";
|
||||||
|
|
||||||
import "./Links.css";
|
import "./Links.css";
|
||||||
|
|
||||||
interface LinksProps {}
|
interface LinksProps {}
|
||||||
|
|
||||||
const Links: React.FC<LinksProps> = () => {
|
const Links: React.FC<LinksProps> = () => {
|
||||||
|
const setModalOpen = useSetRecoilState(modalOpenAtom);
|
||||||
|
|
||||||
|
const openDonationModal = () => setTimeout(() => setModalOpen(true), 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="links">
|
<div className="links">
|
||||||
<div>
|
<div>
|
||||||
@@ -19,28 +25,29 @@ const Links: React.FC<LinksProps> = () => {
|
|||||||
eventLabel="Download all"
|
eventLabel="Download all"
|
||||||
download
|
download
|
||||||
type="application/zip"
|
type="application/zip"
|
||||||
|
onClick={openDonationModal}
|
||||||
>
|
>
|
||||||
Download all ({iconCount})
|
Download all ({iconCount})
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
<span>
|
<span>
|
||||||
<OutboundLink
|
<OutboundLink
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
to="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
|
to="https://www.figma.com/community/file/903830135544202908/Phosphor-Icons"
|
||||||
eventLabel="Figma library"
|
eventLabel="Figma library"
|
||||||
>
|
>
|
||||||
Figma library
|
Figma library
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
{" / "}
|
{" / "}
|
||||||
<OutboundLink
|
<OutboundLink
|
||||||
className="nav-link"
|
className="nav-link"
|
||||||
to="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
|
to="https://www.figma.com/community/plugin/898620911119764089/Phosphor-Icons"
|
||||||
eventLabel="Figma plugin"
|
eventLabel="Figma plugin"
|
||||||
>
|
>
|
||||||
plugin
|
plugin
|
||||||
</OutboundLink>
|
</OutboundLink>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -66,7 +73,11 @@ const Links: React.FC<LinksProps> = () => {
|
|||||||
</div> */}
|
</div> */}
|
||||||
<div>
|
<div>
|
||||||
<ArrowElbowDownRight size={24} />
|
<ArrowElbowDownRight size={24} />
|
||||||
<a className="nav-link" href="https://paypal.me/minoraxis">
|
<a
|
||||||
|
className="nav-link"
|
||||||
|
href="https://paypal.me/minoraxis"
|
||||||
|
onClick={openDonationModal}
|
||||||
|
>
|
||||||
Donate on PayPal
|
Donate on PayPal
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
68
src/components/Modal/DonationModal.tsx
Normal file
68
src/components/Modal/DonationModal.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useSetRecoilState } from "recoil";
|
||||||
|
import { X } from "phosphor-react";
|
||||||
|
|
||||||
|
import { modalOpenAtom } from "../../state/atoms";
|
||||||
|
import DonationStepMethod from "./DonationStepMethod";
|
||||||
|
import DonationStepDropin from "./DonationStepDropin";
|
||||||
|
import DonationStepThanks from "./DonationStepThanks";
|
||||||
|
|
||||||
|
const routes = [DonationStepMethod, DonationStepDropin, DonationStepThanks];
|
||||||
|
|
||||||
|
export interface StepProps {
|
||||||
|
previousStep: () => void;
|
||||||
|
nextStep: () => void;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StepperProps {
|
||||||
|
routes: Array<React.FC<StepProps>>;
|
||||||
|
routeProps: any;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stepper: React.FC<StepperProps> = ({ routes, routeProps, close }) => {
|
||||||
|
const [currentStep, setCurrentStep] = useState<number>(0);
|
||||||
|
|
||||||
|
const previousStep = () => {
|
||||||
|
if (currentStep <= 0) return;
|
||||||
|
setTimeout(() => setCurrentStep((c) => c - 1), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep >= routes.length - 1) return;
|
||||||
|
setTimeout(() => setCurrentStep((c) => c + 1), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Component = routes[currentStep];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Component
|
||||||
|
previousStep={previousStep}
|
||||||
|
nextStep={nextStep}
|
||||||
|
close={close}
|
||||||
|
{...routeProps}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DonationModal: React.FC<{}> = () => {
|
||||||
|
const setModalOpen = useSetRecoilState(modalOpenAtom);
|
||||||
|
const close = () => setModalOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-content">
|
||||||
|
<button
|
||||||
|
className="modal-close-button"
|
||||||
|
onClick={() => setModalOpen(false)}
|
||||||
|
>
|
||||||
|
<X size={32} />
|
||||||
|
</button>
|
||||||
|
<Stepper routes={routes} routeProps={{}} close={close} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DonationModal;
|
||||||
80
src/components/Modal/DonationStepCC.tsx
Normal file
80
src/components/Modal/DonationStepCC.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useRef, useEffect } from "react";
|
||||||
|
import { client, hostedFields, Client, HostedFields } from "braintree-web";
|
||||||
|
|
||||||
|
const BT_STYLES = {
|
||||||
|
":focus": {
|
||||||
|
color: "blue",
|
||||||
|
},
|
||||||
|
".valid": {
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
".invalid": {
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const BT_PAYMENT_FIELDS = {
|
||||||
|
number: {
|
||||||
|
container: "#card-number",
|
||||||
|
placeholder: "4111 1111 1111 1111",
|
||||||
|
},
|
||||||
|
cvv: {
|
||||||
|
container: "#cvv",
|
||||||
|
placeholder: "123",
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
container: "#expiration-date",
|
||||||
|
placeholder: "10/2022",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaymentModal: React.FC<{}> = () => {
|
||||||
|
const bt = useRef<Client>();
|
||||||
|
const form = useRef<HostedFields>();
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!form.current || !bt.current) return;
|
||||||
|
|
||||||
|
const response = await form.current.tokenize();
|
||||||
|
console.log({ response });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeBt = async () => {
|
||||||
|
try {
|
||||||
|
bt.current = await client.create({
|
||||||
|
authorization: "sandbox_246jdjxq_8h7hm5rvngkykjds",
|
||||||
|
});
|
||||||
|
|
||||||
|
form.current = await hostedFields.create({
|
||||||
|
client: bt.current!!,
|
||||||
|
fields: BT_PAYMENT_FIELDS,
|
||||||
|
styles: BT_STYLES,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeBt();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form action="/" method="post" onSubmit={handleSubmit}>
|
||||||
|
<label htmlFor="card-number">Card Number</label>
|
||||||
|
<div id="card-number"></div>
|
||||||
|
|
||||||
|
<label htmlFor="cvv">CVV</label>
|
||||||
|
<div id="cvv"></div>
|
||||||
|
|
||||||
|
<label htmlFor="expiration-date">Expiration Date</label>
|
||||||
|
<div id="expiration-date"></div>
|
||||||
|
|
||||||
|
<div className="step-button-container">
|
||||||
|
<input type="submit" value="Pay" className="main-button" disabled />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentModal;
|
||||||
85
src/components/Modal/DonationStepDropin.tsx
Normal file
85
src/components/Modal/DonationStepDropin.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { useRef, useState, useEffect } from "react";
|
||||||
|
import dropin, { Dropin } from "braintree-web-drop-in";
|
||||||
|
import { StepProps } from "./DonationModal";
|
||||||
|
|
||||||
|
const BT_PAYMENT_FIELDS = {
|
||||||
|
number: {
|
||||||
|
placeholder: "4111 1111 1111 1111",
|
||||||
|
},
|
||||||
|
cvv: {
|
||||||
|
placeholder: "123",
|
||||||
|
},
|
||||||
|
expirationDate: {
|
||||||
|
placeholder: "10/22",
|
||||||
|
},
|
||||||
|
cardholderName: {
|
||||||
|
placeholder: "Person McFace",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const PaymentModal: React.FC<StepProps> = ({ previousStep }) => {
|
||||||
|
const instance = useRef<Dropin>();
|
||||||
|
const [isValid, setIsValid] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
if (!instance.current) return;
|
||||||
|
const payload = await instance.current.requestPaymentMethod();
|
||||||
|
console.log({ payload });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initializePayments = async () => {
|
||||||
|
try {
|
||||||
|
instance.current = await dropin.create({
|
||||||
|
authorization: "sandbox_246jdjxq_8h7hm5rvngkykjds",
|
||||||
|
container: "#braintree-dropin",
|
||||||
|
card: {
|
||||||
|
cardholderName: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
fields: BT_PAYMENT_FIELDS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paypal: {
|
||||||
|
flow: "checkout",
|
||||||
|
amount: "10.00",
|
||||||
|
currency: "USD",
|
||||||
|
commit: true,
|
||||||
|
},
|
||||||
|
paypalCredit: {
|
||||||
|
flow: "checkout",
|
||||||
|
amount: "10.00",
|
||||||
|
currency: "USD",
|
||||||
|
commit: true,
|
||||||
|
},
|
||||||
|
venmo: { allowNewBrowserTab: false },
|
||||||
|
});
|
||||||
|
instance.current.on("paymentMethodRequestable", () => setIsValid(true));
|
||||||
|
instance.current.on("noPaymentMethodRequestable", () =>
|
||||||
|
setIsValid(false)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializePayments();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="braintree-dropin"></div>
|
||||||
|
<div className="step-button-container">
|
||||||
|
<button className="main-button" onClick={previousStep}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button className="main-button" onClick={submit} disabled={!isValid}>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentModal;
|
||||||
131
src/components/Modal/DonationStepMethod.tsx
Normal file
131
src/components/Modal/DonationStepMethod.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
import { StepProps } from "./DonationModal";
|
||||||
|
|
||||||
|
enum DonationAmount {
|
||||||
|
FIVE_DOLLARS = 5,
|
||||||
|
TEN_DOLLARS = 10,
|
||||||
|
TWENTY_DOLLARS = 20,
|
||||||
|
FIFTY_DOLLARS = 50,
|
||||||
|
ONE_HUNDRED_DOLLARS = 100,
|
||||||
|
CUSTOM = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
const DonationStepMethod: React.FC<StepProps> = ({
|
||||||
|
previousStep,
|
||||||
|
nextStep,
|
||||||
|
close,
|
||||||
|
}) => {
|
||||||
|
const [donationType, setDonationType] = useState<DonationAmount>();
|
||||||
|
const [donationAmount, setDonationAmount] = useState<number>(0);
|
||||||
|
|
||||||
|
const onDonationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setDonationType(+e.target.value as DonationAmount);
|
||||||
|
if (!(+e.target.value === DonationAmount.CUSTOM))
|
||||||
|
setDonationAmount(+e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
void previousStep;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
Thanks for using Phosphor! Would you like to make a donation to help
|
||||||
|
support our work?
|
||||||
|
</h3>
|
||||||
|
<div className="radio-group">
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-5"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.FIVE_DOLLARS}
|
||||||
|
checked={donationType === DonationAmount.FIVE_DOLLARS}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-5">$5</label>
|
||||||
|
</div>
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-10"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.TEN_DOLLARS}
|
||||||
|
checked={donationType === DonationAmount.TEN_DOLLARS}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-10">$10</label>
|
||||||
|
</div>
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-20"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.TWENTY_DOLLARS}
|
||||||
|
checked={donationType === DonationAmount.TWENTY_DOLLARS}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-20">$20</label>
|
||||||
|
</div>
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-50"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.FIFTY_DOLLARS}
|
||||||
|
checked={donationType === DonationAmount.FIFTY_DOLLARS}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-50">$50</label>
|
||||||
|
</div>
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-100"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.ONE_HUNDRED_DOLLARS}
|
||||||
|
checked={donationType === DonationAmount.ONE_HUNDRED_DOLLARS}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-100">$100</label>
|
||||||
|
</div>
|
||||||
|
<div className="radio-button">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="donate-custom"
|
||||||
|
name="donation-type"
|
||||||
|
value={DonationAmount.CUSTOM}
|
||||||
|
checked={donationType === DonationAmount.CUSTOM}
|
||||||
|
onChange={onDonationChange}
|
||||||
|
/>
|
||||||
|
<label htmlFor="donate-custom">
|
||||||
|
Other: $
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={2}
|
||||||
|
max={1000000}
|
||||||
|
id="donate-custom"
|
||||||
|
value={donationAmount}
|
||||||
|
disabled={donationType !== DonationAmount.CUSTOM}
|
||||||
|
onChange={(e) => setDonationAmount(+e.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="step-button-container">
|
||||||
|
<button className="main-button" onClick={close}>
|
||||||
|
No thanks
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="main-button"
|
||||||
|
onClick={nextStep}
|
||||||
|
disabled={!(donationType && donationAmount)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DonationStepMethod;
|
||||||
7
src/components/Modal/DonationStepThanks.tsx
Normal file
7
src/components/Modal/DonationStepThanks.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const DonationStepThanks: React.FC<{}> = () => {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DonationStepThanks;
|
||||||
67
src/components/Modal/Modal.css
Normal file
67
src/components/Modal/Modal.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
.modal-container {
|
||||||
|
position: fixed;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
inset: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.4);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 400px;
|
||||||
|
min-height: 600px;
|
||||||
|
/* background-color: #ffe8dc; */
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid black;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.modal-content {
|
||||||
|
min-width: 90%;
|
||||||
|
min-height: 100%;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-button-container {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.modal-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 24px;
|
||||||
|
right: 24px;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: content-box;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-methods button > svg {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
25
src/components/Modal/Modal.tsx
Normal file
25
src/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useRecoilState } from "recoil";
|
||||||
|
import { useLockBodyScroll } from "react-use";
|
||||||
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
|
|
||||||
|
import { modalOpenAtom } from "../../state/atoms";
|
||||||
|
import DonationModal from "./DonationModal";
|
||||||
|
|
||||||
|
import "./Modal.css";
|
||||||
|
|
||||||
|
const Modal: React.FC<{}> = () => {
|
||||||
|
const [isModalOpen, setModalOpen] = useRecoilState(modalOpenAtom);
|
||||||
|
useHotkeys("esc", () => isModalOpen && setModalOpen(false), [isModalOpen]);
|
||||||
|
useLockBodyScroll(isModalOpen);
|
||||||
|
|
||||||
|
if (!isModalOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-container">
|
||||||
|
<DonationModal />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@@ -25,3 +25,8 @@ export const iconPreviewOpenAtom = atom<string | false>({
|
|||||||
key: "iconPreviewOpenAtom",
|
key: "iconPreviewOpenAtom",
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const modalOpenAtom = atom<boolean>({
|
||||||
|
key: "modalOpenAtom",
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user