Shortener.tsx 7.6 KB

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