Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57016742a5 | ||
|
|
7cd27c509b | ||
|
|
8a7921d082 | ||
|
|
a885931831 | ||
|
|
2eb51f7ca7 | ||
|
|
7e1bd3d18e | ||
|
|
94e5d9b305 | ||
|
|
02e70848b1 | ||
|
|
b8eac52689 | ||
|
|
73b66e2e86 | ||
|
|
0e50efb5ea | ||
|
|
dc6764e387 | ||
|
|
2ba5ac332b | ||
|
|
b9a0b93067 | ||
|
|
6596bce68a | ||
|
|
6d74c9f719 | ||
|
|
02525cabb5 |
4
.vscode/phosphor-home.code-snippets
vendored
4
.vscode/phosphor-home.code-snippets
vendored
@@ -21,8 +21,8 @@
|
||||
"{",
|
||||
"\tname: \"${1:name}\",",
|
||||
"\tcategories: [IconCategory${2:categories}],",
|
||||
"\ttags: [${3:tags}],",
|
||||
"\tIcon: ${4:icon},",
|
||||
"\ttags: [\"*new*\", ${3:tags}],",
|
||||
"\tIcon: Icons.${4:icon},",
|
||||
"},"
|
||||
],
|
||||
"description": "Create an IconEntry for phosphor-home"
|
||||
|
||||
13
package.json
13
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "phosphor-home",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"license": "MIT",
|
||||
"homepage": "https://phosphoricons.com",
|
||||
"author": {
|
||||
@@ -21,10 +21,13 @@
|
||||
"repository": "github:phosphor-icons/phosphor-home",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@types/braintree-web-drop-in": "^1.22.3",
|
||||
"axios": "^0.21.1",
|
||||
"braintree-web-drop-in": "^1.30.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"framer-motion": "^2.1.0",
|
||||
"framer-motion": "^3.10.0",
|
||||
"fuse.js": "^6.4.1",
|
||||
"phosphor-react": "^1.2.0",
|
||||
"phosphor-react": "^1.2.1",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-dropdown-select": "^4.4.2",
|
||||
@@ -32,9 +35,9 @@
|
||||
"react-hotkeys-hook": "^3.2.1",
|
||||
"react-scripts": "3.4.1",
|
||||
"react-use": "^15.3.2",
|
||||
"recoil": "^0.1.2",
|
||||
"recoil": "^0.1.3",
|
||||
"svg2png-converter": "^1.0.0",
|
||||
"tinycolor2": "^1.4.1"
|
||||
"tinycolor2": "^1.4.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
|
||||
@@ -15,10 +15,10 @@ h2 {
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
@@ -49,6 +49,7 @@ button {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
input.main-button,
|
||||
button.main-button {
|
||||
height: 64px;
|
||||
padding: 0 48px 0 40px;
|
||||
@@ -65,25 +66,36 @@ button.main-button {
|
||||
transform: translate(0, 0);
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
user-select: none;
|
||||
margin: 0 24px 24px 0;
|
||||
}
|
||||
|
||||
input.main-button:active,
|
||||
button.main-button:active {
|
||||
transform: translate(4px, 4px);
|
||||
box-shadow: 0 0 0 0 black;
|
||||
}
|
||||
|
||||
input.main-button:focus,
|
||||
button.main-button:focus {
|
||||
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) {
|
||||
margin: 0 24px 24px 0;
|
||||
} */
|
||||
|
||||
input.main-button,
|
||||
button.main-button svg {
|
||||
margin-right: 12px;
|
||||
}
|
||||
@@ -113,3 +125,55 @@ a.main-link:after {
|
||||
a.main-link:hover:after {
|
||||
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: 8px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
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 Header from "../Header/Header";
|
||||
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 paymentFallback = <Notice message="Could not connect to payments" />;
|
||||
const waitingFallback = <Notice type="none" message="" />;
|
||||
|
||||
const App: React.FC<any> = () => {
|
||||
@@ -26,6 +30,9 @@ const App: React.FC<any> = () => {
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
<Suspense fallback={paymentFallback}>
|
||||
<Modal />
|
||||
</Suspense>
|
||||
<Footer />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
2
src/components/App/index.ts
Normal file
2
src/components/App/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import App from "./App";
|
||||
export default App;
|
||||
@@ -16,10 +16,11 @@
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
input.color-input {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
|
||||
import { iconColorAtom } from "../../state/atoms";
|
||||
import { isDarkThemeSelector } from "../../state/selectors";
|
||||
import useThrottled from "../../hooks/useThrottled";
|
||||
import { iconColorAtom } from "state/atoms";
|
||||
import { isDarkThemeSelector } from "state/selectors";
|
||||
import useThrottled from "hooks/useThrottled";
|
||||
|
||||
import "./ColorInput.css";
|
||||
|
||||
type ColorInputProps = {};
|
||||
|
||||
2
src/components/ColorInput/index.ts
Normal file
2
src/components/ColorInput/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import ColorInput from "./ColorInput";
|
||||
export default ColorInput;
|
||||
2
src/components/ErrorBoundary/index.ts
Normal file
2
src/components/ErrorBoundary/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import ErrorBoundary from "./ErrorBoundary";
|
||||
export default ErrorBoundary;
|
||||
@@ -48,9 +48,9 @@ footer .links {
|
||||
|
||||
.illustrations-footer {
|
||||
display: none;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from "react";
|
||||
import { Coffee, Heart } from "phosphor-react";
|
||||
|
||||
import uArrowUpLeft from "../../assets/u-arrow-up-left.svg";
|
||||
import markerGreen from "../../assets/marker-green.svg";
|
||||
import postIt from "../../assets/footer-mobile.svg";
|
||||
import Links from "../Links/Links";
|
||||
import uArrowUpLeft from "assets/u-arrow-up-left.svg";
|
||||
import markerGreen from "assets/marker-green.svg";
|
||||
import postIt from "assets/footer-mobile.svg";
|
||||
|
||||
import Links from "components/Links";
|
||||
|
||||
import "./Footer.css";
|
||||
|
||||
type FooterProps = {};
|
||||
@@ -34,8 +36,8 @@ const Footer: React.FC<FooterProps> = () => {
|
||||
a little quirky, too.
|
||||
</p>
|
||||
<p>
|
||||
We're thankful for the tools we've benefited from and
|
||||
this is our contribution towards a collaborative community.
|
||||
We're thankful for the tools we've benefited from and this is our
|
||||
contribution towards a collaborative community.
|
||||
</p>
|
||||
<p>
|
||||
Phosphor is free and open-source, licensed under{" "}
|
||||
|
||||
2
src/components/Footer/index.ts
Normal file
2
src/components/Footer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Footer from "./Footer";
|
||||
export default Footer;
|
||||
@@ -60,7 +60,7 @@ header {
|
||||
top: -158px;
|
||||
}
|
||||
|
||||
#billiard-ball {
|
||||
.billiard-ball {
|
||||
position: absolute;
|
||||
left: 132px;
|
||||
top: -98px;
|
||||
@@ -72,13 +72,13 @@ header {
|
||||
top: 152px;
|
||||
}
|
||||
|
||||
#warning {
|
||||
.warning {
|
||||
position: absolute;
|
||||
left: 394px;
|
||||
top: -304px;
|
||||
}
|
||||
|
||||
#tablet {
|
||||
.tablet {
|
||||
position: absolute;
|
||||
left: 672px;
|
||||
top: -900px;
|
||||
@@ -94,18 +94,18 @@ header {
|
||||
height: 612px;
|
||||
}
|
||||
|
||||
#cutting-mat {
|
||||
.cutting-mat {
|
||||
position: absolute;
|
||||
left: 96px;
|
||||
}
|
||||
|
||||
#receipt {
|
||||
.receipt {
|
||||
position: absolute;
|
||||
left: -36px;
|
||||
top: 190px;
|
||||
}
|
||||
|
||||
#calculator {
|
||||
.calculator {
|
||||
position: absolute;
|
||||
left: 632px;
|
||||
top: 170px;
|
||||
@@ -131,7 +131,7 @@ header {
|
||||
top: -158px;
|
||||
}
|
||||
|
||||
#billiard-ball {
|
||||
.billiard-ball {
|
||||
position: absolute;
|
||||
left: 900px;
|
||||
top: 400px;
|
||||
@@ -148,30 +148,30 @@ header {
|
||||
top: 694px;
|
||||
}
|
||||
|
||||
#warning {
|
||||
.warning {
|
||||
position: absolute;
|
||||
left: 1170px;
|
||||
top: 400px;
|
||||
}
|
||||
|
||||
#tablet {
|
||||
.tablet {
|
||||
position: absolute;
|
||||
left: 578px;
|
||||
top: -900px;
|
||||
}
|
||||
|
||||
#cutting-mat {
|
||||
.cutting-mat {
|
||||
position: absolute;
|
||||
left: 120px;
|
||||
}
|
||||
|
||||
#receipt {
|
||||
.receipt {
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 190px;
|
||||
}
|
||||
|
||||
#calculator {
|
||||
.calculator {
|
||||
position: absolute;
|
||||
left: 924px;
|
||||
top: 114px;
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import React from "react";
|
||||
import { ArrowCircleUpRight, ArrowCircleDown } from "phosphor-react";
|
||||
|
||||
import markerPurple from "../../assets/marker-purple.svg";
|
||||
import paperclips from "../../assets/paperclips-header-mobile.svg";
|
||||
import paperclipsThree from "../../assets/paperclips-header.svg";
|
||||
import tablet from "../../assets/tablet.svg";
|
||||
import tabletSpec from "../../assets/tablet-spec.svg";
|
||||
import billiardBall from "../../assets/billiard-ball.svg";
|
||||
import billiardBallSpec from "../../assets/billiard-ball-spec.svg";
|
||||
import warning from "../../assets/warning.svg";
|
||||
import warningSpec from "../../assets/warning-spec.svg";
|
||||
import cuttingMat from "../../assets/cutting-mat.svg";
|
||||
import cuttingMatSpec from "../../assets/cutting-mat-spec.svg";
|
||||
import receipt from "../../assets/receipt.svg";
|
||||
import receiptSpec from "../../assets/receipt-spec.svg";
|
||||
import calculator from "../../assets/calculator.svg";
|
||||
import calculatorSpec from "../../assets/calculator-spec.svg";
|
||||
import Links from "../Links/Links";
|
||||
import markerPurple from "assets/marker-purple.svg";
|
||||
import paperclips from "assets/paperclips-header-mobile.svg";
|
||||
import paperclipsThree from "assets/paperclips-header.svg";
|
||||
import tablet from "assets/tablet.svg";
|
||||
import tabletSpec from "assets/tablet-spec.svg";
|
||||
import billiardBall from "assets/billiard-ball.svg";
|
||||
import billiardBallSpec from "assets/billiard-ball-spec.svg";
|
||||
import warning from "assets/warning.svg";
|
||||
import warningSpec from "assets/warning-spec.svg";
|
||||
import cuttingMat from "assets/cutting-mat.svg";
|
||||
import cuttingMatSpec from "assets/cutting-mat-spec.svg";
|
||||
import receipt from "assets/receipt.svg";
|
||||
import receiptSpec from "assets/receipt-spec.svg";
|
||||
import calculator from "assets/calculator.svg";
|
||||
import calculatorSpec from "assets/calculator-spec.svg";
|
||||
|
||||
import Links from "components/Links";
|
||||
|
||||
import "./Header.css";
|
||||
|
||||
type HeaderProps = {};
|
||||
@@ -41,22 +43,22 @@ const Header: React.FC<HeaderProps> = () => {
|
||||
<img src={markerPurple} id="marker-purple" alt="" />
|
||||
<img src={paperclips} id="paperclips" alt="" />
|
||||
<img src={paperclipsThree} id="paperclips-three" alt="" />
|
||||
<img id="tablet" src={tabletSpec} alt="" />
|
||||
<img id="tablet" className="inspectable xray" src={tablet} alt="" />
|
||||
<img id="billiard-ball" src={billiardBallSpec} alt="" />
|
||||
<img className="tablet" src={tabletSpec} alt="" />
|
||||
<img className="tablet inspectable xray" src={tablet} alt="" />
|
||||
<img className="billiard-ball" src={billiardBallSpec} alt="" />
|
||||
<img
|
||||
id="billiard-ball"
|
||||
className="inspectable xray"
|
||||
className="billiard-ball inspectable xray"
|
||||
src={billiardBall}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<img id="warning" src={warningSpec} alt="" />
|
||||
<img id="warning" className="inspectable xray" src={warning} alt="" />
|
||||
<img className="warning" src={warningSpec} alt="" />
|
||||
<img className="warning inspectable xray" src={warning} alt="" />
|
||||
</div>
|
||||
<div className="intro">
|
||||
<h2>
|
||||
Phosphor is a flexible icon family for interfaces, diagrams, presentations —
|
||||
Phosphor is a flexible icon family for interfaces, diagrams,
|
||||
presentations —
|
||||
<wbr />
|
||||
whatever, really.
|
||||
</h2>
|
||||
@@ -73,19 +75,17 @@ const Header: React.FC<HeaderProps> = () => {
|
||||
<Links />
|
||||
</div>
|
||||
<div className="illustrations-bottom">
|
||||
<img id="cutting-mat" src={cuttingMatSpec} alt="" />
|
||||
<img className="cutting-mat" src={cuttingMatSpec} alt="" />
|
||||
<img
|
||||
id="cutting-mat"
|
||||
className="inspectable xray"
|
||||
className="cutting-mat inspectable xray"
|
||||
src={cuttingMat}
|
||||
alt=""
|
||||
/>
|
||||
<img id="receipt" src={receiptSpec} alt="" />
|
||||
<img id="receipt" className="inspectable xray" src={receipt} alt="" />
|
||||
<img id="calculator" src={calculatorSpec} alt="" />
|
||||
<img className="receipt" src={receiptSpec} alt="" />
|
||||
<img className="receipt inspectable xray" src={receipt} alt="" />
|
||||
<img className="calculator" src={calculatorSpec} alt="" />
|
||||
<img
|
||||
id="calculator"
|
||||
className="inspectable xray"
|
||||
className="calculator inspectable xray"
|
||||
src={calculator}
|
||||
alt=""
|
||||
/>
|
||||
|
||||
2
src/components/Header/index.ts
Normal file
2
src/components/Header/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Header from "./Header";
|
||||
export default Header;
|
||||
@@ -3,39 +3,40 @@ import { useRecoilValue, useSetRecoilState } from "recoil";
|
||||
import { motion } from "framer-motion";
|
||||
import { Svg2Png } from "svg2png-converter";
|
||||
import { saveAs } from "file-saver";
|
||||
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
||||
import ReactGA from "react-ga";
|
||||
import { Copy, X, CheckCircle, Download } from "phosphor-react";
|
||||
|
||||
import {
|
||||
iconWeightAtom,
|
||||
iconSizeAtom,
|
||||
iconColorAtom,
|
||||
iconPreviewOpenAtom,
|
||||
} from "../../state/atoms";
|
||||
import useTransientState from "../../hooks/useTransientState";
|
||||
} from "state/atoms";
|
||||
import { IconEntry } from "lib";
|
||||
import useTransientState from "hooks/useTransientState";
|
||||
|
||||
import TagCloud from "./TagCloud";
|
||||
import { IconEntry } from "../../lib";
|
||||
|
||||
const panelVariants = {
|
||||
open: {
|
||||
opacity: 1,
|
||||
height: "100%",
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
|
||||
marginTop: "4px",
|
||||
marginBottom: "4px",
|
||||
transition: { type: "tween", duration: 0.1 },
|
||||
},
|
||||
collapsed: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
marginTop: 0,
|
||||
marginBottom: 0,
|
||||
// transition: { stiffness: 600, damping: 32, duration: 0.2 },
|
||||
height: "0px",
|
||||
marginTop: "0px",
|
||||
marginBottom: "0px",
|
||||
transition: { type: "tween", duration: 0.1 },
|
||||
},
|
||||
};
|
||||
|
||||
const contentVariants = {
|
||||
open: { opacity: 1, transition: { duration: 0.2 } },
|
||||
collapsed: { opacity: 0, transition: { duration: 0.1 } },
|
||||
open: { opacity: 1, transition: { duration: 0.2, delay: 0.1 } },
|
||||
collapsed: { opacity: 0, transition: { duration: 0 } },
|
||||
};
|
||||
|
||||
const buttonColor = "#35313D";
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
justify-content: center;
|
||||
margin: 4px;
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
/* transition: background-color 100ms ease; */
|
||||
}
|
||||
@@ -46,6 +46,24 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 536px) {
|
||||
.grid-container {
|
||||
padding: 32px 8px;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
width: 108px;
|
||||
height: unset;
|
||||
padding: 4px 0;
|
||||
justify-content: flex-start;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.grid-item p {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-box {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -64,6 +82,10 @@
|
||||
.icon-usage {
|
||||
padding-left: 10% !important;
|
||||
}
|
||||
|
||||
.snippet pre {
|
||||
padding: 12px 8px 12px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
@@ -95,9 +117,9 @@
|
||||
align-items: center;
|
||||
text-overflow: ellipsis;
|
||||
color: black;
|
||||
user-select: all;
|
||||
-moz-user-select: all;
|
||||
-webkit-user-select: all;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.snippet pre:focus {
|
||||
@@ -106,9 +128,9 @@
|
||||
|
||||
@keyframes select {
|
||||
to {
|
||||
user-select: text;
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,18 +3,27 @@ import { useRecoilValue } from "recoil";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
import { IconContext } from "phosphor-react";
|
||||
|
||||
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "../../state/atoms";
|
||||
import { iconWeightAtom, iconSizeAtom, iconColorAtom } from "state/atoms";
|
||||
import {
|
||||
filteredQueryResultsSelector,
|
||||
isDarkThemeSelector,
|
||||
} from "../../state/selectors";
|
||||
import useGridSpans from "../../hooks/useGridSpans";
|
||||
} from "state/selectors";
|
||||
import useGridSpans from "hooks/useGridSpans";
|
||||
|
||||
import IconGridItem from "./IconGridItem";
|
||||
import TagCloud from "./TagCloud";
|
||||
import Notice from "../Notice/Notice";
|
||||
import Notice from "components/Notice";
|
||||
|
||||
import "./IconGrid.css";
|
||||
|
||||
const defaultSearchTags = ["*new*", "communication", "editor", "emoji", "maps", "weather"];
|
||||
const defaultSearchTags = [
|
||||
"*new*",
|
||||
"communication",
|
||||
"editor",
|
||||
"emoji",
|
||||
"maps",
|
||||
"weather",
|
||||
];
|
||||
|
||||
type IconGridProps = {};
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import React, {
|
||||
import { useRecoilState } from "recoil";
|
||||
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 { IconEntry } from "../../lib";
|
||||
|
||||
interface IconGridItemProps {
|
||||
index: number;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
|
||||
import { searchQueryAtom } from "../../state/atoms";
|
||||
import { searchQueryAtom } from "state/atoms";
|
||||
|
||||
import "./TagCloud.css";
|
||||
|
||||
interface TagCloudProps {
|
||||
|
||||
2
src/components/IconGrid/index.ts
Normal file
2
src/components/IconGrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import IconGrid from "./IconGrid";
|
||||
export default IconGrid;
|
||||
@@ -20,13 +20,14 @@
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
a.nav-link {
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
color: black;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a.nav-link:after {
|
||||
.nav-link:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
@@ -36,6 +37,6 @@ a.nav-link:after {
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
a.nav-link:hover:after {
|
||||
.nav-link:hover:after {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import React from "react";
|
||||
import { OutboundLink } from "react-ga";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { ArrowElbowDownRight } from "phosphor-react";
|
||||
|
||||
import { iconCount } from "../../lib/icons";
|
||||
import { iconCount } from "lib/icons";
|
||||
import { modalSelector } from "state/selectors";
|
||||
import DonationModal from "components/Modal/DonationModal";
|
||||
|
||||
import "./Links.css";
|
||||
|
||||
interface LinksProps {}
|
||||
|
||||
const Links: React.FC<LinksProps> = () => {
|
||||
const openModal = useSetRecoilState(modalSelector);
|
||||
const openDonationModal = () => openModal({ type: DonationModal });
|
||||
|
||||
const delayedOpenDonationModal = () => setTimeout(openDonationModal, 2000);
|
||||
|
||||
return (
|
||||
<div className="links">
|
||||
<div>
|
||||
@@ -19,6 +27,7 @@ const Links: React.FC<LinksProps> = () => {
|
||||
eventLabel="Download all"
|
||||
download
|
||||
type="application/zip"
|
||||
onClick={delayedOpenDonationModal}
|
||||
>
|
||||
Download all ({iconCount})
|
||||
</OutboundLink>
|
||||
@@ -43,26 +52,6 @@ const Links: React.FC<LinksProps> = () => {
|
||||
</OutboundLink>
|
||||
</span>
|
||||
</div>
|
||||
{/* <div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<OutboundLink
|
||||
className="nav-link"
|
||||
to="https://www.figma.com/file/xMCDSp5g0g7Fw8aMyAdVVr/Phosphor-Icon-Library-0.6.0"
|
||||
eventLabel="Figma library"
|
||||
>
|
||||
Figma library
|
||||
</OutboundLink>
|
||||
</div>
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<OutboundLink
|
||||
className="nav-link"
|
||||
to="https://www.figma.com/community/plugin/892854133443228626/Phosphor-Icons"
|
||||
eventLabel="Figma plugin"
|
||||
>
|
||||
Figma plugin
|
||||
</OutboundLink>
|
||||
</div> */}
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<a
|
||||
@@ -72,23 +61,11 @@ const Links: React.FC<LinksProps> = () => {
|
||||
Request an icon
|
||||
</a>
|
||||
</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>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
<a className="nav-link" href="https://paypal.me/minoraxis">
|
||||
Donate on PayPal
|
||||
</a>
|
||||
<span className="nav-link" onClick={openDonationModal}>
|
||||
Donate
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<ArrowElbowDownRight size={24} />
|
||||
|
||||
2
src/components/Links/index.ts
Normal file
2
src/components/Links/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Links from "./Links";
|
||||
export default Links;
|
||||
91
src/components/Modal/DonationModal/DonationModal.tsx
Normal file
91
src/components/Modal/DonationModal/DonationModal.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { ModalInstance } from "../Modal";
|
||||
import DonationStepMethod from "./DonationStepMethod";
|
||||
import DonationStepDropin from "./DonationStepDropin";
|
||||
import DonationStepThanks from "./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 {
|
||||
previousStep: () => void;
|
||||
nextStep: () => void;
|
||||
close: () => void;
|
||||
routeProps: RouteProps;
|
||||
}
|
||||
|
||||
interface StepperProps {
|
||||
routes: Array<React.FC<StepProps>>;
|
||||
routeProps: RouteProps;
|
||||
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={routeProps}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const DonationModal = ({ close }: ModalInstance): JSX.Element => {
|
||||
const [donationType, setDonationType] = useState<DonationType | undefined>();
|
||||
const [donationAmount, setDonationAmount] = useState<number>(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="modal-titlebar">
|
||||
<h2>Donate</h2>
|
||||
</div>
|
||||
<Stepper
|
||||
routes={routes}
|
||||
routeProps={{
|
||||
donationType,
|
||||
setDonationType,
|
||||
donationAmount,
|
||||
setDonationAmount,
|
||||
}}
|
||||
close={close}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DonationModal;
|
||||
128
src/components/Modal/DonationModal/DonationStepDropin.tsx
Normal file
128
src/components/Modal/DonationModal/DonationStepDropin.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import dropin, { Dropin } from "braintree-web-drop-in";
|
||||
import axios from "axios";
|
||||
import { CurrencyCircleDollar } from "phosphor-react";
|
||||
|
||||
import { StepProps } from "./DonationModal";
|
||||
|
||||
const PaymentServer = axios.create({
|
||||
baseURL: "https://us-central1-phosphor-14c61.cloudfunctions.net/paymentsApi",
|
||||
});
|
||||
|
||||
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> = ({
|
||||
nextStep,
|
||||
previousStep,
|
||||
routeProps,
|
||||
}) => {
|
||||
const instance = useRef<Dropin>();
|
||||
const [isValid, setIsValid] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
|
||||
const { donationAmount } = routeProps;
|
||||
|
||||
const submit = async () => {
|
||||
if (!instance.current) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const payload = await instance.current.requestPaymentMethod();
|
||||
console.log({ ...payload, donationAmount });
|
||||
const response = await PaymentServer.post("/", {
|
||||
...payload,
|
||||
donationAmount,
|
||||
});
|
||||
|
||||
console.log({ response });
|
||||
if (!!response.data?.success) {
|
||||
nextStep();
|
||||
} else {
|
||||
setIsValid(false);
|
||||
}
|
||||
} catch (_e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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: donationAmount.toFixed(2).toString(),
|
||||
currency: "USD",
|
||||
commit: false,
|
||||
},
|
||||
paypalCredit: {
|
||||
flow: "checkout",
|
||||
amount: donationAmount.toFixed(2).toString(),
|
||||
currency: "USD",
|
||||
commit: false,
|
||||
},
|
||||
venmo: { allowNewBrowserTab: false },
|
||||
});
|
||||
instance.current.on("paymentMethodRequestable", () => setIsValid(true));
|
||||
instance.current.on("noPaymentMethodRequestable", () =>
|
||||
setIsValid(false)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
initializePayments();
|
||||
}, [donationAmount]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="braintree-dropin"></div>
|
||||
<div className="donation-details">
|
||||
<CurrencyCircleDollar size={64} weight="duotone" />
|
||||
<span>
|
||||
Preparing your ${routeProps.donationAmount.toFixed(2)} donation...
|
||||
</span>
|
||||
</div>
|
||||
<div className="step-button-container">
|
||||
<button className="main-button" onClick={previousStep}>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
className="main-button"
|
||||
onClick={submit}
|
||||
disabled={isLoading || !isValid}
|
||||
>
|
||||
{isLoading ? "Processing..." : "Donate"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentModal;
|
||||
123
src/components/Modal/DonationModal/DonationStepMethod.tsx
Normal file
123
src/components/Modal/DonationModal/DonationStepMethod.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React from "react";
|
||||
|
||||
import { StepProps, DonationType } from "./DonationModal";
|
||||
|
||||
const DonationStepMethod: React.FC<StepProps> = ({
|
||||
previousStep,
|
||||
nextStep,
|
||||
close,
|
||||
routeProps,
|
||||
}) => {
|
||||
const { donationType, donationAmount, setDonationType, setDonationAmount } =
|
||||
routeProps;
|
||||
|
||||
const onDonationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDonationType(+e.target.value as DonationType);
|
||||
if (!(+e.target.value === DonationType.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={DonationType.FIVE_DOLLARS}
|
||||
checked={donationType === DonationType.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={DonationType.TEN_DOLLARS}
|
||||
checked={donationType === DonationType.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={DonationType.TWENTY_DOLLARS}
|
||||
checked={donationType === DonationType.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={DonationType.FIFTY_DOLLARS}
|
||||
checked={donationType === DonationType.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={DonationType.ONE_HUNDRED_DOLLARS}
|
||||
checked={donationType === DonationType.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={DonationType.CUSTOM}
|
||||
checked={donationType === DonationType.CUSTOM}
|
||||
onChange={onDonationChange}
|
||||
/>
|
||||
<label htmlFor="donate-custom">
|
||||
Other: $
|
||||
<input
|
||||
type="number"
|
||||
min={2}
|
||||
max={1000000}
|
||||
id="donate-custom"
|
||||
value={donationAmount}
|
||||
disabled={donationType !== DonationType.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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const DonationStepThanks: React.FC<{}> = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export default DonationStepThanks;
|
||||
2
src/components/Modal/DonationModal/index.ts
Normal file
2
src/components/Modal/DonationModal/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import DonationModal from "./DonationModal";
|
||||
export default DonationModal;
|
||||
85
src/components/Modal/Modal.css
Normal file
85
src/components/Modal/Modal.css
Normal file
@@ -0,0 +1,85 @@
|
||||
.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: white;
|
||||
border-radius: 8px;
|
||||
border: 2px solid black;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
.modal-content {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: unset;
|
||||
min-height: unset;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.step-button-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.donation-details {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
button.modal-close-button {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
box-sizing: content-box;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.modal-titlebar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-titlebar > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
38
src/components/Modal/Modal.tsx
Normal file
38
src/components/Modal/Modal.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import { useRecoilState, useRecoilValue } from "recoil";
|
||||
import { useLockBodyScroll } from "react-use";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { X } from "phosphor-react";
|
||||
|
||||
import { modalAtom, modalOpenAtom } from "state/atoms";
|
||||
|
||||
import "./Modal.css";
|
||||
|
||||
export interface ModalInstance {
|
||||
close: () => void;
|
||||
children?: JSX.Element;
|
||||
}
|
||||
|
||||
const Modal = (): JSX.Element | null => {
|
||||
const Instance = useRecoilValue(modalAtom);
|
||||
const [isModalOpen, setModalOpen] = useRecoilState(modalOpenAtom);
|
||||
useHotkeys("esc", () => isModalOpen && setModalOpen(false), [isModalOpen]);
|
||||
useLockBodyScroll(isModalOpen);
|
||||
|
||||
const close = () => setModalOpen(false);
|
||||
|
||||
if (!Instance || !isModalOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-container">
|
||||
<div className="modal-content">
|
||||
<button className="modal-close-button" onClick={close}>
|
||||
<X size={32} />
|
||||
</button>
|
||||
<Instance close={close} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
3
src/components/Modal/index.ts
Normal file
3
src/components/Modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Modal from "./Modal";
|
||||
export default Modal;
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useRecoilValue } from "recoil";
|
||||
|
||||
import { isDarkThemeSelector } from "../../state/selectors";
|
||||
import { searchQueryAtom } from "../../state/atoms";
|
||||
import { HourglassMedium, Question, SmileyXEyes } from "phosphor-react";
|
||||
|
||||
import { isDarkThemeSelector } from "state/selectors";
|
||||
import { searchQueryAtom } from "state/atoms";
|
||||
|
||||
interface NoticeProps {
|
||||
message?: string;
|
||||
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 query = useRecoilValue(searchQueryAtom);
|
||||
|
||||
|
||||
2
src/components/Notice/index.ts
Normal file
2
src/components/Notice/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Notice from "./Notice";
|
||||
export default Notice;
|
||||
@@ -56,6 +56,8 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
min-width: 42px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { Command, MagnifyingGlass, X, HourglassHigh } from "phosphor-react";
|
||||
import ReactGA from "react-ga";
|
||||
|
||||
import { searchQueryAtom } from "../../state/atoms";
|
||||
import { searchQueryAtom } from "state/atoms";
|
||||
|
||||
import "./SearchInput.css";
|
||||
|
||||
const apple = /iPhone|iPod|iPad|Macintosh|MacIntel|MacPPC/i;
|
||||
@@ -19,7 +20,8 @@ type SearchInputProps = {};
|
||||
const SearchInput: React.FC<SearchInputProps> = () => {
|
||||
const [value, setValue] = useState<string>("");
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
@@ -81,7 +83,7 @@ const SearchInput: React.FC<SearchInputProps> = () => {
|
||||
key === "Enter" && currentTarget.blur()
|
||||
}
|
||||
/>
|
||||
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl"} + K</Keys>}
|
||||
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
|
||||
{value ? (
|
||||
isReady() ? (
|
||||
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
|
||||
|
||||
2
src/components/SearchInput/index.ts
Normal file
2
src/components/SearchInput/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SearchInput from "./SearchInput";
|
||||
export default SearchInput;
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useCallback } from "react";
|
||||
import { useRecoilState } from "recoil";
|
||||
|
||||
import { iconSizeAtom } from "../../state/atoms";
|
||||
import { iconSizeAtom } from "state/atoms";
|
||||
|
||||
import "./SizeInput.css";
|
||||
|
||||
type SizeInputProps = {};
|
||||
|
||||
2
src/components/SizeInput/index.ts
Normal file
2
src/components/SizeInput/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import SizeInput from "./SizeInput";
|
||||
export default SizeInput;
|
||||
@@ -3,8 +3,9 @@ import { useRecoilState } from "recoil";
|
||||
import Select from "react-dropdown-select";
|
||||
import { PencilLine } from "phosphor-react";
|
||||
|
||||
import { iconWeightAtom } from "../../state/atoms";
|
||||
import { IconStyle } from "../../lib";
|
||||
import { iconWeightAtom } from "state/atoms";
|
||||
import { IconStyle } from "lib";
|
||||
|
||||
import "./StyleInput.css";
|
||||
|
||||
type WeightOption = { key: string; value: IconStyle; icon: JSX.Element };
|
||||
|
||||
2
src/components/StyleInput/index.ts
Normal file
2
src/components/StyleInput/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import StyleInput from "./StyleInput";
|
||||
export default StyleInput;
|
||||
@@ -1,4 +1,5 @@
|
||||
menu.toolbar {
|
||||
nav.toolbar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
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 StyleInput from "../StyleInput/StyleInput";
|
||||
import SearchInput from "../SearchInput/SearchInput";
|
||||
import SizeInput from "../SizeInput/SizeInput";
|
||||
import ColorInput from "../ColorInput/ColorInput";
|
||||
|
||||
type ToolbarProps = {};
|
||||
|
||||
const Toolbar: React.FC<ToolbarProps> = () => {
|
||||
return (
|
||||
<menu className="toolbar" id="toolbar">
|
||||
<nav className="toolbar" id="toolbar">
|
||||
<div className="toolbar-contents">
|
||||
<StyleInput />
|
||||
<SearchInput />
|
||||
<SizeInput />
|
||||
<ColorInput />
|
||||
</div>
|
||||
</menu>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
2
src/components/Toolbar/index.ts
Normal file
2
src/components/Toolbar/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import Toolbar from "./Toolbar";
|
||||
export default Toolbar;
|
||||
8
src/hooks/index.ts
Normal file
8
src/hooks/index.ts
Normal 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";
|
||||
16
src/hooks/useDebounce.ts
Normal file
16
src/hooks/useDebounce.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { DependencyList, useEffect } from "react";
|
||||
import useTimeoutFn from "./useTimeoutFn";
|
||||
|
||||
export type UseDebounceReturn = [() => boolean | null, () => void];
|
||||
|
||||
export default function useDebounce(
|
||||
fn: Function,
|
||||
ms: number = 0,
|
||||
deps: DependencyList = []
|
||||
): UseDebounceReturn {
|
||||
const [isReady, cancel, reset] = useTimeoutFn(fn, ms);
|
||||
|
||||
useEffect(reset, deps);
|
||||
|
||||
return [isReady, cancel];
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useWindowSize } from "react-use";
|
||||
|
||||
const MOBILE_BREAKPOINT = 536;
|
||||
|
||||
const GRID_PADDING = 32; // .grid-container { padding }
|
||||
const TOOLBAR_WIDTH = 17; // IS THIS BROWSER-SPECIFIC?
|
||||
const MAX_GRID_WIDTH = 1120; // .grid { max-width }
|
||||
const ITEM_WIDTH = 168; // .grid-item { width; height; margin }
|
||||
const ITEM_WIDTH_MOBILE = 108; // .grid-item { width; height; margin }
|
||||
|
||||
export default (): number => {
|
||||
const { width } = useWindowSize();
|
||||
const itemWidth = width <= MOBILE_BREAKPOINT ? ITEM_WIDTH_MOBILE : ITEM_WIDTH;
|
||||
|
||||
return Math.floor(
|
||||
Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / ITEM_WIDTH
|
||||
Math.min(width - GRID_PADDING - TOOLBAR_WIDTH, MAX_GRID_WIDTH) / itemWidth
|
||||
);
|
||||
};
|
||||
|
||||
37
src/hooks/useThrottle.ts
Normal file
37
src/hooks/useThrottle.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useUnmount from "./useUnmount";
|
||||
|
||||
const useThrottle = <T>(value: T, ms: number = 200) => {
|
||||
const [state, setState] = useState<T>(value);
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const nextValue = useRef(null) as any;
|
||||
const hasNextValue = useRef(0) as any;
|
||||
|
||||
useEffect(() => {
|
||||
if (!timeout.current) {
|
||||
setState(value);
|
||||
const timeoutCallback = () => {
|
||||
if (hasNextValue.current) {
|
||||
hasNextValue.current = false;
|
||||
setState(nextValue.current);
|
||||
timeout.current = setTimeout(timeoutCallback, ms);
|
||||
} else {
|
||||
timeout.current = undefined;
|
||||
}
|
||||
};
|
||||
timeout.current = setTimeout(timeoutCallback, ms);
|
||||
} else {
|
||||
nextValue.current = value;
|
||||
hasNextValue.current = true;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value]);
|
||||
|
||||
useUnmount(() => {
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default useThrottle;
|
||||
44
src/hooks/useTimeoutFn.ts
Normal file
44
src/hooks/useTimeoutFn.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void];
|
||||
|
||||
export default function useTimeoutFn(
|
||||
fn: Function,
|
||||
ms: number = 0
|
||||
): UseTimeoutFnReturn {
|
||||
const ready = useRef<boolean | null>(false);
|
||||
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
const callback = useRef(fn);
|
||||
|
||||
const isReady = useCallback(() => ready.current, []);
|
||||
|
||||
const set = useCallback(() => {
|
||||
ready.current = false;
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
ready.current = true;
|
||||
callback.current();
|
||||
}, ms);
|
||||
}, [ms]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
ready.current = null;
|
||||
timeout.current && clearTimeout(timeout.current);
|
||||
}, []);
|
||||
|
||||
// update ref when function changes
|
||||
useEffect(() => {
|
||||
callback.current = fn;
|
||||
}, [fn]);
|
||||
|
||||
// set on mount, clear on unmount
|
||||
useEffect(() => {
|
||||
set();
|
||||
|
||||
return clear;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ms]);
|
||||
|
||||
return [isReady, clear, set];
|
||||
}
|
||||
12
src/hooks/useUnmount.ts
Normal file
12
src/hooks/useUnmount.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useRef, useEffect } from "react";
|
||||
|
||||
const useUnmount = (fn: () => any): void => {
|
||||
const fnRef = useRef(fn);
|
||||
|
||||
// update the ref each render so if it change the newest callback will be invoked
|
||||
fnRef.current = fn;
|
||||
|
||||
useEffect(() => () => fnRef.current(), []);
|
||||
};
|
||||
|
||||
export default useUnmount;
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { RecoilRoot } from "recoil";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import App from "./components/App/App";
|
||||
import App from "./components/App";
|
||||
import ReactGA from "react-ga";
|
||||
|
||||
ReactGA.initialize("UA-179205759-1", { titleCase: false });
|
||||
@@ -22,8 +22,8 @@ ReactDOM.render(
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
|
||||
// prettier-ignore
|
||||
console.log(`
|
||||
|
||||
%c sphorphosphor %co%cspho
|
||||
%c s%cphorphosphor %co%csphorpho%cs
|
||||
%c o %cp%chorphosphor %co%csphorphosph%co
|
||||
|
||||
1697
src/lib/icons.ts
1697
src/lib/icons.ts
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { atom } from "recoil";
|
||||
import { ModalInstance } from "../components/Modal/Modal";
|
||||
import { IconStyle } from "../lib";
|
||||
|
||||
export const searchQueryAtom = atom<string>({
|
||||
@@ -25,3 +26,13 @@ export const iconPreviewOpenAtom = atom<string | false>({
|
||||
key: "iconPreviewOpenAtom",
|
||||
default: false,
|
||||
});
|
||||
|
||||
export const modalAtom = atom<((props: ModalInstance) => JSX.Element) | null>({
|
||||
key: "modalAtom",
|
||||
default: null,
|
||||
});
|
||||
|
||||
export const modalOpenAtom = atom<boolean>({
|
||||
key: "modalOpenAtom",
|
||||
default: false,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { selector, selectorFamily } from "recoil";
|
||||
import { DefaultValue, selector, selectorFamily } from "recoil";
|
||||
import TinyColor from "tinycolor2";
|
||||
import Fuse from "fuse.js";
|
||||
|
||||
import { searchQueryAtom, iconColorAtom } from "./atoms";
|
||||
import {
|
||||
searchQueryAtom,
|
||||
iconColorAtom,
|
||||
modalAtom,
|
||||
modalOpenAtom,
|
||||
} from "./atoms";
|
||||
import { IconEntry, IconCategory } from "../lib";
|
||||
import { icons } from "../lib/icons";
|
||||
import { ModalInstance } from "../components/Modal/Modal";
|
||||
|
||||
const fuse = new Fuse(icons, {
|
||||
keys: [{ name: "name", weight: 4 }, "tags", "categories"],
|
||||
@@ -52,7 +58,9 @@ export const singleCategoryQueryResultsSelector = selectorFamily<
|
||||
IconCategory
|
||||
>({
|
||||
key: "singleCategoryQueryResultsSelector",
|
||||
get: (category: IconCategory) => ({ get }) => {
|
||||
get:
|
||||
(category: IconCategory) =>
|
||||
({ get }) => {
|
||||
const filteredResults = get(filteredQueryResultsSelector);
|
||||
return new Promise((resolve) =>
|
||||
resolve(
|
||||
@@ -66,3 +74,24 @@ export const isDarkThemeSelector = selector<boolean>({
|
||||
key: "isDarkThemeSelector",
|
||||
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 };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es6",
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"baseUrl": "./src/",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"sourceMap": true,
|
||||
"declaration": true,
|
||||
"lib": ["es6", "dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"experimentalDecorators": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noEmit": true
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es5"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"build"
|
||||
]
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "build"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user