Procházet zdrojové kódy

refactor: icons and layout

poeti8 před 6 roky
rodič
revize
ac275fd71c

+ 1 - 20
client/components/Button.tsx

@@ -17,7 +17,7 @@ interface Props extends BoxProps {
   type?: "button" | "submit" | "reset";
 }
 
-const StyledButton = styled(Flex)<Props>`
+const Button = styled(Flex)<Props>`
   position: relative;
   align-items: center;
   justify-content: center;
@@ -96,25 +96,6 @@ const Icon = styled(SVG)`
   }
 `;
 
-export const Button: FC<Props> = props => {
-  const SVGIcon = props.icon ? (
-    <Icon
-      icon={props.icon}
-      isRound={props.isRound}
-      color={props.color}
-      src={`/images/${props.icon}.svg`}
-    />
-  ) : (
-    ""
-  );
-  return (
-    <StyledButton {...props}>
-      {SVGIcon}
-      {props.icon !== "loader" && props.children}
-    </StyledButton>
-  );
-};
-
 Button.defaultProps = {
   as: "button",
   width: "auto",

+ 11 - 1
client/components/Checkbox.tsx

@@ -1,5 +1,5 @@
 import React, { FC } from "react";
-import styled, { css } from "styled-components";
+import styled, { css, keyframes } from "styled-components";
 import { ifProp } from "styled-tools";
 import { Flex, BoxProps } from "reflexbox/styled-components";
 
@@ -55,6 +55,16 @@ const Box = styled(Flex).attrs({
         background-color: #9575cd;
         box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2);
         cursor: pointer;
+        animation: ${keyframes`
+          from {
+            opacity: 0;
+            transform: scale(0, 0);
+          }
+          to {
+            opacity: 1;
+            transform: scale(1, 1);
+          }
+        `} 0.1s ease-in;
       }
     `
   )}

+ 1 - 1
client/components/Divider.tsx

@@ -5,7 +5,7 @@ import { Colors } from "../consts";
 
 const Divider = styled(Flex).attrs({ as: "hr" })`
   width: 100%;
-  height: 1px;
+  height: 2px;
   outline: none;
   border: none;
   background-color: ${Colors.Divider};

+ 1 - 1
client/components/Features.tsx

@@ -28,7 +28,7 @@ const Features = () => (
         Create, protect and delete your links and monitor them with detailed
         statistics.
       </FeaturesItem>
-      <FeaturesItem title="Custom domain" icon="navigation">
+      <FeaturesItem title="Custom domain" icon="shuffle">
         Use custom domains for your links. Add or remove them for free.
       </FeaturesItem>
       <FeaturesItem title="API" icon="zap">

+ 4 - 15
client/components/FeaturesItem.tsx

@@ -3,10 +3,12 @@ import styled from "styled-components";
 import { Flex } from "reflexbox/styled-components";
 
 import { fadeIn } from "../helpers/animations";
+import Icon from "./Icon";
+import { Icons } from "./Icon/Icon";
 
 interface Props {
   title: string;
-  icon: string; // TODO: better typing
+  icon: Icons;
 }
 
 const Block = styled(Flex).attrs({
@@ -34,19 +36,6 @@ const IconBox = styled(Flex).attrs({
   background-color: #2196f3;
 `;
 
-const Icon = styled.img`
-  display: inline-block;
-  width: 16px;
-  height: 16px;
-  margin: 0;
-  padding: 0;
-
-  @media only screen and (max-width: 448px) {
-    width: 14px;
-    height: 14px;
-  }
-`;
-
 const Title = styled.h3`
   margin: 16px;
   font-size: 15px;
@@ -71,7 +60,7 @@ const Description = styled.p`
 const FeaturesItem: FC<Props> = ({ children, icon, title }) => (
   <Block>
     <IconBox>
-      <Icon src={`/images/${icon}.svg`} />
+      <Icon name={icon} stroke="white" strokeWidth="2" />
     </IconBox>
     <Title>{title}</Title>
     <Description>{children}</Description>

+ 0 - 48
client/components/HeaderLogo.tsx

@@ -1,48 +0,0 @@
-import React, { FC } from "react";
-import Router from "next/router";
-import styled from "styled-components";
-
-const LogoImage = styled.div`
-  & > a {
-    position: relative;
-    display: flex;
-    align-items: center;
-    margin: 0 8px 0 0;
-    font-size: 22px;
-    font-weight: bold;
-    text-decoration: none;
-    color: inherit;
-    transition: border-color 0.2s ease-out;
-  }
-
-  @media only screen and (max-width: 488px) {
-    a {
-      font-size: 18px;
-    }
-  }
-
-  img {
-    width: 18px;
-    margin-right: 11px;
-  }
-`;
-
-const HeaderLogo: FC = () => {
-  const goTo = e => {
-    e.preventDefault();
-    const path = e.target.getAttribute("href");
-    if (window.location.pathname === path) return;
-    Router.push(path);
-  };
-
-  return (
-    <LogoImage>
-      <a href="/" title="Homepage" onClick={goTo}>
-        <img src="/images/logo.svg" alt="" />
-        Kutt.it
-      </a>
-    </LogoImage>
-  );
-};
-
-export default HeaderLogo;

+ 21 - 0
client/components/Icon/ArrowLeft.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+
+function ArrowLeft() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M19 12H6m6-7l-7 7 7 7"></path>
+    </svg>
+  );
+}
+
+export default React.memo(ArrowLeft);

+ 22 - 0
client/components/Icon/Edit.tsx

@@ -0,0 +1,22 @@
+import React from "react";
+
+function Edit() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M20 14.66V20a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2h5.34"></path>
+      <path d="M18 2L22 6 12 16 8 16 8 12 18 2z"></path>
+    </svg>
+  );
+}
+
+export default React.memo(Edit);

+ 21 - 0
client/components/Icon/Heart.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+
+function Heart() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M20.84 4.61a5.5 5.5 0 00-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 00-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 000-7.78z"></path>
+    </svg>
+  );
+}
+
+export default React.memo(Heart);

+ 24 - 26
client/components/Icon/Icon.tsx

@@ -7,57 +7,55 @@ import ChevronRight from "./ChevronRight";
 import ChevronLeft from "./ChevronLeft";
 import { Colors } from "../../consts";
 import Clipboard from "./Clipboard";
+import ArrowLeft from "./ArrowLeft";
 import PieChart from "./PieChart";
 import Refresh from "./Refresh";
 import Spinner from "./Spinner";
+import Shuffle from "./Shuffle";
 import QRCode from "./QRCode";
+import Signup from "./Signup";
 import Trash from "./Trash";
 import Check from "./Check";
+import Login from "./Login";
+import Heart from "./Heart";
 import Plus from "./Plus";
 import Lock from "./Lock";
+import Edit from "./Edit";
 import Copy from "./Copy";
 import Send from "./Send";
 import Key from "./Key";
 import Zap from "./Zap";
-
-export interface IIcons {
-  clipboard: JSX.Element;
-  chevronRight: JSX.Element;
-  chevronLeft: JSX.Element;
-  pieChart: JSX.Element;
-  key: JSX.Element;
-  plus: JSX.Element;
-  Lock: JSX.Element;
-  copy: JSX.Element;
-  refresh: JSX.Element;
-  check: JSX.Element;
-  send: JSX.Element;
-  spinner: JSX.Element;
-  trash: JSX.Element;
-  zap: JSX.Element;
-  qrcode: JSX.Element;
-}
+import X from "./X";
 
 const icons = {
-  clipboard: Clipboard,
-  chevronRight: ChevronRight,
+  arrowLeft: ArrowLeft,
+  check: Check,
   chevronLeft: ChevronLeft,
-  pieChart: PieChart,
+  chevronRight: ChevronRight,
+  clipboard: Clipboard,
+  shuffle: Shuffle,
+  copy: Copy,
+  heart: Heart,
+  edit: Edit,
   key: Key,
   lock: Lock,
-  check: Check,
+  login: Login,
+  pieChart: PieChart,
   plus: Plus,
-  copy: Copy,
+  qrcode: QRCode,
   refresh: Refresh,
   send: Send,
+  signup: Signup,
   spinner: Spinner,
   trash: Trash,
-  zap: Zap,
-  qrcode: QRCode
+  x: X,
+  zap: Zap
 };
 
+export type Icons = keyof typeof icons;
+
 interface Props extends React.ComponentProps<typeof Flex> {
-  name: keyof typeof icons;
+  name: Icons;
   stroke?: string;
   fill?: string;
   hoverFill?: string;

+ 21 - 0
client/components/Icon/Login.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+
+function Login() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4m-5-4l5-5-5-5m3.8 5H3"></path>
+    </svg>
+  );
+}
+
+export default React.memo(Login);

+ 21 - 0
client/components/Icon/Shuffle.tsx

@@ -0,0 +1,21 @@
+import React from "react";
+
+function Shuffle() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M16 3h5v5M4 20L20.2 3.8M21 16v5h-5m-1-6l5.1 5.1M4 4l5 5"></path>
+    </svg>
+  );
+}
+
+export default React.memo(Shuffle);

+ 24 - 0
client/components/Icon/Signup.tsx

@@ -0,0 +1,24 @@
+import React from "react";
+
+function Signup() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"></path>
+      <circle cx="8.5" cy="7" r="4"></circle>
+      <path d="M20 8L20 14"></path>
+      <path d="M23 11L17 11"></path>
+    </svg>
+  );
+}
+
+export default React.memo(Signup);

+ 3 - 3
client/components/Icon/Spinner.tsx

@@ -1,5 +1,5 @@
-import React from "react";
 import styled, { keyframes } from "styled-components";
+import React from "react";
 
 const spin = keyframes`
   from {
@@ -11,8 +11,8 @@ const spin = keyframes`
 `;
 
 const Svg = styled.svg`
- animation: ${spin} 1s linear infinite;
-`
+  animation: ${spin} 1s linear infinite;
+`;
 
 function Spinner() {
   return (

+ 22 - 0
client/components/Icon/X.tsx

@@ -0,0 +1,22 @@
+import React from "react";
+
+function X() {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="48"
+      height="48"
+      fill="none"
+      stroke="#000"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+      strokeWidth="2"
+      viewBox="0 0 24 24"
+    >
+      <path d="M18 6L6 18"></path>
+      <path d="M6 6L18 18"></path>
+    </svg>
+  );
+}
+
+export default React.memo(X);

+ 48 - 37
client/components/Settings/SettingsApi.tsx

@@ -9,29 +9,29 @@ import ALink from "../ALink";
 import Icon from "../Icon";
 import Text, { H2 } from "../Text";
 import { Col } from "../Layout";
+import { useCopy } from "../../hooks";
+import Animation from "../Animation";
+import { Colors } from "../../consts";
 
 const ApiKey = styled(Text).attrs({
-  mr: 3,
-  fontSize: [14, 16],
-  fontWeight: 700
+  mt: "2px",
+  fontSize: [15, 16],
+  bold: true
 })`
-  max-width: 100%;
-  border-bottom: 2px dotted #999;
+  border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
+  cursor: pointer;
+
+  :hover {
+    opacity: 0.8;
+  }
 `;
 
 const SettingsApi: FC = () => {
-  const [copied, setCopied] = useState(false);
+  const [copied, setCopied] = useCopy();
   const [loading, setLoading] = useState(false);
   const apikey = useStoreState(s => s.settings.apikey);
   const generateApiKey = useStoreActions(s => s.settings.generateApiKey);
 
-  const onCopy = () => {
-    setCopied(true);
-    setTimeout(() => {
-      setCopied(false);
-    }, 1500);
-  };
-
   const onSubmit = async () => {
     if (loading) return;
     setLoading(true);
@@ -58,33 +58,44 @@ const SettingsApi: FC = () => {
         </ALink>
       </Text>
       {apikey && (
-        <Col style={{ position: "relative" }} my={3}>
-          {copied && (
-            <Text
-              color="green"
-              fontSize={14}
-              style={{ position: "absolute", top: -24 }}
-            >
-              Copied to clipboard.
-            </Text>
+        <Flex alignItems="center" my={3}>
+          {copied ? (
+            <Animation offset="10px" duration="0.2s">
+              <Icon
+                size={[23, 24]}
+                py={0}
+                px={0}
+                mr={2}
+                p="3px"
+                name="check"
+                strokeWidth="3"
+                stroke={Colors.CheckIcon}
+              />
+            </Animation>
+          ) : (
+            <Animation offset="-10px" duration="0.2s">
+              <CopyToClipboard text={apikey} onCopy={setCopied}>
+                <Icon
+                  as="button"
+                  py={0}
+                  px={0}
+                  mr={2}
+                  size={[23, 24]}
+                  p={["4px", "5px"]}
+                  name="copy"
+                  strokeWidth="2.5"
+                  stroke={Colors.CopyIcon}
+                  backgroundColor={Colors.CopyIconBg}
+                />
+              </CopyToClipboard>
+            </Animation>
           )}
-          <Flex
-            maxWidth="100%"
-            flexDirection={["column", "column", "row"]}
-            flexWrap="wrap"
-            alignItems={["flex-start", "flex-start", "center"]}
-            mb={16}
-          >
+          <CopyToClipboard text={apikey} onCopy={setCopied}>
             <ApiKey>{apikey}</ApiKey>
-            <CopyToClipboard text={apikey} onCopy={onCopy}>
-              <Button icon="copy" height={36} mt={[3, 3, 0]}>
-                Copy
-              </Button>
-            </CopyToClipboard>
-          </Flex>
-        </Col>
+          </CopyToClipboard>
+        </Flex>
       )}
-      <Button color="purple" onClick={onSubmit} disabled={loading}>
+      <Button mt={2} color="purple" onClick={onSubmit} disabled={loading}>
         <Icon name={loading ? "spinner" : "zap"} mr={2} stroke="white" />
         {loading ? "Generating..." : apikey ? "Regenerate" : "Generate"} key
       </Button>

+ 4 - 11
client/components/Shortener.tsx

@@ -8,7 +8,7 @@ import { Col, RowCenterH, RowCenter } from "./Layout";
 import { useFormState } from "react-use-form-state";
 import { removeProtocol } from "../utils";
 import { Link } from "../store/links";
-import { useMessage } from "../hooks";
+import { useMessage, useCopy } from "../hooks";
 import TextInput from "./TextInput";
 import Animation from "./Animation";
 import { Colors } from "../consts";
@@ -61,7 +61,7 @@ const Shortener = () => {
   const [link, setLink] = useState<Link | null>(null);
   const [message, setMessage] = useMessage(3000);
   const [loading, setLoading] = useState(false);
-  const [copied, setCopied] = useState(false);
+  const [copied, setCopied] = useCopy();
   const [formState, { raw, password, text, label }] = useFormState<Form>(
     { showAdvanced: false },
     {
@@ -125,13 +125,6 @@ const Shortener = () => {
     </H1>
   );
 
-  const onCopy = () => {
-    setCopied(true);
-    setTimeout(() => {
-      setCopied(false);
-    }, 1500);
-  };
-
   const result = link && (
     <Animation
       as={RowCenter}
@@ -154,7 +147,7 @@ const Shortener = () => {
         </Animation>
       ) : (
         <Animation offset="-10px" duration="0.2s">
-          <CopyToClipboard text={link.shortLink} onCopy={onCopy}>
+          <CopyToClipboard text={link.shortLink} onCopy={setCopied}>
             <Icon
               as="button"
               py={0}
@@ -170,7 +163,7 @@ const Shortener = () => {
           </CopyToClipboard>
         </Animation>
       )}
-      <CopyToClipboard text={link.shortLink} onCopy={onCopy}>
+      <CopyToClipboard text={link.shortLink} onCopy={setCopied}>
         <ShortenedLink fontSize={[30]} pb="2px" light>
           {removeProtocol(link.shortLink)}
         </ShortenedLink>

+ 2 - 1
client/consts/consts.ts

@@ -21,6 +21,7 @@ export enum Colors {
   Spinner = "hsl(200, 15%, 70%)",
   FeaturesBg = "hsl(230, 15%, 92%)",
   ExtensionsBg = "hsl(230, 15%, 20%)",
+  Icon = "hsl(200, 35%, 45%)",
   IconShadow = "hsla(200, 15%, 60%, 0.12)",
   CopyIcon = "hsl(144, 40%, 57%)",
   CopyIconBg = "hsl(144, 100%, 96%)",
@@ -38,5 +39,5 @@ export enum Colors {
   TableShadow = "hsla(200, 20%, 70%, 0.3)",
   StatsLastUpdateText = "hsl(200, 14%, 60%)",
   StatsTotalUnderline = "hsl(200, 35%, 65%)",
-  Divider = "hsl(200, 15%, 90%)"
+  Divider = "hsl(200, 20%, 92%)"
 }

+ 14 - 0
client/hooks.ts

@@ -15,3 +15,17 @@ export const useMessage = (timeout?: number) => {
 
   return [message, setMessage] as const;
 };
+
+export const useCopy = (timeout = 1500) => {
+  const [copied, set] = useState(false);
+
+  const setCopied = (isCopied = true) => {
+    set(isCopied);
+
+    if (isCopied && timeout) {
+      setTimeout(() => set(false), timeout);
+    }
+  };
+
+  return [copied, setCopied] as const;
+};

+ 15 - 6
client/pages/login.tsx

@@ -10,11 +10,12 @@ import { Flex } from "reflexbox/styled-components";
 import { useStoreState, useStoreActions } from "../store";
 import { ColCenterV } from "../components/Layout";
 import AppWrapper from "../components/AppWrapper";
-import { fadeIn } from "../helpers/animations";
 import TextInput from "../components/TextInput";
+import { fadeIn } from "../helpers/animations";
 import { Button } from "../components/Button";
-import ALink from "../components/ALink";
 import Text, { H2 } from "../components/Text";
+import ALink from "../components/ALink";
+import Icon from "../components/Icon";
 import { API } from "../consts";
 
 const LoginForm = styled(Flex).attrs({
@@ -111,7 +112,7 @@ const LoginPage = () => {
               {...email("email")}
               placeholder="Email address..."
               height={[56, 64, 72]}
-              mb={[24, 32, 36]}
+              mb={[24, 4, 48]}
               autoFocus
             />
             <Text {...label("password")} as="label" mb={2} bold>
@@ -121,26 +122,34 @@ const LoginPage = () => {
               {...password("password")}
               placeholder="Password..."
               height={[56, 64, 72]}
-              mb={[24, 32, 36]}
+              mb={[24, 4, 48]}
             />
             <Flex justifyContent="center">
               <Button
                 flex="1 1 auto"
                 mr={["8px", 16]}
                 height={[44, 56]}
-                icon={loading.login ? "loader" : "login"}
                 onClick={onSubmit("login")}
               >
+                <Icon
+                  name={loading.login ? "spinner" : "login"}
+                  stroke="white"
+                  mr={2}
+                />
                 Log in
               </Button>
               <Button
                 flex="1 1 auto"
                 ml={["8px", 16]}
                 height={[44, 56]}
-                icon={loading.signup ? "loader" : "signup"}
                 color="purple"
                 onClick={onSubmit("signup")}
               >
+                <Icon
+                  name={loading.signup ? "spinner" : "signup"}
+                  stroke="white"
+                  mr={2}
+                />
                 Sign up
               </Button>
             </Flex>

+ 5 - 9
client/pages/report.tsx

@@ -3,13 +3,14 @@ import axios from "axios";
 import { useFormState } from "react-use-form-state";
 import { Flex } from "reflexbox/styled-components";
 
+import Text, { H2, Span } from "../components/Text";
 import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
-import Text, { H2, Span } from "../components/Text";
+import { Col } from "../components/Layout";
+import Icon from "../components/Icon";
 import { useMessage } from "../hooks";
 import { API } from "../consts";
-import { Col } from "../components/Layout";
 
 const ReportPage = () => {
   const [formState, { text }] = useFormState<{ url: string }>();
@@ -63,13 +64,8 @@ const ReportPage = () => {
             mr={3}
             required
           />
-          <Button
-            type="submit"
-            icon={loading ? "loader" : ""}
-            flex="0 0 auto"
-            height={[40, 44]}
-            mt={[3, 0]}
-          >
+          <Button type="submit" flex="0 0 auto" height={[40, 44]} mt={[3, 0]}>
+            {loading && <Icon name={"spinner"} stroke="white" mr={2} />}
             Send report
           </Button>
         </Flex>

+ 5 - 8
client/pages/reset-password.tsx

@@ -11,11 +11,12 @@ import { useStoreState, useStoreActions } from "../store";
 import AppWrapper from "../components/AppWrapper";
 import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
-import { TokenPayload } from "../types";
 import Text, { H2 } from "../components/Text";
+import { Col } from "../components/Layout";
+import { TokenPayload } from "../types";
 import { useMessage } from "../hooks";
+import Icon from "../components/Icon";
 import { API } from "../consts";
-import { Col } from "../components/Layout";
 
 interface Props {
   token?: string;
@@ -86,12 +87,8 @@ const ResetPassword: NextPage<Props> = ({ token }) => {
             autoFocus
             required
           />
-          <Button
-            icon={loading ? "loader" : ""}
-            type="submit"
-            height={[40, 44]}
-            my={3}
-          >
+          <Button type="submit" height={[40, 44]} my={3}>
+            {loading && <Icon name={"spinner"} stroke="white" mr={2} />}
             Reset password
           </Button>
         </Flex>

+ 4 - 4
client/pages/settings.tsx

@@ -31,17 +31,17 @@ const SettingsPage: NextPage = props => {
           </Span>
           .
         </H1>
-        <Divider my={[4, 48]} />
+        <Divider mt={4} mb={48} />
         {isAdmin && (
           <>
             <SettingsBan />
-            <Divider my={[12, 24]} />
+            <Divider mt={4} mb={48} />
           </>
         )}
         <SettingsDomain />
-        <Divider my={[12, 24]} />
+        <Divider mt={4} mb={48} />
         <SettingsPassword />
-        <Divider my={[12, 24]} />
+        <Divider mt={4} mb={48} />
         <SettingsApi />
       </Col>
       <Footer />

+ 19 - 7
client/pages/stats.tsx

@@ -5,17 +5,18 @@ import { NextPage } from "next";
 import Link from "next/link";
 import axios from "axios";
 
+import Text, { H1, H2, H4, Span } from "../components/Text";
 import { getAxiosConfig, removeProtocol } from "../utils";
 import { Button, NavButton } from "../components/Button";
 import { Col, RowCenterV } from "../components/Layout";
 import { Area, Bar, Pie } from "../components/Charts";
 import PageLoading from "../components/PageLoading";
 import AppWrapper from "../components/AppWrapper";
-import Text, { H1, H2, H4, Span } from "../components/Text";
 import Divider from "../components/Divider";
 import { useStoreState } from "../store";
 import ALink from "../components/ALink";
 import { API, Colors } from "../consts";
+import Icon from "../components/Icon";
 
 interface Props {
   domain?: string;
@@ -32,7 +33,7 @@ const StatsPage: NextPage<Props> = ({ domain, id }) => {
   const stats = data && data[period];
 
   useEffect(() => {
-    if (!id) return;
+    if (!id || !isAuthenticated) return;
     axios
       .get(`${API.STATS}?id=${id}&domain=${domain}`, getAxiosConfig())
       .then(({ data }) => {
@@ -49,13 +50,21 @@ const StatsPage: NextPage<Props> = ({ domain, id }) => {
   let errorMessage;
 
   if (!isAuthenticated) {
-    // TODO: use icons
-    errorMessage = <H2>You need to login to view stats.</H2>;
+    errorMessage = (
+      <Flex mt={3}>
+        <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
+        <H2>You need to login to view stats.</H2>
+      </Flex>
+    );
   }
 
   if (!id || error) {
-    // TODO: use icons
-    errorMessage = <H2>Couldn't get stats.</H2>;
+    errorMessage = (
+      <Flex mt={3}>
+        <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
+        <H2>Couldn't get stats.</H2>
+      </Flex>
+    );
   }
 
   const loader = loading && <PageLoading />;
@@ -181,7 +190,10 @@ const StatsPage: NextPage<Props> = ({ domain, id }) => {
             <Box alignSelf="center" my={64}>
               <Link href="/">
                 <ALink href="/" title="Back to homepage" forButton>
-                  <Button icon="arrow-left">Back to homepage</Button>
+                  <Button>
+                    <Icon name="arrowLeft" stroke="white" mr={2} />
+                    Back to homepage
+                  </Button>
                 </ALink>
               </Link>
             </Box>

+ 3 - 5
client/pages/url-password.tsx

@@ -9,6 +9,7 @@ import TextInput from "../components/TextInput";
 import { Button } from "../components/Button";
 import Text, { H2 } from "../components/Text";
 import { Col } from "../components/Layout";
+import Icon from "../components/Icon";
 
 interface Props {
   protectedLink?: string;
@@ -69,11 +70,8 @@ const UrlPasswordPage: NextPage<Props> = ({ protectedLink }) => {
               autoFocus
               required
             />
-            <Button
-              type="submit"
-              icon={loading ? "loader" : ""}
-              height={[40, 44]}
-            >
+            <Button type="submit" height={[40, 44]}>
+              {loading && <Icon name={"spinner"} stroke="white" mr={2} />}
               Go
             </Button>
           </Flex>

+ 28 - 27
client/pages/verify.tsx

@@ -1,16 +1,19 @@
 import { Flex } from "reflexbox/styled-components";
 import React, { useEffect } from "react";
 import styled from "styled-components";
-import Router from "next/router";
 import decode from "jwt-decode";
 import cookie from "js-cookie";
+import Link from "next/link";
 
 import AppWrapper from "../components/AppWrapper";
 import { Button } from "../components/Button";
 import { useStoreActions } from "../store";
+import { Col } from "../components/Layout";
 import { TokenPayload } from "../types";
+import Icon from "../components/Icon";
 import { NextPage } from "next";
-import { Col } from "../components/Layout";
+import { Colors } from "../consts";
+import ALink from "../components/ALink";
 
 interface Props {
   token?: string;
@@ -31,18 +34,6 @@ const Message = styled.p`
   }
 `;
 
-const Icon = styled.img`
-  width: 32px;
-  height: 32px;
-  margin-right: 16px;
-
-  @media only screen and (max-width: 768px) {
-    width: 26px;
-    height: 26px;
-    margin-right: 8px;
-  }
-`;
-
 const Verify: NextPage<Props> = ({ token }) => {
   const addAuth = useStoreActions(s => s.auth.add);
 
@@ -54,28 +45,38 @@ const Verify: NextPage<Props> = ({ token }) => {
     }
   }, []);
 
-  const goToHomepage = e => {
-    e.preventDefault();
-    Router.push("/");
-  };
-
   return (
     <AppWrapper>
       {token ? (
         <Col alignItems="center">
           <MessageWrapper>
-            <Icon src="/images/check.svg" />
+            <Icon name="check" size={32} mr={3} stroke={Colors.CheckIcon} />
             <Message>Your account has been verified successfully!</Message>
           </MessageWrapper>
-          <Button icon="arrow-left" onClick={goToHomepage}>
-            Back to homepage
-          </Button>
+          <Link href="/">
+            <ALink href="/" forButton>
+              <Button>
+                <Icon name="arrowLeft" stroke="white" mr={2} />
+                Back to homepage
+              </Button>
+            </ALink>
+          </Link>
         </Col>
       ) : (
-        <MessageWrapper>
-          <Icon src="/images/x.svg" />
-          <Message>Invalid verification.</Message>
-        </MessageWrapper>
+        <Col alignItems="center">
+          <MessageWrapper>
+            <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
+            <Message>Invalid verification.</Message>
+          </MessageWrapper>
+          <Link href="/login">
+            <ALink href="/login" forButton>
+              <Button color="purple">
+                <Icon name="arrowLeft" stroke="white" mr={2} />
+                Back to signup
+              </Button>
+            </ALink>
+          </Link>
+        </Col>
       )}
     </AppWrapper>
   );

binární
dump.rdb


+ 5 - 4
server/controllers/authController.ts

@@ -83,10 +83,7 @@ const authenticate = (
     })(req, res, next);
   };
 
-export const authLocal = authenticate(
-  "local",
-  "Login email and/or password are wrong."
-);
+export const authLocal = authenticate("local", "Login credentials are wrong.");
 export const authJwt = authenticate("jwt", "Unauthorized.");
 export const authJwtLoose = authenticate("jwt", "Unauthorized.", false);
 export const authApikey = authenticate(
@@ -180,11 +177,15 @@ export const renew: Handler = (req, res) => {
 };
 
 export const verify: Handler = async (req, _res, next) => {
+  const { verificationToken } = req.params;
+  if (!verificationToken) return next();
+
   const user = await verifyUser(req.params.verificationToken);
   if (user) {
     const token = signToken(user);
     req.token = token;
   }
+
   return next();
 };