app: Generify Modal and restructure imports

This commit is contained in:
rektdeckard
2021-06-21 17:07:52 -04:00
parent 8a7921d082
commit 7cd27c509b
43 changed files with 305 additions and 269 deletions

View File

@@ -21,9 +21,7 @@
"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", "@types/braintree-web-drop-in": "^1.22.3",
"braintree-web": "^3.78.2",
"braintree-web-drop-in": "^1.30.1", "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",

View File

@@ -144,7 +144,7 @@ button.text-button svg {
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-around; justify-content: space-around;
row-gap: 16px; row-gap: 8px;
} }
.radio-button input[type="radio"] { .radio-button input[type="radio"] {

View File

@@ -1,14 +1,16 @@
import React, { Suspense } from "react"; import React, { Suspense } from "react";
import Header from "components/Header";
import Modal from "components/Modal";
import Toolbar from "components/Toolbar";
import IconGrid from "components/IconGrid";
import Footer from "components/Footer";
import ErrorBoundary from "components/ErrorBoundary";
import Notice from "components/Notice";
import useIconParameters from "hooks/useIconParameters";
import "./App.css"; import "./App.css";
import Header from "../Header/Header";
import Modal from "../Modal/Modal";
import Toolbar from "../Toolbar/Toolbar";
import IconGrid from "../IconGrid/IconGrid";
import Footer from "../Footer/Footer";
import ErrorBoundary from "../ErrorBoundary/ErrorBoundary";
import Notice from "../Notice/Notice";
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 paymentFallback = <Notice message="Could not connect to payments" />;

View File

@@ -0,0 +1,2 @@
import App from "./App";
export default App;

View File

@@ -1,9 +1,10 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useRecoilState, useRecoilValue } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { iconColorAtom } from "../../state/atoms"; import { iconColorAtom } from "state/atoms";
import { isDarkThemeSelector } from "../../state/selectors"; import { isDarkThemeSelector } from "state/selectors";
import useThrottled from "../../hooks/useThrottled"; import useThrottled from "hooks/useThrottled";
import "./ColorInput.css"; import "./ColorInput.css";
type ColorInputProps = {}; type ColorInputProps = {};

View File

@@ -0,0 +1,2 @@
import ColorInput from "./ColorInput";
export default ColorInput;

View File

@@ -0,0 +1,2 @@
import ErrorBoundary from "./ErrorBoundary";
export default ErrorBoundary;

View File

@@ -1,10 +1,12 @@
import React from "react"; import React from "react";
import { Coffee, Heart } from "phosphor-react"; import { Coffee, Heart } from "phosphor-react";
import uArrowUpLeft from "../../assets/u-arrow-up-left.svg"; import uArrowUpLeft from "assets/u-arrow-up-left.svg";
import markerGreen from "../../assets/marker-green.svg"; import markerGreen from "assets/marker-green.svg";
import postIt from "../../assets/footer-mobile.svg"; import postIt from "assets/footer-mobile.svg";
import Links from "../Links/Links";
import Links from "components/Links";
import "./Footer.css"; import "./Footer.css";
type FooterProps = {}; type FooterProps = {};
@@ -34,8 +36,8 @@ const Footer: React.FC<FooterProps> = () => {
a little quirky, too. a little quirky, too.
</p> </p>
<p> <p>
We're thankful for the tools we've benefited from and We're thankful for the tools we've benefited from and this is our
this is our contribution towards a collaborative community. contribution towards a collaborative community.
</p> </p>
<p> <p>
Phosphor is free and open-source, licensed under{" "} Phosphor is free and open-source, licensed under{" "}

View File

@@ -0,0 +1,2 @@
import Footer from "./Footer";
export default Footer;

View File

@@ -1,22 +1,24 @@
import React from "react"; import React from "react";
import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react"; import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
import markerPurple from "../../assets/marker-purple.svg"; import markerPurple from "assets/marker-purple.svg";
import paperclips from "../../assets/paperclips-header-mobile.svg"; import paperclips from "assets/paperclips-header-mobile.svg";
import paperclipsThree from "../../assets/paperclips-header.svg"; import paperclipsThree from "assets/paperclips-header.svg";
import tablet from "../../assets/tablet.svg"; import tablet from "assets/tablet.svg";
import tabletSpec from "../../assets/tablet-spec.svg"; import tabletSpec from "assets/tablet-spec.svg";
import billiardBall from "../../assets/billiard-ball.svg"; import billiardBall from "assets/billiard-ball.svg";
import billiardBallSpec from "../../assets/billiard-ball-spec.svg"; import billiardBallSpec from "assets/billiard-ball-spec.svg";
import warning from "../../assets/warning.svg"; import warning from "assets/warning.svg";
import warningSpec from "../../assets/warning-spec.svg"; import warningSpec from "assets/warning-spec.svg";
import cuttingMat from "../../assets/cutting-mat.svg"; import cuttingMat from "assets/cutting-mat.svg";
import cuttingMatSpec from "../../assets/cutting-mat-spec.svg"; import cuttingMatSpec from "assets/cutting-mat-spec.svg";
import receipt from "../../assets/receipt.svg"; import receipt from "assets/receipt.svg";
import receiptSpec from "../../assets/receipt-spec.svg"; import receiptSpec from "assets/receipt-spec.svg";
import calculator from "../../assets/calculator.svg"; import calculator from "assets/calculator.svg";
import calculatorSpec from "../../assets/calculator-spec.svg"; import calculatorSpec from "assets/calculator-spec.svg";
import Links from "../Links/Links";
import Links from "components/Links";
import "./Header.css"; import "./Header.css";
type HeaderProps = {}; type HeaderProps = {};

View File

@@ -0,0 +1,2 @@
import Header from "./Header";
export default Header;

View File

@@ -3,18 +3,19 @@ import { useRecoilValue, useSetRecoilState } from "recoil";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { Svg2Png } from "svg2png-converter"; import { Svg2Png } from "svg2png-converter";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { Copy, X, CheckCircle, Download } from "phosphor-react";
import ReactGA from "react-ga"; import ReactGA from "react-ga";
import { Copy, X, CheckCircle, Download } from "phosphor-react";
import { import {
iconWeightAtom, iconWeightAtom,
iconSizeAtom, iconSizeAtom,
iconColorAtom, iconColorAtom,
iconPreviewOpenAtom, iconPreviewOpenAtom,
} from "../../state/atoms"; } from "state/atoms";
import useTransientState from "../../hooks/useTransientState"; import { IconEntry } from "lib";
import useTransientState from "hooks/useTransientState";
import TagCloud from "./TagCloud"; import TagCloud from "./TagCloud";
import { IconEntry } from "../../lib";
const panelVariants = { const panelVariants = {
open: { open: {

View File

@@ -3,18 +3,27 @@ import { useRecoilValue } from "recoil";
import { motion, useAnimation } from "framer-motion"; import { motion, useAnimation } from "framer-motion";
import { IconContext } from "phosphor-react"; import { IconContext } from "phosphor-react";
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms"; import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "state/atoms";
import { import {
filteredQueryResultsSelector, filteredQueryResultsSelector,
isDarkThemeSelector, isDarkThemeSelector,
} from "../../state/selectors"; } from "state/selectors";
import useGridSpans from "../../hooks/useGridSpans"; import useGridSpans from "hooks/useGridSpans";
import IconGridItem from "./IconGridItem"; import IconGridItem from "./IconGridItem";
import TagCloud from "./TagCloud"; import TagCloud from "./TagCloud";
import Notice from "../Notice/Notice"; import Notice from "components/Notice";
import "./IconGrid.css"; import "./IconGrid.css";
const defaultSearchTags = ["*new*", "communication", "editor", "emoji", "maps", "weather"]; const defaultSearchTags = [
"*new*",
"communication",
"editor",
"emoji",
"maps",
"weather",
];
type IconGridProps = {}; type IconGridProps = {};

View File

@@ -7,9 +7,9 @@ import React, {
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { iconPreviewOpenAtom } from "../../state/atoms"; import { IconEntry } from "lib";
import { iconPreviewOpenAtom } from "state/atoms";
import DetailsPanel from "./DetailsPanel"; import DetailsPanel from "./DetailsPanel";
import { IconEntry } from "../../lib";
interface IconGridItemProps { interface IconGridItemProps {
index: number; index: number;
@@ -86,9 +86,9 @@ const IconGridItem: React.FC<IconGridItemProps> = (props) => {
<Icon /> <Icon />
<p>{name}</p> <p>{name}</p>
</motion.div> </motion.div>
<AnimatePresence initial={false}> <AnimatePresence initial={false}>
{isOpen && <DetailsPanel {...props} />} {isOpen && <DetailsPanel {...props} />}
</AnimatePresence> </AnimatePresence>
</> </>
); );
}; };

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useSetRecoilState } from "recoil"; import { useSetRecoilState } from "recoil";
import { searchQueryAtom } from "../../state/atoms"; import { searchQueryAtom } from "state/atoms";
import "./TagCloud.css"; import "./TagCloud.css";
interface TagCloudProps { interface TagCloudProps {

View File

@@ -0,0 +1,2 @@
import IconGrid from "./IconGrid";
export default IconGrid;

View File

@@ -20,13 +20,14 @@
margin-right: 12px; margin-right: 12px;
} }
a.nav-link { .nav-link {
text-decoration: none; text-decoration: none;
position: relative; position: relative;
color: black; color: black;
cursor: pointer;
} }
a.nav-link:after { .nav-link:after {
content: ""; content: "";
position: absolute; position: absolute;
bottom: -2px; bottom: -2px;
@@ -36,6 +37,6 @@ a.nav-link:after {
transition: 0.2s; transition: 0.2s;
} }
a.nav-link:hover:after { .nav-link:hover:after {
width: 100%; width: 100%;
} }

View File

@@ -3,17 +3,19 @@ import { OutboundLink } from "react-ga";
import { useSetRecoilState } from "recoil"; 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 { modalSelector } from "state/selectors";
import DonationModal from "components/Modal/DonationModal";
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 openModal = useSetRecoilState(modalSelector);
const openDonationModal = () => openModal({ type: DonationModal });
const openDonationModal = () => setTimeout(() => setModalOpen(true), 1000); const delayedOpenDonationModal = () => setTimeout(openDonationModal, 2000);
return ( return (
<div className="links"> <div className="links">
@@ -25,7 +27,7 @@ const Links: React.FC<LinksProps> = () => {
eventLabel="Download all" eventLabel="Download all"
download download
type="application/zip" type="application/zip"
onClick={openDonationModal} onClick={delayedOpenDonationModal}
> >
Download all ({iconCount}) Download all ({iconCount})
</OutboundLink> </OutboundLink>
@@ -59,27 +61,11 @@ const Links: React.FC<LinksProps> = () => {
Request an icon Request an icon
</a> </a>
</div> </div>
{/* <div>
<ArrowElbowDownRight size={24} />
<span>
<a className="nav-link" href="https://paypal.me/minoraxis">
Donate on PayPal
</a>
{" / "}
<a className="nav-link" href="https://patreon.com/phosphoricons">
Patreon
</a>
</span>
</div> */}
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRight size={24} />
<a <span className="nav-link" onClick={openDonationModal}>
className="nav-link" Donate
href="https://paypal.me/minoraxis" </span>
onClick={openDonationModal}
>
Donate on PayPal
</a>
</div> </div>
<div> <div>
<ArrowElbowDownRight size={24} /> <ArrowElbowDownRight size={24} />

View File

@@ -0,0 +1,2 @@
import Links from "./Links";
export default Links;

View File

@@ -1,23 +1,40 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { useSetRecoilState } from "recoil";
import { X } from "phosphor-react";
import { modalOpenAtom } from "../../state/atoms"; import { ModalInstance } from "../Modal";
import DonationStepMethod from "./DonationStepMethod"; import DonationStepMethod from "./DonationStepMethod";
import DonationStepDropin from "./DonationStepDropin"; import DonationStepDropin from "./DonationStepDropin";
import DonationStepThanks from "./DonationStepThanks"; import DonationStepThanks from "./DonationStepThanks";
const routes = [DonationStepMethod, DonationStepDropin, DonationStepThanks]; const routes = [DonationStepMethod, DonationStepDropin, DonationStepThanks];
export enum DonationType {
FIVE_DOLLARS = 5,
TEN_DOLLARS = 10,
TWENTY_DOLLARS = 20,
FIFTY_DOLLARS = 50,
ONE_HUNDRED_DOLLARS = 100,
CUSTOM = -1,
}
interface RouteProps {
donationType: DonationType | undefined;
setDonationType: React.Dispatch<
React.SetStateAction<DonationType | undefined>
>;
donationAmount: number;
setDonationAmount: React.Dispatch<React.SetStateAction<number>>;
}
export interface StepProps { export interface StepProps {
previousStep: () => void; previousStep: () => void;
nextStep: () => void; nextStep: () => void;
close: () => void; close: () => void;
routeProps: RouteProps;
} }
interface StepperProps { interface StepperProps {
routes: Array<React.FC<StepProps>>; routes: Array<React.FC<StepProps>>;
routeProps: any; routeProps: RouteProps;
close: () => void; close: () => void;
} }
@@ -42,26 +59,32 @@ const Stepper: React.FC<StepperProps> = ({ routes, routeProps, close }) => {
previousStep={previousStep} previousStep={previousStep}
nextStep={nextStep} nextStep={nextStep}
close={close} close={close}
{...routeProps} routeProps={routeProps}
/> />
</> </>
); );
}; };
const DonationModal: React.FC<{}> = () => { const DonationModal = ({ close }: ModalInstance): JSX.Element => {
const setModalOpen = useSetRecoilState(modalOpenAtom); const [donationType, setDonationType] = useState<DonationType | undefined>();
const close = () => setModalOpen(false); const [donationAmount, setDonationAmount] = useState<number>(0);
return ( return (
<div className="modal-content"> <>
<button <div className="modal-titlebar">
className="modal-close-button" <h2>Donate</h2>
onClick={() => setModalOpen(false)} </div>
> <Stepper
<X size={32} /> routes={routes}
</button> routeProps={{
<Stepper routes={routes} routeProps={{}} close={close} /> donationType,
</div> setDonationType,
donationAmount,
setDonationAmount,
}}
close={close}
/>
</>
); );
}; };

View File

@@ -17,10 +17,12 @@ const BT_PAYMENT_FIELDS = {
}, },
}; };
const PaymentModal: React.FC<StepProps> = ({ previousStep }) => { const PaymentModal: React.FC<StepProps> = ({ previousStep, routeProps }) => {
const instance = useRef<Dropin>(); const instance = useRef<Dropin>();
const [isValid, setIsValid] = useState<boolean>(false); const [isValid, setIsValid] = useState<boolean>(false);
const { donationAmount } = routeProps;
const submit = async () => { const submit = async () => {
if (!instance.current) return; if (!instance.current) return;
const payload = await instance.current.requestPaymentMethod(); const payload = await instance.current.requestPaymentMethod();
@@ -43,13 +45,13 @@ const PaymentModal: React.FC<StepProps> = ({ previousStep }) => {
}, },
paypal: { paypal: {
flow: "checkout", flow: "checkout",
amount: "10.00", amount: donationAmount.toFixed(2).toString(),
currency: "USD", currency: "USD",
commit: true, commit: true,
}, },
paypalCredit: { paypalCredit: {
flow: "checkout", flow: "checkout",
amount: "10.00", amount: donationAmount.toFixed(2).toString(),
currency: "USD", currency: "USD",
commit: true, commit: true,
}, },
@@ -65,7 +67,7 @@ const PaymentModal: React.FC<StepProps> = ({ previousStep }) => {
}; };
initializePayments(); initializePayments();
}, []); }, [donationAmount]);
return ( return (
<> <>

View File

@@ -1,27 +1,19 @@
import React, { useState } from "react"; import React from "react";
import { StepProps } from "./DonationModal"; import { StepProps, DonationType } 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> = ({ const DonationStepMethod: React.FC<StepProps> = ({
previousStep, previousStep,
nextStep, nextStep,
close, close,
routeProps,
}) => { }) => {
const [donationType, setDonationType] = useState<DonationAmount>(); const { donationType, donationAmount, setDonationType, setDonationAmount } =
const [donationAmount, setDonationAmount] = useState<number>(0); routeProps;
const onDonationChange = (e: React.ChangeEvent<HTMLInputElement>) => { const onDonationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setDonationType(+e.target.value as DonationAmount); setDonationType(+e.target.value as DonationType);
if (!(+e.target.value === DonationAmount.CUSTOM)) if (!(+e.target.value === DonationType.CUSTOM))
setDonationAmount(+e.target.value); setDonationAmount(+e.target.value);
}; };
@@ -39,8 +31,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-5" id="donate-5"
name="donation-type" name="donation-type"
value={DonationAmount.FIVE_DOLLARS} value={DonationType.FIVE_DOLLARS}
checked={donationType === DonationAmount.FIVE_DOLLARS} checked={donationType === DonationType.FIVE_DOLLARS}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-5">$5</label> <label htmlFor="donate-5">$5</label>
@@ -50,8 +42,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-10" id="donate-10"
name="donation-type" name="donation-type"
value={DonationAmount.TEN_DOLLARS} value={DonationType.TEN_DOLLARS}
checked={donationType === DonationAmount.TEN_DOLLARS} checked={donationType === DonationType.TEN_DOLLARS}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-10">$10</label> <label htmlFor="donate-10">$10</label>
@@ -61,8 +53,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-20" id="donate-20"
name="donation-type" name="donation-type"
value={DonationAmount.TWENTY_DOLLARS} value={DonationType.TWENTY_DOLLARS}
checked={donationType === DonationAmount.TWENTY_DOLLARS} checked={donationType === DonationType.TWENTY_DOLLARS}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-20">$20</label> <label htmlFor="donate-20">$20</label>
@@ -72,8 +64,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-50" id="donate-50"
name="donation-type" name="donation-type"
value={DonationAmount.FIFTY_DOLLARS} value={DonationType.FIFTY_DOLLARS}
checked={donationType === DonationAmount.FIFTY_DOLLARS} checked={donationType === DonationType.FIFTY_DOLLARS}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-50">$50</label> <label htmlFor="donate-50">$50</label>
@@ -83,8 +75,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-100" id="donate-100"
name="donation-type" name="donation-type"
value={DonationAmount.ONE_HUNDRED_DOLLARS} value={DonationType.ONE_HUNDRED_DOLLARS}
checked={donationType === DonationAmount.ONE_HUNDRED_DOLLARS} checked={donationType === DonationType.ONE_HUNDRED_DOLLARS}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-100">$100</label> <label htmlFor="donate-100">$100</label>
@@ -94,8 +86,8 @@ const DonationStepMethod: React.FC<StepProps> = ({
type="radio" type="radio"
id="donate-custom" id="donate-custom"
name="donation-type" name="donation-type"
value={DonationAmount.CUSTOM} value={DonationType.CUSTOM}
checked={donationType === DonationAmount.CUSTOM} checked={donationType === DonationType.CUSTOM}
onChange={onDonationChange} onChange={onDonationChange}
/> />
<label htmlFor="donate-custom"> <label htmlFor="donate-custom">
@@ -106,7 +98,7 @@ const DonationStepMethod: React.FC<StepProps> = ({
max={1000000} max={1000000}
id="donate-custom" id="donate-custom"
value={donationAmount} value={donationAmount}
disabled={donationType !== DonationAmount.CUSTOM} disabled={donationType !== DonationType.CUSTOM}
onChange={(e) => setDonationAmount(+e.target.value)} onChange={(e) => setDonationAmount(+e.target.value)}
/> />
</label> </label>

View File

@@ -0,0 +1,2 @@
import DonationModal from "./DonationModal";
export default DonationModal;

View File

@@ -1,80 +0,0 @@
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;

View File

@@ -13,7 +13,6 @@
flex-direction: column; flex-direction: column;
width: 400px; width: 400px;
min-height: 600px; min-height: 600px;
/* background-color: #ffe8dc; */
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
border: 2px solid black; border: 2px solid black;
@@ -22,8 +21,10 @@
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
.modal-content { .modal-content {
min-width: 90%; position: fixed;
min-height: 100%; inset: 0;
width: unset;
min-height: unset;
border: none; border: none;
border-radius: 0; border-radius: 0;
} }
@@ -48,6 +49,17 @@ button.modal-close-button {
z-index: 1; z-index: 1;
} }
.modal-titlebar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.modal-titlebar > * {
margin: 0;
}
.payment-methods { .payment-methods {
display: grid; display: grid;
gap: 16px; gap: 16px;

View File

@@ -1,23 +1,36 @@
import React from "react"; import React from "react";
import { useRecoilState } from "recoil"; import { useRecoilState, useRecoilValue } from "recoil";
import { useLockBodyScroll } from "react-use"; import { useLockBodyScroll } from "react-use";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { X } from "phosphor-react";
import { modalOpenAtom } from "../../state/atoms"; import { modalAtom, modalOpenAtom } from "state/atoms";
import DonationModal from "./DonationModal";
import "./Modal.css"; import "./Modal.css";
const Modal: React.FC<{}> = () => { export interface ModalInstance {
close: () => void;
children?: JSX.Element;
}
const Modal = (): JSX.Element | null => {
const Instance = useRecoilValue(modalAtom);
const [isModalOpen, setModalOpen] = useRecoilState(modalOpenAtom); const [isModalOpen, setModalOpen] = useRecoilState(modalOpenAtom);
useHotkeys("esc", () => isModalOpen && setModalOpen(false), [isModalOpen]); useHotkeys("esc", () => isModalOpen && setModalOpen(false), [isModalOpen]);
useLockBodyScroll(isModalOpen); useLockBodyScroll(isModalOpen);
if (!isModalOpen) return null; const close = () => setModalOpen(false);
if (!Instance || !isModalOpen) return null;
return ( return (
<div className="modal-container"> <div className="modal-container">
<DonationModal /> <div className="modal-content">
<button className="modal-close-button" onClick={close}>
<X size={32} />
</button>
<Instance close={close} />
</div>
</div> </div>
); );
}; };

View File

@@ -0,0 +1,3 @@
import Modal from "./Modal";
export default Modal;

View File

@@ -1,17 +1,21 @@
import React from "react"; import React from "react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { useRecoilValue } from "recoil"; import { useRecoilValue } from "recoil";
import { isDarkThemeSelector } from "../../state/selectors";
import { searchQueryAtom } from "../../state/atoms";
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react"; import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
import { isDarkThemeSelector } from "state/selectors";
import { searchQueryAtom } from "state/atoms";
interface NoticeProps { interface NoticeProps {
message?: string; message?: string;
type?: "wait" | "help" | "warn" | "none"; type?: "wait" | "help" | "warn" | "none";
} }
const Notice: React.FC<NoticeProps> = ({ message, type = "warn", children }) => { const Notice: React.FC<NoticeProps> = ({
message,
type = "warn",
children,
}) => {
const isDark = useRecoilValue(isDarkThemeSelector); const isDark = useRecoilValue(isDarkThemeSelector);
const query = useRecoilValue(searchQueryAtom); const query = useRecoilValue(searchQueryAtom);

View File

@@ -0,0 +1,2 @@
import Notice from "./Notice";
export default Notice;

View File

@@ -5,7 +5,8 @@ import { useHotkeys } from "react-hotkeys-hook";
import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react"; import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
import ReactGA from "react-ga"; import ReactGA from "react-ga";
import { searchQueryAtom } from "../../state/atoms"; import { searchQueryAtom } from "state/atoms";
import "./SearchInput.css"; import "./SearchInput.css";
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i; const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
@@ -19,7 +20,8 @@ type SearchInputProps = {};
const SearchInput: React.FC<SearchInputProps> = () => { const SearchInput: React.FC<SearchInputProps> = () => {
const [value, setValue] = useState<string>(""); const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom); const [query, setQuery] = useRecoilState(searchQueryAtom);
const inputRef = useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>; const inputRef =
useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
useHotkeys("ctrl+k,cmd+k", (e) => { useHotkeys("ctrl+k,cmd+k", (e) => {
e.preventDefault(); e.preventDefault();

View File

@@ -0,0 +1,2 @@
import SearchInput from "./SearchInput";
export default SearchInput;

View File

@@ -1,7 +1,8 @@
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { useRecoilState } from "recoil"; import { useRecoilState } from "recoil";
import { iconSizeAtom } from "../../state/atoms"; import { iconSizeAtom } from "state/atoms";
import "./SizeInput.css"; import "./SizeInput.css";
type SizeInputProps = {}; type SizeInputProps = {};

View File

@@ -0,0 +1,2 @@
import SizeInput from "./SizeInput";
export default SizeInput;

View File

@@ -3,8 +3,9 @@ import { useRecoilState } from "recoil";
import Select from "react-dropdown-select"; import Select from "react-dropdown-select";
import { PencilLine } from "phosphor-react"; import { PencilLine } from "phosphor-react";
import { iconWeightAtom } from "../../state/atoms"; import { iconWeightAtom } from "state/atoms";
import { IconStyle } from "../../lib"; import { IconStyle } from "lib";
import "./StyleInput.css"; import "./StyleInput.css";
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element }; type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };

View File

@@ -0,0 +1,2 @@
import StyleInput from "./StyleInput";
export default StyleInput;

View File

@@ -1,10 +1,11 @@
import React from "react"; import React from "react";
import StyleInput from "components/StyleInput";
import SearchInput from "components/SearchInput";
import SizeInput from "components/SizeInput";
import ColorInput from "components/ColorInput";
import "./Toolbar.css"; import "./Toolbar.css";
import StyleInput from "../StyleInput/StyleInput";
import SearchInput from "../SearchInput/SearchInput";
import SizeInput from "../SizeInput/SizeInput";
import ColorInput from "../ColorInput/ColorInput";
type ToolbarProps = {}; type ToolbarProps = {};

View File

@@ -0,0 +1,2 @@
import Toolbar from "./Toolbar";
export default Toolbar;

8
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,8 @@
export { default as useDebounce } from "./useDebounce";
export { default as useGridSpans } from "./useGridSpans";
export { default as useIconParameters } from "./useIconParameters";
export { default as useThrottle } from "./useThrottle";
export { default as useThrottled } from "./useThrottled";
export { default as useTimeoutFn } from "./useTimeoutFn";
export { default as useTransientState } from "./useTransientState";
export { default as useUnmount } from "./useUnmount";

View File

@@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { RecoilRoot } from "recoil"; import { RecoilRoot } from "recoil";
import * as serviceWorker from "./serviceWorker"; import * as serviceWorker from "./serviceWorker";
import App from "./components/App/App"; import App from "./components/App";
import ReactGA from "react-ga"; import ReactGA from "react-ga";
ReactGA.initialize("UA-179205759-1", { titleCase: false }); ReactGA.initialize("UA-179205759-1", { titleCase: false });
@@ -23,7 +23,6 @@ ReactDOM.render(
serviceWorker.unregister(); serviceWorker.unregister();
console.log(` console.log(`
%c sphorphosphor %co%cspho %c sphorphosphor %co%cspho
%c s%cphorphosphor %co%csphorpho%cs %c s%cphorphosphor %co%csphorpho%cs
%c o %cp%chorphosphor %co%csphorphosph%co %c o %cp%chorphosphor %co%csphorphosph%co

View File

@@ -1,4 +1,5 @@
import { atom } from "recoil"; import { atom } from "recoil";
import { ModalInstance } from "../components/Modal/Modal";
import { IconStyle } from "../lib"; import { IconStyle } from "../lib";
export const searchQueryAtom = atom<string>({ export const searchQueryAtom = atom<string>({
@@ -26,7 +27,12 @@ export const iconPreviewOpenAtom = atom<string | false>({
default: false, default: false,
}); });
export const modalAtom = atom<((props: ModalInstance) => JSX.Element) | null>({
key: "modalAtom",
default: null,
});
export const modalOpenAtom = atom<boolean>({ export const modalOpenAtom = atom<boolean>({
key: "modalOpenAtom", key: "modalOpenAtom",
default: true, default: false,
}); });

View File

@@ -1,10 +1,16 @@
import { selector, selectorFamily } from "recoil"; import { DefaultValue, selector, selectorFamily } from "recoil";
import TinyColor from "tinycolor2"; import TinyColor from "tinycolor2";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { searchQueryAtom, iconColorAtom } from "./atoms"; import {
searchQueryAtom,
iconColorAtom,
modalAtom,
modalOpenAtom,
} from "./atoms";
import { IconEntry, IconCategory } from "../lib"; import { IconEntry, IconCategory } from "../lib";
import { icons } from "../lib/icons"; import { icons } from "../lib/icons";
import { ModalInstance } from "../components/Modal/Modal";
const fuse = new Fuse(icons, { const fuse = new Fuse(icons, {
keys: [{ name: "name", weight: 4 }, "tags", "categories"], keys: [{ name: "name", weight: 4 }, "tags", "categories"],
@@ -52,17 +58,40 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
IconCategory IconCategory
>({ >({
key: "singleCategoryQueryResultsSelector", key: "singleCategoryQueryResultsSelector",
get: (category: IconCategory) => ({ get }) => { get:
const filteredResults = get(filteredQueryResultsSelector); (category: IconCategory) =>
return new Promise((resolve) => ({ get }) => {
resolve( const filteredResults = get(filteredQueryResultsSelector);
filteredResults.filter((icon) => icon.categories.includes(category)) return new Promise((resolve) =>
) resolve(
); filteredResults.filter((icon) => icon.categories.includes(category))
}, )
);
},
}); });
export const isDarkThemeSelector = selector<boolean>({ export const isDarkThemeSelector = selector<boolean>({
key: "isDarkThemeSelector", key: "isDarkThemeSelector",
get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(), get: ({ get }) => TinyColor(get(iconColorAtom)).isLight(),
}); });
export const modalSelector = selector<{
type: (props: ModalInstance) => JSX.Element;
} | null>({
key: "openModalSelector",
set: ({ set }, instance) => {
if (instance instanceof DefaultValue || instance === null) {
set(modalAtom, null);
set(modalOpenAtom, false);
return;
}
set(modalAtom, () => instance.type!!);
set(modalOpenAtom, true);
},
get: ({ get }) => {
const currentModal = get(modalAtom);
if (!currentModal) return null;
return { type: currentModal };
},
});

View File

@@ -1,36 +1,27 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5",
"lib": [
"es6",
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "baseUrl": "./src/",
"declaration": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"jsx": "react", "jsx": "react",
"sourceMap": true, "lib": ["es6", "dom", "dom.iterable", "esnext"],
"declaration": true, "module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"experimentalDecorators": true, "resolveJsonModule": true,
"noFallthroughCasesInSwitch": true, "skipLibCheck": true,
"noEmit": true "sourceMap": true,
"strict": true,
"target": "es5"
}, },
"include": [ "include": ["src"],
"src" "exclude": ["node_modules", "build"]
],
"exclude": [
"node_modules",
"build"
]
} }