Shortener.tsx 10 KB

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