Shortener.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. import { CopyToClipboard } from "react-copy-to-clipboard";
  2. import { useFormState } from "react-use-form-state";
  3. import { Flex } from "reflexbox/styled-components";
  4. import React, { FC, useState } from "react";
  5. import styled from "styled-components";
  6. import { useStoreActions, useStoreState } from "../store";
  7. import { Checkbox, Select, TextInput } from "./Input";
  8. import { Col, RowCenterH, RowCenter } from "./Layout";
  9. import { useMessage, useCopy } from "../hooks";
  10. import { removeProtocol } from "../utils";
  11. import Text, { H1, Span } from "./Text";
  12. import { Link } from "../store/links";
  13. import Animation from "./Animation";
  14. import { Colors } from "../consts";
  15. import Icon from "./Icon";
  16. const SubmitIconWrapper = styled.div`
  17. content: "";
  18. position: absolute;
  19. top: 0;
  20. right: 12px;
  21. width: 64px;
  22. height: 100%;
  23. display: flex;
  24. justify-content: center;
  25. align-items: center;
  26. cursor: pointer;
  27. :hover svg {
  28. fill: #673ab7;
  29. }
  30. @media only screen and (max-width: 448px) {
  31. right: 8px;
  32. width: 40px;
  33. }
  34. `;
  35. const ShortenedLink = styled(H1)`
  36. cursor: "pointer";
  37. border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
  38. cursor: pointer;
  39. :hover {
  40. opacity: 0.8;
  41. }
  42. `;
  43. interface Form {
  44. target: string;
  45. domain?: string;
  46. customurl?: string;
  47. password?: string;
  48. description?: string;
  49. showAdvanced?: boolean;
  50. }
  51. const defaultDomain = process.env.DEFAULT_DOMAIN;
  52. const Shortener = () => {
  53. const { isAuthenticated } = useStoreState(s => s.auth);
  54. const domains = useStoreState(s => s.settings.domains);
  55. const submit = useStoreActions(s => s.links.submit);
  56. const [link, setLink] = useState<Link | null>(null);
  57. const [message, setMessage] = useMessage(3000);
  58. const [loading, setLoading] = useState(false);
  59. const [copied, setCopied] = useCopy();
  60. const [formState, { raw, password, text, select, label }] = useFormState<
  61. Form
  62. >(
  63. { showAdvanced: false },
  64. {
  65. withIds: true,
  66. onChange(e, stateValues, nextStateValues) {
  67. if (stateValues.showAdvanced && !nextStateValues.showAdvanced) {
  68. formState.clear();
  69. formState.setField("target", stateValues.target);
  70. }
  71. }
  72. }
  73. );
  74. const submitLink = async (reCaptchaToken?: string) => {
  75. try {
  76. const link = await submit({ ...formState.values, reCaptchaToken });
  77. setLink(link);
  78. formState.clear();
  79. } catch (err) {
  80. setMessage(
  81. err?.response?.data?.error || "Couldn't create the short link."
  82. );
  83. }
  84. setLoading(false);
  85. };
  86. const onSubmit = async e => {
  87. e.preventDefault();
  88. if (loading) return;
  89. setCopied(false);
  90. setLoading(true);
  91. if (process.env.NODE_ENV === "production" && !isAuthenticated) {
  92. window.grecaptcha.execute(window.captchaId);
  93. const getCaptchaToken = () => {
  94. setTimeout(() => {
  95. if (window.isCaptchaReady) {
  96. const reCaptchaToken = window.grecaptcha.getResponse(
  97. window.captchaId
  98. );
  99. window.isCaptchaReady = false;
  100. window.grecaptcha.reset(window.captchaId);
  101. return submitLink(reCaptchaToken);
  102. }
  103. return getCaptchaToken();
  104. }, 200);
  105. };
  106. return getCaptchaToken();
  107. }
  108. return submitLink();
  109. };
  110. const title = !link && (
  111. <H1 fontSize={[25, 27, 32]} light>
  112. Kutt your links{" "}
  113. <Span style={{ borderBottom: "2px dotted #999" }} light>
  114. shorter
  115. </Span>
  116. .
  117. </H1>
  118. );
  119. const result = link && (
  120. <Animation
  121. as={RowCenter}
  122. offset="-20px"
  123. duration="0.4s"
  124. style={{ position: "relative" }}
  125. >
  126. {copied ? (
  127. <Animation offset="10px" duration="0.2s" alignItems="center">
  128. <Icon
  129. size={[30, 35]}
  130. py={0}
  131. px={0}
  132. mr={3}
  133. p={["4px", "5px"]}
  134. name="check"
  135. strokeWidth="3"
  136. stroke={Colors.CheckIcon}
  137. />
  138. </Animation>
  139. ) : (
  140. <Animation offset="-10px" duration="0.2s">
  141. <CopyToClipboard text={link.link} onCopy={setCopied}>
  142. <Icon
  143. as="button"
  144. py={0}
  145. px={0}
  146. mr={3}
  147. size={[30, 35]}
  148. p={["6px", "7px"]}
  149. name="copy"
  150. strokeWidth="2.5"
  151. stroke={Colors.CopyIcon}
  152. backgroundColor={Colors.CopyIconBg}
  153. />
  154. </CopyToClipboard>
  155. </Animation>
  156. )}
  157. <CopyToClipboard text={link.link} onCopy={setCopied}>
  158. <ShortenedLink fontSize={[24, 26, 30]} pb="2px" light>
  159. {removeProtocol(link.link)}
  160. </ShortenedLink>
  161. </CopyToClipboard>
  162. </Animation>
  163. );
  164. return (
  165. <Col width={800} maxWidth="100%" px={[3]} flex="0 0 auto" mt={4}>
  166. <RowCenterH mb={[4, 48]}>
  167. {title}
  168. {result}
  169. </RowCenterH>
  170. <Flex
  171. as="form"
  172. id="shortenerform"
  173. width={1}
  174. alignItems="center"
  175. justifyContent="center"
  176. style={{ position: "relative" }}
  177. onSubmit={onSubmit}
  178. >
  179. <TextInput
  180. {...text("target")}
  181. placeholder="Paste your long URL"
  182. placeholderSize={[16, 17, 18]}
  183. fontSize={[18, 20, 22]}
  184. width={1}
  185. height={[58, 64, 72]}
  186. px={0}
  187. pr={[48, 84]}
  188. pl={[32, 40]}
  189. autoFocus
  190. data-lpignore
  191. />
  192. <SubmitIconWrapper onClick={onSubmit}>
  193. <Icon
  194. name={loading ? "spinner" : "send"}
  195. size={[22, 26, 28]}
  196. fill={loading ? "none" : "#aaa"}
  197. stroke={loading ? Colors.Spinner : "none"}
  198. mb={1}
  199. mr={1}
  200. />
  201. </SubmitIconWrapper>
  202. </Flex>
  203. {message.text && (
  204. <Text color={message.color} mt={24} mb={1} textAlign="center">
  205. {message.text}
  206. </Text>
  207. )}
  208. <Checkbox
  209. {...raw({
  210. name: "showAdvanced",
  211. onChange: e => {
  212. if (!isAuthenticated) {
  213. setMessage(
  214. "You need to log in or sign up to use advanced options."
  215. );
  216. return false;
  217. }
  218. return !formState.values.showAdvanced;
  219. }
  220. })}
  221. checked={formState.values.showAdvanced}
  222. label="Show advanced options"
  223. mt={[3, 24]}
  224. alignSelf="flex-start"
  225. />
  226. {formState.values.showAdvanced && (
  227. <div>
  228. <Flex mt={4} flexDirection={["column", "row"]}>
  229. <Col mb={[3, 0]}>
  230. <Text
  231. as="label"
  232. {...label("domain")}
  233. fontSize={[14, 15]}
  234. mb={2}
  235. bold
  236. >
  237. Domain
  238. </Text>
  239. <Select
  240. {...select("domain")}
  241. data-lpignore
  242. pl={[3, 24]}
  243. pr={[3, 24]}
  244. fontSize={[14, 15]}
  245. height={[40, 44]}
  246. width={[1, 210, 240]}
  247. options={[
  248. { key: defaultDomain, value: "" },
  249. ...domains.map(d => ({
  250. key: d.address,
  251. value: d.address
  252. }))
  253. ]}
  254. />
  255. </Col>
  256. <Col mb={[3, 0]} ml={[0, 24]}>
  257. <Text
  258. as="label"
  259. {...label("customurl")}
  260. fontSize={[14, 15]}
  261. mb={2}
  262. bold
  263. >
  264. {formState.values.domain || defaultDomain}/
  265. </Text>
  266. <TextInput
  267. {...text("customurl")}
  268. placeholder="Custom address..."
  269. autocomplete="off"
  270. data-lpignore
  271. pl={[3, 24]}
  272. pr={[3, 24]}
  273. placeholderSize={[13, 14]}
  274. fontSize={[14, 15]}
  275. height={[40, 44]}
  276. width={[1, 210, 240]}
  277. />
  278. </Col>
  279. <Col ml={[0, 24]}>
  280. <Text
  281. as="label"
  282. {...label("password")}
  283. fontSize={[14, 15]}
  284. mb={2}
  285. bold
  286. >
  287. Password:
  288. </Text>
  289. <TextInput
  290. {...password("password")}
  291. placeholder="Password..."
  292. autocomplete="off"
  293. data-lpignore
  294. pl={[3, 24]}
  295. pr={[3, 24]}
  296. placeholderSize={[13, 14]}
  297. fontSize={[14, 15]}
  298. height={[40, 44]}
  299. width={[1, 210, 240]}
  300. />
  301. </Col>
  302. </Flex>
  303. <Flex mt={[3]} flexDirection={["column", "row"]}>
  304. <Col width={1}>
  305. <Text
  306. as="description"
  307. {...label("description")}
  308. fontSize={[14, 15]}
  309. mb={2}
  310. bold
  311. >
  312. Description
  313. </Text>
  314. <TextInput
  315. {...text("description")}
  316. placeholder="Description"
  317. data-lpignore
  318. pl={[3, 24]}
  319. pr={[3, 24]}
  320. placeholderSize={[13, 14]}
  321. fontSize={[14, 15]}
  322. height={[40, 44]}
  323. width={1}
  324. maxWidth="100%"
  325. />
  326. </Col>
  327. </Flex>
  328. </div>
  329. )}
  330. </Col>
  331. );
  332. };
  333. export default Shortener;