ソースを参照

delete nextjs, update packages

Pouria Ezzati 1 年間 前
コミット
698cf6e305
100 ファイル変更1071 行追加11311 行削除
  1. 1 0
      .gitignore
  2. 0 7
      .travis.yml
  3. 1 1
      Dockerfile
  4. 0 51
      client/components/ALink.tsx
  5. 0 17
      client/components/Animation.ts
  6. 0 54
      client/components/AppWrapper.tsx
  7. 0 118
      client/components/Button.tsx
  8. 0 80
      client/components/Charts/Area.tsx
  9. 0 40
      client/components/Charts/Bar.tsx
  10. 0 83
      client/components/Charts/Map.tsx
  11. 0 33
      client/components/Charts/Pie.tsx
  12. 0 4
      client/components/Charts/index.tsx
  13. 0 48
      client/components/Charts/world.json
  14. 0 14
      client/components/Divider.tsx
  15. 0 116
      client/components/Extensions.tsx
  16. 0 43
      client/components/Features.tsx
  17. 0 71
      client/components/FeaturesItem.tsx
  18. 0 66
      client/components/Footer.tsx
  19. 0 184
      client/components/Header.tsx
  20. 0 21
      client/components/Icon/ArrowLeft.tsx
  21. 0 21
      client/components/Icon/Check.tsx
  22. 0 22
      client/components/Icon/ChevronLeft.tsx
  23. 0 22
      client/components/Icon/ChevronRight.tsx
  24. 0 23
      client/components/Icon/Clipboard.tsx
  25. 0 23
      client/components/Icon/Copy.tsx
  26. 0 22
      client/components/Icon/Edit.tsx
  27. 0 21
      client/components/Icon/EditAlt.tsx
  28. 0 21
      client/components/Icon/Heart.tsx
  29. 0 155
      client/components/Icon/Icon.tsx
  30. 0 22
      client/components/Icon/Key.tsx
  31. 0 22
      client/components/Icon/Lock.tsx
  32. 0 21
      client/components/Icon/Login.tsx
  33. 0 21
      client/components/Icon/PieChart.tsx
  34. 0 23
      client/components/Icon/Plus.tsx
  35. 0 19
      client/components/Icon/QRCode.tsx
  36. 0 24
      client/components/Icon/Refresh.tsx
  37. 0 18
      client/components/Icon/Send.tsx
  38. 0 21
      client/components/Icon/Shuffle.tsx
  39. 0 24
      client/components/Icon/Signup.tsx
  40. 0 43
      client/components/Icon/Spinner.tsx
  41. 0 22
      client/components/Icon/Stop.tsx
  42. 0 25
      client/components/Icon/Trash.tsx
  43. 0 22
      client/components/Icon/X.tsx
  44. 0 22
      client/components/Icon/Zap.tsx
  45. 0 1
      client/components/Icon/index.ts
  46. 0 257
      client/components/Input.tsx
  47. 0 38
      client/components/Layout.tsx
  48. 0 761
      client/components/LinksTable.tsx
  49. 0 58
      client/components/Modal.tsx
  50. 0 76
      client/components/NeedToLogin.tsx
  51. 0 19
      client/components/PageLoading.tsx
  52. 0 24
      client/components/ReCaptcha.tsx
  53. 0 108
      client/components/Settings/SettingsApi.tsx
  54. 0 99
      client/components/Settings/SettingsChangeEmail.tsx
  55. 0 122
      client/components/Settings/SettingsDeleteAccount.tsx
  56. 0 204
      client/components/Settings/SettingsDomain.tsx
  57. 0 89
      client/components/Settings/SettingsPassword.tsx
  58. 0 380
      client/components/Shortener.tsx
  59. 0 84
      client/components/Table.ts
  60. 0 67
      client/components/Text.tsx
  61. 0 14
      client/components/Tooltip.tsx
  62. 0 62
      client/consts/consts.ts
  63. 0 1
      client/consts/index.ts
  64. 0 30
      client/helpers/animations.ts
  65. 0 11
      client/helpers/recaptcha.js
  66. 0 31
      client/hooks.ts
  67. 0 19
      client/module.d.ts
  68. 0 5
      client/next-env.d.ts
  69. 0 81
      client/pages/_app.tsx
  70. 0 110
      client/pages/_document.tsx
  71. 0 37
      client/pages/banned.tsx
  72. 0 38
      client/pages/index.tsx
  73. 0 185
      client/pages/login.tsx
  74. 0 19
      client/pages/logout.tsx
  75. 0 98
      client/pages/protected/[id].tsx
  76. 0 84
      client/pages/report.tsx
  77. 0 110
      client/pages/reset-password.tsx
  78. 0 45
      client/pages/settings.tsx
  79. 0 207
      client/pages/stats.tsx
  80. 0 66
      client/pages/terms.tsx
  81. 0 32
      client/pages/url-info.tsx
  82. 0 55
      client/pages/verify-email.tsx
  83. 0 84
      client/pages/verify.tsx
  84. 0 51
      client/store/auth.ts
  85. 0 1
      client/store/index.ts
  86. 0 142
      client/store/links.ts
  87. 0 17
      client/store/loading.ts
  88. 0 85
      client/store/settings.ts
  89. 0 35
      client/store/store.ts
  90. 0 31
      client/tsconfig.json
  91. 0 8
      client/types.ts
  92. 0 23
      client/utils.ts
  93. 0 631
      docs/api/api.ts
  94. 0 48
      docs/api/generate.ts
  95. 0 160
      global.d.ts
  96. 0 5
      jest-setup.ts
  97. 0 13
      next.config.js
  98. 999 4410
      package-lock.json
  99. 56 110
      package.json
  100. 14 0
      server/cron.js

+ 1 - 0
.gitignore

@@ -12,3 +12,4 @@ production-server
 dump.rdb
 docs/api/*.js
 docs/api/static
+**/.DS_Store

+ 0 - 7
.travis.yml

@@ -1,7 +0,0 @@
-language: node_js
-
-node_js:
-  - "12"
-
-script:
-  - yarn run lint:nofix

+ 1 - 1
Dockerfile

@@ -1,4 +1,4 @@
-FROM node:12-alpine
+FROM node:18-alpine
 
 RUN apk add --update bash
 

+ 0 - 51
client/components/ALink.tsx

@@ -1,51 +0,0 @@
-import { FC } from "react";
-import { Box, BoxProps } from "rebass/styled-components";
-import styled, { css } from "styled-components";
-import { ifProp } from "styled-tools";
-import Link from "next/link";
-
-interface Props extends BoxProps {
-  href?: string;
-  title?: string;
-  target?: string;
-  rel?: string;
-  forButton?: boolean;
-  isNextLink?: boolean;
-}
-const StyledBox = styled(Box)<Props>`
-  cursor: pointer;
-  color: #2196f3;
-  border-bottom: 1px dotted transparent;
-  text-decoration: none;
-  transition: all 0.2s ease-out;
-
-  ${ifProp(
-    { forButton: false },
-    css`
-      :hover {
-        border-bottom-color: #2196f3;
-      }
-    `
-  )}
-`;
-
-export const ALink: FC<Props> = (props) => {
-  if (props.isNextLink) {
-    const { href, target, title, rel, ...rest } = props;
-    return (
-      <Link href={href} target={target} title={title} rel={rel} passHref>
-        <StyledBox as="a" {...rest} />
-      </Link>
-    );
-  }
-  return <StyledBox as="a" {...props} />;
-};
-
-ALink.displayName = "ALink";
-
-ALink.defaultProps = {
-  pb: "1px",
-  forButton: false
-};
-
-export default ALink;

+ 0 - 17
client/components/Animation.ts

@@ -1,17 +0,0 @@
-import { fadeInVertical } from "../helpers/animations";
-import { Flex } from "rebass/styled-components";
-import styled from "styled-components";
-import { prop } from "styled-tools";
-import { FC } from "react";
-
-interface Props extends React.ComponentProps<typeof Flex> {
-  offset: string;
-  duration?: string;
-}
-
-const Animation: FC<Props> = styled(Flex)<Props>`
-  animation: ${(props) => fadeInVertical(props.offset)}
-    ${prop("duration", "0.3s")} ease-out;
-`;
-
-export default Animation;

+ 0 - 54
client/components/AppWrapper.tsx

@@ -1,54 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import React, { useEffect } from "react";
-import styled from "styled-components";
-
-import { useStoreState, useStoreActions } from "../store";
-import PageLoading from "./PageLoading";
-import Header from "./Header";
-
-const Wrapper = styled(Flex)`
-  input {
-    filter: none;
-  }
-
-  * {
-    box-sizing: border-box;
-  }
-
-  *::-moz-focus-inner {
-    border: none;
-  }
-`;
-
-const AppWrapper = ({ children }: { children: any }) => {
-  const isAuthenticated = useStoreState((s) => s.auth.isAuthenticated);
-  const logout = useStoreActions((s) => s.auth.logout);
-  const fetched = useStoreState((s) => s.settings.fetched);
-  const loading = useStoreState((s) => s.loading.loading);
-  const getSettings = useStoreActions((s) => s.settings.getSettings);
-
-  const isVerifyEmailPage =
-    typeof window !== "undefined" &&
-    window.location.pathname.includes("verify-email");
-
-  useEffect(() => {
-    if (isAuthenticated && !fetched && !isVerifyEmailPage) {
-      getSettings().catch(() => logout());
-    }
-  }, [isAuthenticated, fetched, isVerifyEmailPage, getSettings, logout]);
-
-  return (
-    <Wrapper
-      minHeight="100vh"
-      width={1}
-      flex="0 0 auto"
-      alignItems="center"
-      flexDirection="column"
-    >
-      <Header />
-      {loading ? <PageLoading /> : children}
-    </Wrapper>
-  );
-};
-
-export default AppWrapper;

+ 0 - 118
client/components/Button.tsx

@@ -1,118 +0,0 @@
-import styled, { css } from "styled-components";
-import { switchProp, prop, ifProp } from "styled-tools";
-import { Flex, BoxProps } from "rebass/styled-components";
-
-interface Props extends BoxProps {
-  color?: "purple" | "gray" | "blue" | "red";
-  disabled?: boolean;
-  icon?: string; // TODO: better typing
-  isRound?: boolean;
-  onClick?: any; // TODO: better typing
-  type?: "button" | "submit" | "reset";
-}
-
-export const Button = styled(Flex)<Props>`
-  position: relative;
-  align-items: center;
-  justify-content: center;
-  font-weight: normal;
-  text-align: center;
-  line-height: 1;
-  word-break: keep-all;
-  color: ${switchProp(prop("color", "blue"), {
-    blue: "white",
-    red: "white",
-    purple: "white",
-    gray: "#444"
-  })};
-  background: ${switchProp(prop("color", "blue"), {
-    blue: "linear-gradient(to right, #42a5f5, #2979ff)",
-    red: "linear-gradient(to right, #ee3b3b, #e11c1c)",
-    purple: "linear-gradient(to right, #7e57c2, #6200ea)",
-    gray: "linear-gradient(to right, #e0e0e0, #bdbdbd)"
-  })};
-  box-shadow: ${switchProp(prop("color", "blue"), {
-    blue: "0 5px 6px rgba(66, 165, 245, 0.5)",
-    red: "0 5px 6px rgba(168, 45, 45, 0.5)",
-    purple: "0 5px 6px rgba(81, 45, 168, 0.5)",
-    gray: "0 5px 6px rgba(160, 160, 160, 0.5)"
-  })};
-  border: none;
-  border-radius: 100px;
-  transition: all 0.4s ease-out;
-  cursor: pointer;
-  overflow: hidden;
-
-  :hover,
-  :focus {
-    outline: none;
-    box-shadow: ${switchProp(prop("color", "blue"), {
-      blue: "0 6px 15px rgba(66, 165, 245, 0.5)",
-      red: "0 6px 15px rgba(168, 45, 45, 0.5)",
-      purple: "0 6px 15px rgba(81, 45, 168, 0.5)",
-      gray: "0 6px 15px rgba(160, 160, 160, 0.5)"
-    })};
-    transform: translateY(-2px) scale(1.02, 1.02);
-  }
-`;
-
-Button.defaultProps = {
-  as: "button",
-  width: "auto",
-  flex: "0 0 auto",
-  height: [36, 40],
-  py: 0,
-  px: [24, 32],
-  fontSize: [12, 13],
-  color: "blue",
-  icon: "",
-  isRound: false
-};
-
-interface NavButtonProps extends BoxProps {
-  disabled?: boolean;
-  onClick?: any; // TODO: better typing
-  type?: "button" | "submit" | "reset";
-  key?: string;
-}
-
-export const NavButton = styled(Flex)<NavButtonProps>`
-  display: flex;
-  border: none;
-  border-radius: 4px;
-  box-shadow: 0 0px 10px rgba(100, 100, 100, 0.1);
-  background-color: white;
-  cursor: pointer;
-  transition: all 0.2s ease-out;
-  box-sizing: border-box;
-
-  :hover {
-    transform: translateY(-2px);
-  }
-
-  ${ifProp(
-    "disabled",
-    css`
-      background-color: #f6f6f6;
-      box-shadow: 0 0px 5px rgba(150, 150, 150, 0.1);
-      cursor: default;
-
-      :hover {
-        transform: none;
-      }
-    `
-  )}
-`;
-
-NavButton.defaultProps = {
-  as: "button",
-  type: "button",
-  flex: "0 0 auto",
-  alignItems: "center",
-  justifyContent: "center",
-  width: "auto",
-  height: [26, 28],
-  py: 0,
-  px: ["6px", "8px"],
-  fontSize: [12]
-};

+ 0 - 80
client/components/Charts/Area.tsx

@@ -1,80 +0,0 @@
-import subMonths from "date-fns/subMonths";
-import subHours from "date-fns/subHours";
-import formatDate from "date-fns/format";
-import subDays from "date-fns/subDays";
-import React, { FC } from "react";
-import {
-  AreaChart,
-  Area,
-  XAxis,
-  YAxis,
-  CartesianGrid,
-  ResponsiveContainer,
-  Tooltip
-} from "recharts";
-
-interface Props {
-  data: number[];
-  period: string;
-}
-
-const ChartArea: FC<Props> = ({ data: rawData, period }) => {
-  const now = new Date();
-  const getDate = index => {
-    switch (period) {
-      case "allTime":
-        return formatDate(
-          subMonths(now, rawData.length - index - 1),
-          "MMM yyy"
-        );
-      case "lastDay":
-        return formatDate(subHours(now, rawData.length - index - 1), "HH:00");
-      case "lastMonth":
-      case "lastWeek":
-      default:
-        return formatDate(subDays(now, rawData.length - index - 1), "MMM dd");
-    }
-  };
-  const data = rawData.map((view, index) => ({
-    name: getDate(index),
-    views: view
-  }));
-
-  return (
-    <ResponsiveContainer
-      width="100%"
-      height={window.innerWidth < 468 ? 240 : 320}
-    >
-      <AreaChart
-        data={data}
-        margin={{
-          top: 16,
-          right: 0,
-          left: 0,
-          bottom: 16
-        }}
-      >
-        <defs>
-          <linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
-            <stop offset="5%" stopColor="#B39DDB" stopOpacity={0.8} />
-            <stop offset="95%" stopColor="#B39DDB" stopOpacity={0} />
-          </linearGradient>
-        </defs>
-        <XAxis dataKey="name" />
-        <YAxis />
-        <CartesianGrid strokeDasharray="1 1" />
-        <Tooltip />
-        <Area
-          type="monotone"
-          dataKey="views"
-          isAnimationActive={false}
-          stroke="#B39DDB"
-          fillOpacity={1}
-          fill="url(#colorUv)"
-        />
-      </AreaChart>
-    </ResponsiveContainer>
-  );
-};
-
-export default ChartArea;

+ 0 - 40
client/components/Charts/Bar.tsx

@@ -1,40 +0,0 @@
-import React, { FC } from "react";
-import {
-  BarChart,
-  Bar,
-  XAxis,
-  YAxis,
-  CartesianGrid,
-  Tooltip,
-  ResponsiveContainer
-} from "recharts";
-
-interface Props {
-  data: any[]; // TODO: types
-}
-
-const ChartBar: FC<Props> = ({ data }) => (
-  <ResponsiveContainer
-    width="100%"
-    height={window.innerWidth < 468 ? 240 : 320}
-  >
-    <BarChart
-      data={data}
-      layout="vertical"
-      margin={{
-        top: 0,
-        right: 0,
-        left: 24,
-        bottom: 0
-      }}
-    >
-      <XAxis type="number" dataKey="value" />
-      <YAxis type="category" dataKey="name" />
-      <CartesianGrid strokeDasharray="1 1" />
-      <Tooltip />
-      <Bar dataKey="value" fill="#B39DDB" />
-    </BarChart>
-  </ResponsiveContainer>
-);
-
-export default ChartBar;

+ 0 - 83
client/components/Charts/Map.tsx

@@ -1,83 +0,0 @@
-import styled from "styled-components";
-import React, { FC } from "react";
-// import { VectorMap } from "@south-paw/react-vector-maps";
-
-import { Colors } from "../../consts";
-import Tooltip from "../Tooltip";
-import world from "./world.json";
-
-const Svg = styled.svg`
-  path {
-    fill: ${Colors.Map0};
-    stroke: #fff;
-  }
-
-  path.country-6 {
-    fill: ${Colors.Map06};
-    stroke: #fff;
-  }
-  path.country-5 {
-    fill: ${Colors.Map05};
-    stroke: #fff;
-  }
-  path.country-4 {
-    fill: ${Colors.Map04};
-    stroke: #fff;
-  }
-  path.country-3 {
-    fill: ${Colors.Map03};
-    stroke: #fff;
-  }
-  path.country-2 {
-    fill: ${Colors.Map02};
-    stroke: #fff;
-  }
-  path.country-1 {
-    fill: ${Colors.Map01};
-    stroke: #fff;
-  }
-`;
-
-interface Props {
-  data: Array<{ name: string; value: number }>;
-}
-
-const Map: FC<Props> = ({ data }) => {
-  const [mostVisits] = data.sort((a, b) => (b.value - a.value > 0 ? 1 : -1));
-  return (
-    <>
-      {world.layers.map(layer => (
-        <Tooltip
-          key={layer.id}
-          type="light"
-          effect="float"
-          id={`${layer.id}-tooltip-country`}
-        >
-          {layer.name}:{" "}
-          {data.find(d => d.name.toLowerCase() === layer.id)?.value || 0}
-        </Tooltip>
-      ))}
-      <Svg
-        xmlns="http://www.w3.org/2000/svg"
-        aria-label="world map"
-        viewBox={world.viewBox}
-      >
-        {world.layers.map(layer => (
-          <path
-            key={layer.id}
-            data-tip
-            data-for={`${layer.id}-tooltip-country`}
-            className={`country-${Math.ceil(
-              ((data.find(d => d.name.toLowerCase() === layer.id)?.value || 0) /
-                mostVisits?.value || 0) * 6
-            )}`}
-            aria-label={layer.name}
-            d={layer.d}
-          />
-        ))}
-      </Svg>
-    </>
-  );
-};
-
-export default Map;

+ 0 - 33
client/components/Charts/Pie.tsx

@@ -1,33 +0,0 @@
-import { PieChart, Pie, Tooltip, ResponsiveContainer } from "recharts";
-import React, { FC } from "react";
-
-interface Props {
-  data: any[]; // TODO: types
-}
-
-const ChartPie: FC<Props> = ({ data }) => (
-  <ResponsiveContainer
-    width="100%"
-    height={window.innerWidth < 468 ? 240 : 320}
-  >
-    <PieChart
-      margin={{
-        top: window.innerWidth < 468 ? 56 : 0,
-        right: window.innerWidth < 468 ? 56 : 0,
-        bottom: window.innerWidth < 468 ? 56 : 0,
-        left: window.innerWidth < 468 ? 56 : 0
-      }}
-    >
-      <Pie
-        data={data}
-        dataKey="value"
-        innerRadius={window.innerWidth < 468 ? 20 : 80}
-        fill="#B39DDB"
-        label={({ name }) => name}
-      />
-      <Tooltip />
-    </PieChart>
-  </ResponsiveContainer>
-);
-
-export default ChartPie;

+ 0 - 4
client/components/Charts/index.tsx

@@ -1,4 +0,0 @@
-export { default as Area } from "./Area";
-export { default as Bar } from "./Bar";
-export { default as Pie } from "./Pie";
-export { default as Map } from "./Map";

ファイルの差分が大きいため隠しています
+ 0 - 48
client/components/Charts/world.json


+ 0 - 14
client/components/Divider.tsx

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

+ 0 - 116
client/components/Extensions.tsx

@@ -1,116 +0,0 @@
-import React from "react";
-import styled from "styled-components";
-import { Flex } from "rebass/styled-components";
-import SVG from "react-inlinesvg"; // TODO: another solution
-import { Colors } from "../consts";
-import { ColCenterH } from "./Layout";
-import { H3 } from "./Text";
-
-const Button = styled.button`
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  margin: 0 16px;
-  padding: 12px 28px;
-  font-family: "Nunito", sans-serif;
-  background-color: #eee;
-  border: 1px solid #aaa;
-  font-size: 14px;
-  font-weight: bold;
-  text-decoration: none;
-  border-radius: 4px;
-  outline: none;
-  box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
-  transition: transform 0.3s ease-out;
-  cursor: pointer;
-
-  @media only screen and (max-width: 768px) {
-    margin-bottom: 16px;
-    padding: 8px 16px;
-    font-size: 12px;
-  }
-
-  > * {
-    text-decoration: none;
-  }
-
-  :hover {
-    transform: translateY(-2px);
-  }
-`;
-
-const FirefoxButton = styled(Button)`
-  color: #e0890f;
-`;
-
-const ChromeButton = styled(Button)`
-  color: #4285f4;
-`;
-
-const Link = styled.a`
-  text-decoration: none;
-
-  :visited,
-  :hover,
-  :active,
-  :focus {
-    text-decoration: none;
-  }
-`;
-
-const Icon = styled(SVG)`
-  width: 18px;
-  height: 18px;
-  margin-right: 16px;
-  fill: ${(props) => props.color || "#333"};
-
-  @media only screen and (max-width: 768px) {
-    width: 13px;
-    height: 13px;
-    margin-right: 10px;
-  }
-`;
-
-const Extensions = () => (
-  <ColCenterH
-    width={1}
-    flex="0 0 auto"
-    flexWrap={["wrap", "wrap", "nowrap"]}
-    py={[64, 96]}
-    backgroundColor={Colors.ExtensionsBg}
-  >
-    <H3 fontSize={[26, 28]} mb={5} color="white" light>
-      Browser extensions.
-    </H3>
-    <Flex
-      width={1200}
-      maxWidth="100%"
-      flex="1 1 auto"
-      justifyContent="center"
-      flexWrap={["wrap", "wrap", "nowrap"]}
-    >
-      <Link
-        href="https://chrome.google.com/webstore/detail/kutt/pklakpjfiegjacoppcodencchehlfnpd"
-        target="_blank"
-        rel="noopener noreferrer"
-      >
-        <ChromeButton>
-          <Icon src="/images/googlechrome.svg" color="#4285f4" />
-          <span>Download for Chrome</span>
-        </ChromeButton>
-      </Link>
-      <Link
-        href="https://addons.mozilla.org/en-US/firefox/addon/kutt/"
-        target="_blank"
-        rel="noopener noreferrer"
-      >
-        <FirefoxButton>
-          <Icon src="/images/mozillafirefox.svg" color="#e0890f" />
-          <span>Download for Firefox</span>
-        </FirefoxButton>
-      </Link>
-    </Flex>
-  </ColCenterH>
-);
-
-export default Extensions;

+ 0 - 43
client/components/Features.tsx

@@ -1,43 +0,0 @@
-import React from "react";
-import { Flex } from "rebass/styled-components";
-
-import FeaturesItem from "./FeaturesItem";
-import { ColCenterH } from "./Layout";
-import { Colors } from "../consts";
-import { H3 } from "./Text";
-
-const Features = () => (
-  <ColCenterH
-    width={1}
-    flex="0 0 auto"
-    py={[64, 100]}
-    backgroundColor={Colors.FeaturesBg}
-  >
-    <H3 fontSize={[26, 28]} mb={72} light>
-      Kutting edge features.
-    </H3>
-    <Flex
-      width={1200}
-      maxWidth="100%"
-      flex="1 1 auto"
-      justifyContent="center"
-      flexWrap={["wrap", "wrap", "wrap", "nowrap"]}
-    >
-      <FeaturesItem title="Managing links" icon="edit">
-        Create, protect and delete your links and monitor them with detailed
-        statistics.
-      </FeaturesItem>
-      <FeaturesItem title="Custom domain" icon="shuffle">
-        Use custom domains for your links. Add or remove them for free.
-      </FeaturesItem>
-      <FeaturesItem title="API" icon="zap">
-        Use the provided API to create, delete, and get URLs from anywhere.
-      </FeaturesItem>
-      <FeaturesItem title="Free &amp; open source" icon="heart">
-        Completely open source and free. You can host it on your own server.
-      </FeaturesItem>
-    </Flex>
-  </ColCenterH>
-);
-
-export default Features;

+ 0 - 71
client/components/FeaturesItem.tsx

@@ -1,71 +0,0 @@
-import React, { FC, ReactNode } from "react";
-import styled from "styled-components";
-import { Flex } from "rebass/styled-components";
-
-import { fadeIn } from "../helpers/animations";
-import Icon from "./Icon";
-import { Icons } from "./Icon/Icon";
-
-interface Props {
-  title: string;
-  icon: Icons;
-  children?: ReactNode;
-}
-
-const Block = styled(Flex).attrs({
-  maxWidth: ["100%", "100%", "50%", "25%"],
-  flexDirection: "column",
-  alignItems: "center",
-  p: "0 24px",
-  mb: [48, 48, 48, 0]
-})`
-  animation: ${fadeIn} 0.8s ease-out;
-
-  :last-child {
-    margin-right: 0;
-  }
-`;
-
-const IconBox = styled(Flex).attrs({
-  width: [40, 40, 48],
-  height: [40, 40, 48],
-  alignItems: "center",
-  justifyContent: "center"
-})`
-  border-radius: 100%;
-  box-sizing: border-box;
-  background-color: #2196f3;
-`;
-
-const Title = styled.h3`
-  margin: 16px;
-  font-size: 15px;
-
-  @media only screen and (max-width: 448px) {
-    margin: 12px;
-    font-size: 14px;
-  }
-`;
-
-const Description = styled.p`
-  margin: 0;
-  font-size: 14px;
-  font-weight: 300;
-  text-align: center;
-
-  @media only screen and (max-width: 448px) {
-    font-size: 13px;
-  }
-`;
-
-const FeaturesItem: FC<Props> = ({ children, icon, title }) => (
-  <Block>
-    <IconBox>
-      <Icon name={icon} stroke="white" strokeWidth="2" />
-    </IconBox>
-    <Title>{title}</Title>
-    <Description>{children}</Description>
-  </Block>
-);
-
-export default FeaturesItem;

+ 0 - 66
client/components/Footer.tsx

@@ -1,66 +0,0 @@
-import React, { FC, useEffect } from "react";
-import getConfig from "next/config";
-
-import showRecaptcha from "../helpers/recaptcha";
-import { useStoreState } from "../store";
-import { ColCenter } from "./Layout";
-import ReCaptcha from "./ReCaptcha";
-import ALink from "./ALink";
-import Text from "./Text";
-
-const { publicRuntimeConfig } = getConfig();
-
-const Footer: FC = () => {
-  const { isAuthenticated } = useStoreState((s) => s.auth);
-
-  useEffect(() => {
-    showRecaptcha();
-  }, []);
-
-  return (
-    <ColCenter
-      as="footer"
-      width={1}
-      backgroundColor="white"
-      p={isAuthenticated ? 2 : 24}
-    >
-      {!isAuthenticated && <ReCaptcha />}
-      <Text fontSize={[12, 13]} py={2}>
-        Made with love by{" "}
-        <ALink href="//thedevs.network/" title="The Devs" target="_blank">
-          The Devs
-        </ALink>
-        .{" | "}
-        <ALink
-          href="https://github.com/thedevs-network/kutt"
-          title="GitHub"
-          target="_blank"
-        >
-          GitHub
-        </ALink>
-        {" | "}
-        <ALink href="/terms" title="Terms of Service" isNextLink>
-          Terms of Service
-        </ALink>
-        {" | "}
-        <ALink href="/report" title="Report abuse" isNextLink>
-          Report Abuse
-        </ALink>
-        {publicRuntimeConfig.CONTACT_EMAIL && (
-          <>
-            {" | "}
-            <ALink
-              href={`mailto:${publicRuntimeConfig.CONTACT_EMAIL}`}
-              title="Contact us"
-            >
-              Contact us
-            </ALink>
-          </>
-        )}
-        .
-      </Text>
-    </ColCenter>
-  );
-};
-
-export default Footer;

+ 0 - 184
client/components/Header.tsx

@@ -1,184 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import getConfig from "next/config";
-import React, { FC } from "react";
-import Router from "next/router";
-import useMedia from "use-media";
-import Image from "next/image";
-
-import { DISALLOW_REGISTRATION } from "../consts";
-import { useStoreState } from "../store";
-import styled from "styled-components";
-import { RowCenterV } from "./Layout";
-import { Button } from "./Button";
-import ALink from "./ALink";
-
-const { publicRuntimeConfig } = getConfig();
-
-const Li = styled(Flex).attrs({ ml: [12, 24, 32] })`
-  a {
-    color: inherit;
-
-    :hover {
-      color: #2196f3;
-    }
-  }
-`;
-
-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;
-    padding: 0;
-  }
-
-  @media only screen and (max-width: 488px) {
-    a {
-      font-size: 18px;
-    }
-  }
-
-  span {
-    margin-right: 10px !important;
-  }
-`;
-
-const Header: FC = () => {
-  const { isAuthenticated } = useStoreState((s) => s.auth);
-  const isMobile = useMedia({ maxWidth: 640 });
-
-  const login = !isAuthenticated && (
-    <Li>
-      <ALink
-        href="/login"
-        title={!DISALLOW_REGISTRATION ? "login / signup" : "login"}
-        forButton
-        isNextLink
-      >
-        <Button height={[32, 40]}>
-          {!DISALLOW_REGISTRATION ? "Log in / Sign up" : "Log in"}
-        </Button>
-      </ALink>
-    </Li>
-  );
-  const logout = isAuthenticated && (
-    <Li>
-      <ALink href="/logout" title="logout" fontSize={[14, 16]} isNextLink>
-        Log out
-      </ALink>
-    </Li>
-  );
-  const settings = isAuthenticated && (
-    <Li>
-      <ALink href="/settings" title="Settings" forButton isNextLink>
-        <Button height={[32, 40]}>Settings</Button>
-      </ALink>
-    </Li>
-  );
-
-  return (
-    <Flex
-      width={1232}
-      maxWidth="100%"
-      p={[16, "0 32px"]}
-      mb={[32, 0]}
-      height={["auto", "auto", 102]}
-      justifyContent="space-between"
-      alignItems={["flex-start", "center"]}
-    >
-      <Flex
-        flexDirection={["column", "row"]}
-        alignItems={["flex-start", "stretch"]}
-      >
-        <LogoImage>
-          <ALink
-            href="/"
-            title="Homepage"
-            onClick={(e) => {
-              e.preventDefault();
-              if (window.location.pathname !== "/") Router.push("/");
-            }}
-            forButton
-            isNextLink
-          >
-            <Image
-              src="/images/logo.svg"
-              alt="kutt logo"
-              width={18}
-              height={24}
-            />
-            {publicRuntimeConfig.SITE_NAME}
-          </ALink>
-        </LogoImage>
-
-        {!isMobile && (
-          <Flex
-            style={{ listStyle: "none" }}
-            display={["none", "flex"]}
-            alignItems="flex-end"
-            as="ul"
-            m={0}
-            px={0}
-            pt={0}
-            pb="2px"
-          >
-            <Li>
-              <ALink
-                href="//github.com/thedevs-network/kutt"
-                target="_blank"
-                rel="noopener noreferrer"
-                title="GitHub"
-                fontSize={[14, 16]}
-              >
-                GitHub
-              </ALink>
-            </Li>
-            <Li>
-              <ALink
-                href="/report"
-                title="Report abuse"
-                fontSize={[14, 16]}
-                isNextLink
-              >
-                Report
-              </ALink>
-            </Li>
-          </Flex>
-        )}
-      </Flex>
-      <RowCenterV
-        m={0}
-        p={0}
-        justifyContent="flex-end"
-        as="ul"
-        style={{ listStyle: "none" }}
-      >
-        {isMobile && (
-          <Li>
-            <Flex>
-              <ALink
-                href="/report"
-                title="Report"
-                fontSize={[14, 16]}
-                isNextLink
-              >
-                Report
-              </ALink>
-            </Flex>
-          </Li>
-        )}
-        {logout}
-        {settings}
-        {login}
-      </RowCenterV>
-    </Flex>
-  );
-};
-
-export default Header;

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

@@ -1,21 +0,0 @@
-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);

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

@@ -1,21 +0,0 @@
-import React from "react";
-
-function Check() {
-  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 6L9 17 4 12"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Check);

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function ChevronLeft() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-chevron-left"
-      viewBox="0 0 24 24"
-    >
-      <path d="M15 18L9 12 15 6"></path>
-    </svg>
-  );
-}
-
-export default React.memo(ChevronLeft);

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function ChevronRight() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-chevron-right"
-      viewBox="0 0 24 24"
-    >
-      <path d="M9 18L15 12 9 6"></path>
-    </svg>
-  );
-}
-
-export default React.memo(ChevronRight);

+ 0 - 23
client/components/Icon/Clipboard.tsx

@@ -1,23 +0,0 @@
-import React from "react";
-
-function Clipboard() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="auto"
-      height="auto"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-clipboard"
-      viewBox="0 0 24 24"
-    >
-      <path d="M16 4h2a2 2 0 012 2v14a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2h2"></path>
-      <rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
-    </svg>
-  );
-}
-
-export default React.memo(Clipboard);

+ 0 - 23
client/components/Icon/Copy.tsx

@@ -1,23 +0,0 @@
-import React from "react";
-
-function Copy() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-copy"
-      viewBox="0 0 24 24"
-    >
-      <rect width="13" height="13" x="9" y="9" rx="2" ry="2"></rect>
-      <path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Copy);

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

@@ -1,22 +0,0 @@
-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);

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

@@ -1,21 +0,0 @@
-import React from "react";
-
-function EditAlt() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="48"
-      height="48"
-      fill="none"
-      stroke="#5c666b"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      viewBox="0 0 24 24"
-    >
-      <path d="M16 3L21 8 8 21 3 21 3 16 16 3z"></path>
-    </svg>
-  );
-}
-
-export default React.memo(EditAlt);

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

@@ -1,21 +0,0 @@
-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);

+ 0 - 155
client/components/Icon/Icon.tsx

@@ -1,155 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import styled, { css } from "styled-components";
-import { prop, ifProp } from "styled-tools";
-import React, { FC } from "react";
-
-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 EditAlt from "./EditAlt";
-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 Stop from "./Stop";
-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";
-import X from "./X";
-
-const icons = {
-  arrowLeft: ArrowLeft,
-  check: Check,
-  chevronLeft: ChevronLeft,
-  chevronRight: ChevronRight,
-  clipboard: Clipboard,
-  copy: Copy,
-  edit: Edit,
-  editAlt: EditAlt,
-  heart: Heart,
-  key: Key,
-  lock: Lock,
-  login: Login,
-  pieChart: PieChart,
-  plus: Plus,
-  qrcode: QRCode,
-  refresh: Refresh,
-  send: Send,
-  shuffle: Shuffle,
-  signup: Signup,
-  spinner: Spinner,
-  stop: Stop,
-  trash: Trash,
-  x: X,
-  zap: Zap
-};
-
-export type Icons = keyof typeof icons;
-
-interface Props extends React.ComponentProps<typeof Flex> {
-  name: Icons;
-  stroke?: string;
-  fill?: string;
-  hoverFill?: string;
-  hoverStroke?: string;
-  strokeWidth?: string;
-}
-
-const CustomIcon: FC<React.ComponentProps<typeof Flex>> = styled(Flex)`
-  position: relative;
-
-  svg {
-    transition: all 0.2s ease-out;
-    width: 100%;
-    height: 100%;
-
-    ${ifProp(
-      "fill",
-      css`
-        fill: ${prop("fill")};
-      `
-    )}
-
-    ${ifProp(
-      "stroke",
-      css`
-        stroke: ${prop("stroke")};
-      `
-    )}
-
-    ${ifProp(
-      "strokeWidth",
-      css`
-        stroke-width: ${prop("strokeWidth")};
-      `
-    )}
-  }
-
-  ${ifProp(
-    "hoverFill",
-    css`
-      :hover {
-        svg {
-          fill: ${prop("hoverFill")};
-        }
-      }
-    `
-  )}
-
-  ${ifProp(
-    "hoverStroke",
-    css`
-      :hover {
-        svg {
-          stroke: ${prop("stroke")};
-        }
-      }
-    `
-  )}
-
-  ${ifProp(
-    { as: "button" },
-    css`
-      border: none;
-      outline: none;
-      transition: transform 0.4s ease-out;
-      border-radius: 100%;
-      background-color: none !important;
-      cursor: pointer;
-      box-sizing: border-box;
-      box-shadow: 0 2px 1px ${Colors.IconShadow};
-
-      :hover,
-      :focus {
-        transform: translateY(-2px) scale(1.02, 1.02);
-      }
-      :focus {
-        outline: 3px solid rgba(65, 164, 245, 0.5);
-      }
-    `
-  )}
-`;
-
-const Icon: FC<Props> = ({ name, ...rest }) => (
-  <CustomIcon {...rest}>{React.createElement(icons[name])}</CustomIcon>
-);
-
-Icon.defaultProps = {
-  size: 16,
-  alignItems: "center",
-  justifyContent: "center"
-};
-
-export default Icon;

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function Key() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-key"
-      viewBox="0 0 24 24"
-    >
-      <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 11-7.778 7.778 5.5 5.5 0 017.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Key);

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function Lock() {
-  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"
-    >
-      <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect>
-      <path d="M7 11V7a5 5 0 0110 0v4"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Lock);

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

@@ -1,21 +0,0 @@
-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);

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

@@ -1,21 +0,0 @@
-import React from "react";
-
-function Icon() {
-  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="M21.21 15.89A10 10 0 118 2.83M22 12A10 10 0 0012 2v10z"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Icon);

+ 0 - 23
client/components/Icon/Plus.tsx

@@ -1,23 +0,0 @@
-import React from "react";
-
-function Plus() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-plus"
-      viewBox="0 0 24 24"
-    >
-      <path d="M12 5L12 19"></path>
-      <path d="M5 12L19 12"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Plus);

+ 0 - 19
client/components/Icon/QRCode.tsx

@@ -1,19 +0,0 @@
-import React from "react";
-
-function QRCOde() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="32"
-      height="32"
-      fill="currentColor"
-      className="jam jam-qr-code"
-      preserveAspectRatio="xMinYMin"
-      viewBox="-2 -2 24 24"
-    >
-      <path d="M13 18h3a2 2 0 002-2v-3a1 1 0 012 0v3a4 4 0 01-4 4H4a4 4 0 01-4-4v-3a1 1 0 012 0v3a2 2 0 002 2h3a1 1 0 010 2h6a1 1 0 010-2zM2 7a1 1 0 11-2 0V4a4 4 0 014-4h3a1 1 0 110 2H4a2 2 0 00-2 2v3zm16 0V4a2 2 0 00-2-2h-3a1 1 0 010-2h3a4 4 0 014 4v3a1 1 0 01-2 0z"></path>
-    </svg>
-  );
-}
-
-export default React.memo(QRCOde);

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

@@ -1,24 +0,0 @@
-import React from "react";
-
-function Refresh() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-refresh-ccw"
-      viewBox="0 0 24 24"
-    >
-      <path d="M1 4L1 10 7 10"></path>
-      <path d="M23 20L23 14 17 14"></path>
-      <path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Refresh);

+ 0 - 18
client/components/Icon/Send.tsx

@@ -1,18 +0,0 @@
-import React from "react";
-
-function Send() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="32"
-      height="32"
-      fill="currentColor"
-      version="1.1"
-      viewBox="0 0 24 24"
-    >
-      <path d="M2 21l21-9L2 3v7l15 2-15 2v7z"></path>
-    </svg>
-  );
-}
-
-export default Send;

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

@@ -1,21 +0,0 @@
-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);

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

@@ -1,24 +0,0 @@
-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);

+ 0 - 43
client/components/Icon/Spinner.tsx

@@ -1,43 +0,0 @@
-import styled, { keyframes } from "styled-components";
-import React from "react";
-
-const spin = keyframes`
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-`;
-
-const Svg = styled.svg`
-  animation: ${spin} 1s linear infinite;
-`;
-
-function Spinner() {
-  return (
-    <Svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-loader"
-      viewBox="0 0 24 24"
-    >
-      <path d="M12 2L12 6"></path>
-      <path d="M12 18L12 22"></path>
-      <path d="M4.93 4.93L7.76 7.76"></path>
-      <path d="M16.24 16.24L19.07 19.07"></path>
-      <path d="M2 12L6 12"></path>
-      <path d="M18 12L22 12"></path>
-      <path d="M4.93 19.07L7.76 16.24"></path>
-      <path d="M16.24 7.76L19.07 4.93"></path>
-    </Svg>
-  );
-}
-
-export default React.memo(Spinner);

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function Stop() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="48"
-      height="48"
-      fill="none"
-      stroke="#5c666b"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      viewBox="0 0 24 24"
-    >
-      <circle cx="12" cy="12" r="10"></circle>
-      <path d="M4.93 4.93L19.07 19.07"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Stop);

+ 0 - 25
client/components/Icon/Trash.tsx

@@ -1,25 +0,0 @@
-import React from "react";
-
-function Trash() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-trash-2"
-      viewBox="0 0 24 24"
-    >
-      <path d="M3 6L5 6 21 6"></path>
-      <path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"></path>
-      <path d="M10 11L10 17"></path>
-      <path d="M14 11L14 17"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Trash);

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

@@ -1,22 +0,0 @@
-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);

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

@@ -1,22 +0,0 @@
-import React from "react";
-
-function Zap() {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      fill="none"
-      stroke="currentColor"
-      strokeLinecap="round"
-      strokeLinejoin="round"
-      strokeWidth="2"
-      className="feather feather-zap"
-      viewBox="0 0 24 24"
-    >
-      <path d="M13 2L3 14 12 14 11 22 21 10 12 10 13 2z"></path>
-    </svg>
-  );
-}
-
-export default React.memo(Zap);

+ 0 - 1
client/components/Icon/index.ts

@@ -1 +0,0 @@
-export { default } from "./Icon";

+ 0 - 257
client/components/Input.tsx

@@ -1,257 +0,0 @@
-import React from "react";
-import { Flex, BoxProps } from "rebass/styled-components";
-import styled, { css, keyframes } from "styled-components";
-import { withProp, prop, ifProp } from "styled-tools";
-import { FC } from "react";
-
-import { Span } from "./Text";
-
-interface StyledTextProps extends BoxProps {
-  autoFocus?: boolean;
-  name?: string;
-  id?: string;
-  type?: string;
-  value?: string;
-  required?: boolean;
-  onChange?: any;
-  placeholderSize?: number[];
-  br?: string;
-  bbw?: string;
-  autocomplete?: "on" | "off";
-}
-
-export const TextInput = styled(Flex).attrs({
-  as: "input"
-})<StyledTextProps>`
-  position: relative;
-  box-sizing: border-box;
-  letter-spacing: 0.05em;
-  color: #444;
-  background-color: white;
-  box-shadow: 0 10px 35px hsla(200, 15%, 70%, 0.2);
-  border: none;
-  border-radius: ${prop("br", "100px")};
-  border-bottom: 5px solid #f5f5f5;
-  border-bottom-width: ${prop("bbw", "5px")};
-  transition: all 0.5s ease-out;
-
-  :focus {
-    outline: none;
-    box-shadow: 0 20px 35px hsla(200, 15%, 70%, 0.4);
-  }
-
-  ::placeholder {
-    font-size: ${withProp("placeholderSize", (s) => s[0] || 14)}px;
-    letter-spacing: 0.05em;
-    color: #888;
-  }
-
-  @media screen and (min-width: 64em) {
-    ::placeholder {
-      font-size: ${withProp(
-        "placeholderSize",
-        (s) => s[3] || s[2] || s[1] || s[0] || 16
-      )}px;
-    }
-  }
-
-  @media screen and (min-width: 52em) {
-    letter-spacing: 0.1em;
-    border-bottom-width: ${prop("bbw", "6px")};
-    ::placeholder {
-      font-size: ${withProp(
-        "placeholderSize",
-        (s) => s[2] || s[1] || s[0] || 15
-      )}px;
-    }
-  }
-
-  @media screen and (min-width: 40em) {
-    ::placeholder {
-      font-size: ${withProp("placeholderSize", (s) => s[1] || s[0] || 15)}px;
-    }
-  }
-`;
-
-TextInput.defaultProps = {
-  value: "",
-  height: [40, 44],
-  py: 0,
-  px: [3, 24],
-  fontSize: [14, 15],
-  placeholderSize: [13, 14]
-};
-
-interface StyledSelectProps extends BoxProps {
-  name?: string;
-  id?: string;
-  type?: string;
-  value?: string;
-  required?: boolean;
-  onChange?: any;
-  br?: string;
-  bbw?: string;
-}
-
-interface SelectOptions extends StyledSelectProps {
-  options: Array<{ key: string; value: string | number }>;
-}
-
-const StyledSelect: FC<StyledSelectProps> = styled(Flex).attrs({
-  as: "select"
-})<StyledSelectProps>`
-  position: relative;
-  box-sizing: border-box;
-  letter-spacing: 0.05em;
-  color: #444;
-  background-color: white;
-  box-shadow: 0 10px 35px hsla(200, 15%, 70%, 0.2);
-  border: none;
-  border-radius: ${prop("br", "100px")};
-  border-bottom: 5px solid #f5f5f5;
-  border-bottom-width: ${prop("bbw", "5px")};
-  transition: all 0.5s ease-out;
-  appearance: none;
-  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48' viewBox='0 0 24 24' fill='none' stroke='%235c666b' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
-  background-repeat: no-repeat, repeat;
-  background-position: right 1.2em top 50%, 0 0;
-  background-size: 1em auto, 100%;
-
-  :focus {
-    outline: none;
-    box-shadow: 0 20px 35px hsla(200, 15%, 70%, 0.4);
-  }
-
-  @media screen and (min-width: 52em) {
-    letter-spacing: 0.1em;
-    border-bottom-width: ${prop("bbw", "6px")};
-  }
-`;
-
-export const Select: FC<SelectOptions> = ({ options, ...props }) => (
-  <StyledSelect {...props}>
-    {options.map(({ key, value }) => (
-      <option key={value} value={value}>
-        {key}
-      </option>
-    ))}
-  </StyledSelect>
-);
-
-Select.defaultProps = {
-  value: "",
-  height: [40, 44],
-  py: 0,
-  px: [3, 24],
-  fontSize: [14, 15]
-};
-
-interface ChecknoxInputProps {
-  checked: boolean;
-  id?: string;
-  name: string;
-  onChange: any;
-}
-
-const CheckboxInput = styled(Flex).attrs({
-  as: "input",
-  type: "checkbox",
-  m: 0,
-  p: 0,
-  width: 0,
-  height: 0,
-  opacity: 0
-})<ChecknoxInputProps>`
-  position: relative;
-  opacity: 0;
-`;
-
-const CheckboxBox = styled(Flex).attrs({
-  alignItems: "center",
-  justifyContent: "center"
-})<{ checked: boolean }>`
-  position: relative;
-  transition: color 0.3s ease-out;
-  border-radius: 4px;
-  background-color: white;
-  box-shadow: 0 2px 4px rgba(50, 50, 50, 0.2);
-  cursor: pointer;
-
-  input:focus + & {
-    outline: 3px solid rgba(65, 164, 245, 0.5);
-  }
-
-  ${ifProp(
-    "checked",
-    css`
-      box-shadow: 0 3px 5px rgba(50, 50, 50, 0.4);
-
-      :after {
-        content: "";
-        position: absolute;
-        width: 80%;
-        height: 80%;
-        display: block;
-        border-radius: 2px;
-        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;
-      }
-    `
-  )}
-`;
-
-interface CheckboxProps
-  extends ChecknoxInputProps,
-    Omit<BoxProps, "name" | "checked" | "onChange" | "value"> {
-  label: string;
-  value?: boolean | string;
-}
-
-export const Checkbox: FC<CheckboxProps> = ({
-  checked,
-  height,
-  id,
-  label,
-  name,
-  width,
-  onChange,
-  ...rest
-}) => {
-  return (
-    <Flex
-      flex="0 0 auto"
-      as="label"
-      alignItems="center"
-      style={{ cursor: "pointer" }}
-      {...(rest as any)}
-    >
-      <CheckboxInput
-        onChange={onChange}
-        name={name}
-        id={id}
-        checked={checked}
-      />
-      <CheckboxBox checked={checked} width={width} height={height} />
-      <Span ml={[10, 12]} mt="1px" color="#555">
-        {label}
-      </Span>
-    </Flex>
-  );
-};
-
-Checkbox.defaultProps = {
-  width: [16, 18],
-  height: [16, 18],
-  fontSize: [15, 16]
-};

+ 0 - 38
client/components/Layout.tsx

@@ -1,38 +0,0 @@
-import React from "react";
-import { Flex } from "rebass/styled-components";
-import { FC } from "react";
-
-type Props = React.ComponentProps<typeof Flex>;
-
-export const Col: FC<Props> = (props) => (
-  <Flex flexDirection="column" {...props} />
-);
-
-export const RowCenterV: FC<Props> = (props) => (
-  <Flex alignItems="center" {...props} />
-);
-
-export const RowCenterH: FC<Props> = (props) => (
-  <Flex justifyContent="center" {...props} />
-);
-
-export const RowCenter: FC<Props> = (props) => (
-  <Flex alignItems="center" justifyContent="center" {...props} />
-);
-
-export const ColCenterV: FC<Props> = (props) => (
-  <Flex flexDirection="column" justifyContent="center" {...props} />
-);
-
-export const ColCenterH: FC<Props> = (props) => (
-  <Flex flexDirection="column" alignItems="center" {...props} />
-);
-
-export const ColCenter: FC<Props> = (props) => (
-  <Flex
-    flexDirection="column"
-    alignItems="center"
-    justifyContent="center"
-    {...props}
-  />
-);

+ 0 - 761
client/components/LinksTable.tsx

@@ -1,761 +0,0 @@
-import formatDistanceToNow from "date-fns/formatDistanceToNow";
-import { CopyToClipboard } from "react-copy-to-clipboard";
-import React, { FC, useState, useEffect } from "react";
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import styled, { css } from "styled-components";
-import { ifProp } from "styled-tools";
-import getConfig from "next/config";
-import QRCode from "qrcode.react";
-import differenceInMilliseconds from "date-fns/differenceInMilliseconds";
-import ms from "ms";
-
-import { removeProtocol, withComma, errorMessage } from "../utils";
-import { useStoreActions, useStoreState } from "../store";
-import { Link as LinkType } from "../store/links";
-import { Checkbox, TextInput } from "./Input";
-import { NavButton, Button } from "./Button";
-import { Col, RowCenter } from "./Layout";
-import Text, { H2, Span } from "./Text";
-import { useMessage } from "../hooks";
-import Animation from "./Animation";
-import { Colors } from "../consts";
-import Tooltip from "./Tooltip";
-import Table from "./Table";
-import ALink from "./ALink";
-import Modal from "./Modal";
-import Icon from "./Icon";
-
-const { publicRuntimeConfig } = getConfig();
-
-const Tr = styled(Flex).attrs({ as: "tr", px: [12, 12, 2] })``;
-const Th = styled(Flex)``;
-Th.defaultProps = { as: "th", flexBasis: 0, py: [12, 12, 3], px: [12, 12, 3] };
-
-const Td = styled(Flex)<{ withFade?: boolean }>`
-  position: relative;
-  white-space: nowrap;
-
-  ${ifProp(
-    "withFade",
-    css`
-      :after {
-        content: "";
-        position: absolute;
-        right: 0;
-        top: 0;
-        height: 100%;
-        width: 16px;
-        background: linear-gradient(to left, white, rgba(255, 255, 255, 0.001));
-      }
-
-      tr:hover &:after {
-        background: linear-gradient(
-          to left,
-          ${Colors.TableRowHover},
-          rgba(255, 255, 255, 0.001)
-        );
-      }
-    `
-  )}
-`;
-Td.defaultProps = {
-  as: "td",
-  fontSize: [15, 16],
-  alignItems: "center",
-  flexBasis: 0,
-  py: [12, 12, 3],
-  px: [12, 12, 3]
-};
-
-const EditContent = styled(Col)`
-  border-bottom: 1px solid ${Colors.TableRowHover};
-  background-color: #fafafa;
-`;
-
-const Action = (props: React.ComponentProps<typeof Icon>) => (
-  <Icon
-    as="button"
-    py={0}
-    px={0}
-    mr={2}
-    size={[23, 24]}
-    flexShrink={0}
-    p={["4px", "5px"]}
-    stroke="#666"
-    {...props}
-  />
-);
-
-const ogLinkFlex = { flexGrow: [1, 3, 7], flexShrink: [1, 3, 7] };
-const createdFlex = { flexGrow: [1, 1, 2.5], flexShrink: [1, 1, 2.5] };
-const shortLinkFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
-const viewsFlex = {
-  flexGrow: [0.5, 0.5, 1],
-  flexShrink: [0.5, 0.5, 1],
-  justifyContent: "flex-end"
-};
-const actionsFlex = { flexGrow: [1, 1, 3], flexShrink: [1, 1, 3] };
-
-interface RowProps {
-  index: number;
-  link: LinkType;
-  setDeleteModal: (number) => void;
-}
-
-interface BanForm {
-  host: boolean;
-  user: boolean;
-  userLinks: boolean;
-  domain: boolean;
-}
-
-interface EditForm {
-  target: string;
-  address: string;
-  description?: string;
-  expire_in?: string;
-  password?: string;
-}
-
-const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
-  const isAdmin = useStoreState((s) => s.auth.isAdmin);
-  const ban = useStoreActions((s) => s.links.ban);
-  const edit = useStoreActions((s) => s.links.edit);
-  const [banFormState, { checkbox }] = useFormState<BanForm>();
-  const [editFormState, { text, label, password }] = useFormState<EditForm>(
-    {
-      target: link.target,
-      address: link.address,
-      description: link.description,
-      expire_in: link.expire_in
-        ? ms(differenceInMilliseconds(new Date(link.expire_in), new Date()), {
-            long: true
-          })
-        : "",
-      password: ""
-    },
-    { withIds: true }
-  );
-  const [copied, setCopied] = useState(false);
-  const [showEdit, setShowEdit] = useState(false);
-  const [qrModal, setQRModal] = useState(false);
-  const [banModal, setBanModal] = useState(false);
-  const [banLoading, setBanLoading] = useState(false);
-  const [banMessage, setBanMessage] = useMessage();
-  const [editLoading, setEditLoading] = useState(false);
-  const [editMessage, setEditMessage] = useMessage();
-
-  const onCopy = () => {
-    setCopied(true);
-    setTimeout(() => {
-      setCopied(false);
-    }, 1500);
-  };
-
-  const onBan = async () => {
-    setBanLoading(true);
-    try {
-      const res = await ban({ id: link.id, ...banFormState.values });
-      setBanMessage(res.message, "green");
-      setTimeout(() => {
-        setBanModal(false);
-      }, 2000);
-    } catch (err) {
-      setBanMessage(errorMessage(err));
-    }
-    setBanLoading(false);
-  };
-
-  const onEdit = async () => {
-    if (editLoading) return;
-    setEditLoading(true);
-    try {
-      await edit({ id: link.id, ...editFormState.values });
-      setShowEdit(false);
-    } catch (err) {
-      setEditMessage(errorMessage(err));
-    }
-    editFormState.setField("password", "");
-    setEditLoading(false);
-  };
-
-  const toggleEdit = () => {
-    setShowEdit((s) => !s);
-    if (showEdit) editFormState.reset();
-    setEditMessage("");
-  };
-
-  return (
-    <>
-      <Tr key={link.id}>
-        <Td {...ogLinkFlex} withFade>
-          <Col alignItems="flex-start">
-            <ALink href={link.target}>{link.target}</ALink>
-            {link.description && (
-              <Text fontSize={[13, 14]} color="#888">
-                {link.description}
-              </Text>
-            )}
-          </Col>
-        </Td>
-        <Td {...createdFlex} flexDirection="column" alignItems="flex-start">
-          <Text>{formatDistanceToNow(new Date(link.created_at))} ago</Text>
-          {link.expire_in && (
-            <Text fontSize={[13, 14]} color="#888">
-              Expires in{" "}
-              {ms(
-                differenceInMilliseconds(new Date(link.expire_in), new Date()),
-                {
-                  long: true
-                }
-              )}
-            </Text>
-          )}
-        </Td>
-        <Td {...shortLinkFlex} withFade>
-          {copied ? (
-            <Animation
-              minWidth={32}
-              offset="10px"
-              duration="0.2s"
-              alignItems="center"
-            >
-              <Icon
-                size={[23, 24]}
-                py={0}
-                px={0}
-                mr={2}
-                p="3px"
-                name="check"
-                strokeWidth="3"
-                stroke={Colors.CheckIcon}
-              />
-            </Animation>
-          ) : (
-            <Animation minWidth={32} offset="-10px" duration="0.2s">
-              <CopyToClipboard text={link.link} onCopy={onCopy}>
-                <Action
-                  name="copy"
-                  strokeWidth="2.5"
-                  stroke={Colors.CopyIcon}
-                  backgroundColor={Colors.CopyIconBg}
-                />
-              </CopyToClipboard>
-            </Animation>
-          )}
-          <ALink href={link.link}>{removeProtocol(link.link)}</ALink>
-        </Td>
-        <Td {...viewsFlex}>{withComma(link.visit_count)}</Td>
-        <Td {...actionsFlex} justifyContent="flex-end">
-          {link.password && (
-            <>
-              <Tooltip id={`${index}-tooltip-password`}>
-                Password protected
-              </Tooltip>
-              <Action
-                as="span"
-                data-tip
-                data-for={`${index}-tooltip-password`}
-                name="key"
-                stroke={"#bbb"}
-                strokeWidth="2.5"
-                backgroundColor="none"
-              />
-            </>
-          )}
-          {link.banned && (
-            <>
-              <Tooltip id={`${index}-tooltip-banned`}>Banned</Tooltip>
-              <Action
-                as="span"
-                data-tip
-                data-for={`${index}-tooltip-banned`}
-                name="stop"
-                stroke="#bbb"
-                strokeWidth="2.5"
-                backgroundColor="none"
-              />
-            </>
-          )}
-          {link.visit_count > 0 && (
-            <ALink
-              href={`/stats?id=${link.id}`}
-              title="View stats"
-              forButton
-              isNextLink
-            >
-              <Action
-                name="pieChart"
-                stroke={Colors.PieIcon}
-                strokeWidth="2.5"
-                backgroundColor={Colors.PieIconBg}
-              />
-            </ALink>
-          )}
-          <Action
-            name="qrcode"
-            stroke="none"
-            fill={Colors.QrCodeIcon}
-            backgroundColor={Colors.QrCodeIconBg}
-            onClick={() => setQRModal(true)}
-          />
-          <Action
-            name="editAlt"
-            strokeWidth="2.5"
-            stroke={Colors.EditIcon}
-            backgroundColor={Colors.EditIconBg}
-            onClick={toggleEdit}
-          />
-          {isAdmin && !link.banned && (
-            <Action
-              name="stop"
-              strokeWidth="2"
-              stroke={Colors.StopIcon}
-              backgroundColor={Colors.StopIconBg}
-              onClick={() => setBanModal(true)}
-            />
-          )}
-          <Action
-            mr={0}
-            name="trash"
-            strokeWidth="2"
-            stroke={Colors.TrashIcon}
-            backgroundColor={Colors.TrashIconBg}
-            onClick={() => setDeleteModal(index)}
-          />
-        </Td>
-      </Tr>
-      {showEdit && (
-        <EditContent as="tr">
-          <Col
-            as="td"
-            alignItems="flex-start"
-            px={[3, 3, 24]}
-            py={[3, 3, 24]}
-            width={1}
-          >
-            <Flex alignItems="flex-start" width={1}>
-              <Col alignItems="flex-start" mr={3}>
-                <Text
-                  {...label("target")}
-                  as="label"
-                  mb={2}
-                  fontSize={[14, 15]}
-                  bold
-                >
-                  Target:
-                </Text>
-                <Flex as="form">
-                  <TextInput
-                    {...text("target")}
-                    placeholder="Target..."
-                    placeholderSize={[13, 14]}
-                    fontSize={[14, 15]}
-                    height={[40, 44]}
-                    width={[1, 300, 420]}
-                    pl={[3, 24]}
-                    pr={[3, 24]}
-                    required
-                  />
-                </Flex>
-              </Col>
-              <Col alignItems="flex-start" mr={3}>
-                <Text
-                  {...label("address")}
-                  as="label"
-                  mb={2}
-                  fontSize={[14, 15]}
-                  bold
-                >
-                  {link.domain || publicRuntimeConfig.DEFAULT_DOMAIN}/
-                </Text>
-                <Flex as="form">
-                  <TextInput
-                    {...text("address")}
-                    placeholder="Custom address..."
-                    placeholderSize={[13, 14]}
-                    fontSize={[14, 15]}
-                    height={[40, 44]}
-                    width={[1, 210, 240]}
-                    pl={[3, 24]}
-                    pr={[3, 24]}
-                    required
-                  />
-                </Flex>
-              </Col>
-              <Col alignItems="flex-start">
-                <Text
-                  {...label("password")}
-                  as="label"
-                  mb={2}
-                  fontSize={[14, 15]}
-                  bold
-                >
-                  Password
-                </Text>
-                <Flex as="form">
-                  <TextInput
-                    {...password({
-                      name: "password"
-                    })}
-                    placeholder={link.password ? "••••••••" : "Password..."}
-                    autocomplete="off"
-                    data-lpignore
-                    pl={[3, 24]}
-                    pr={[3, 24]}
-                    placeholderSize={[13, 14]}
-                    fontSize={[14, 15]}
-                    height={[40, 44]}
-                    width={[1, 210, 240]}
-                  />
-                </Flex>
-              </Col>
-            </Flex>
-            <Flex alignItems="flex-start" width={1} mt={3}>
-              <Col alignItems="flex-start" mr={3}>
-                <Text
-                  {...label("description")}
-                  as="label"
-                  mb={2}
-                  fontSize={[14, 15]}
-                  bold
-                >
-                  Description:
-                </Text>
-                <Flex as="form">
-                  <TextInput
-                    {...text("description")}
-                    placeholder="description..."
-                    placeholderSize={[13, 14]}
-                    fontSize={[14, 15]}
-                    height={[40, 44]}
-                    width={[1, 300, 420]}
-                    pl={[3, 24]}
-                    pr={[3, 24]}
-                    required
-                  />
-                </Flex>
-              </Col>
-              <Col alignItems="flex-start">
-                <Text
-                  {...label("expire_in")}
-                  as="label"
-                  mb={2}
-                  fontSize={[14, 15]}
-                  bold
-                >
-                  Expire in:
-                </Text>
-                <Flex as="form">
-                  <TextInput
-                    {...text("expire_in")}
-                    placeholder="2 minutes/hours/days"
-                    placeholderSize={[13, 14]}
-                    fontSize={[14, 15]}
-                    height={[40, 44]}
-                    width={[1, 210, 240]}
-                    pl={[3, 24]}
-                    pr={[3, 24]}
-                    required
-                  />
-                </Flex>
-              </Col>
-            </Flex>
-            <Button
-              color="blue"
-              mt={3}
-              height={[30, 38]}
-              disabled={editLoading}
-              onClick={onEdit}
-            >
-              <Icon
-                name={editLoading ? "spinner" : "refresh"}
-                stroke="white"
-                mr={2}
-              />
-              {editLoading ? "Updating..." : "Update"}
-            </Button>
-            {editMessage.text && (
-              <Text mt={3} fontSize={15} color={editMessage.color}>
-                {editMessage.text}
-              </Text>
-            )}
-          </Col>
-        </EditContent>
-      )}
-      <Modal
-        id="table-qrcode-modal"
-        minWidth="max-content"
-        show={qrModal}
-        closeHandler={() => setQRModal(false)}
-      >
-        <RowCenter width={192}>
-          <QRCode size={192} value={link.link} />
-        </RowCenter>
-      </Modal>
-      <Modal
-        id="table-ban-modal"
-        show={banModal}
-        closeHandler={() => setBanModal(false)}
-      >
-        <>
-          <H2 mb={24} textAlign="center" bold>
-            Ban link?
-          </H2>
-          <Text mb={24} textAlign="center">
-            Are you sure do you want to ban the link{" "}
-            <Span bold>&quot;{removeProtocol(link.link)}&quot;</Span>?
-          </Text>
-          <RowCenter>
-            <Checkbox {...checkbox("user")} label="User" mb={12} />
-            <Checkbox {...checkbox("userLinks")} label="User links" mb={12} />
-            <Checkbox {...checkbox("host")} label="Host" mb={12} />
-            <Checkbox {...checkbox("domain")} label="Domain" mb={12} />
-          </RowCenter>
-          <Flex justifyContent="center" mt={4}>
-            {banLoading ? (
-              <>
-                <Icon name="spinner" size={20} stroke={Colors.Spinner} />
-              </>
-            ) : banMessage.text ? (
-              <Text fontSize={15} color={banMessage.color}>
-                {banMessage.text}
-              </Text>
-            ) : (
-              <>
-                <Button color="gray" mr={3} onClick={() => setBanModal(false)}>
-                  Cancel
-                </Button>
-                <Button color="red" ml={3} onClick={onBan}>
-                  <Icon name="stop" stroke="white" mr={2} />
-                  Ban
-                </Button>
-              </>
-            )}
-          </Flex>
-        </>
-      </Modal>
-    </>
-  );
-};
-
-interface Form {
-  all: boolean;
-  limit: string;
-  skip: string;
-  search: string;
-}
-
-const LinksTable: FC = () => {
-  const isAdmin = useStoreState((s) => s.auth.isAdmin);
-  const links = useStoreState((s) => s.links);
-  const { get, remove } = useStoreActions((s) => s.links);
-  const [tableMessage, setTableMessage] = useState("No links to show.");
-  const [deleteModal, setDeleteModal] = useState(-1);
-  const [deleteLoading, setDeleteLoading] = useState(false);
-  const [deleteMessage, setDeleteMessage] = useMessage();
-  const [formState, { label, checkbox, text }] = useFormState<Form>(
-    { skip: "0", limit: "10", all: false },
-    { withIds: true }
-  );
-
-  const options = formState.values;
-  const linkToDelete = links.items[deleteModal];
-
-  useEffect(() => {
-    get(options).catch((err) =>
-      setTableMessage(err?.response?.data?.error || "An error occurred.")
-    );
-  }, [options, get]);
-
-  const onSubmit = (e) => {
-    e.preventDefault();
-    get(options);
-  };
-
-  const onDelete = async () => {
-    setDeleteLoading(true);
-    try {
-      await remove(linkToDelete.id);
-      await get(options);
-      setDeleteModal(-1);
-    } catch (err) {
-      setDeleteMessage(errorMessage(err));
-    }
-    setDeleteLoading(false);
-  };
-
-  const onNavChange = (nextPage: number) => () => {
-    formState.setField("skip", (parseInt(options.skip) + nextPage).toString());
-  };
-
-  const Nav = (
-    <Th
-      alignItems="center"
-      justifyContent="flex-end"
-      flexGrow={1}
-      flexShrink={1}
-    >
-      <Flex as="ul" m={0} p={0} style={{ listStyle: "none" }}>
-        {["10", "25", "50"].map((c) => (
-          <Flex key={c} ml={[10, 12]}>
-            <NavButton
-              disabled={options.limit === c}
-              onClick={() => {
-                formState.setField("limit", c);
-                formState.setField("skip", "0");
-              }}
-            >
-              {c}
-            </NavButton>
-          </Flex>
-        ))}
-      </Flex>
-      <Flex
-        width="1px"
-        height={20}
-        mx={[3, 24]}
-        style={{ backgroundColor: "#ccc" }}
-      />
-      <Flex>
-        <NavButton
-          onClick={onNavChange(-parseInt(options.limit))}
-          disabled={options.skip === "0"}
-          px={2}
-        >
-          <Icon name="chevronLeft" size={15} />
-        </NavButton>
-        <NavButton
-          onClick={onNavChange(parseInt(options.limit))}
-          disabled={
-            parseInt(options.skip) + parseInt(options.limit) > links.total
-          }
-          ml={12}
-          px={2}
-        >
-          <Icon name="chevronRight" size={15} />
-        </NavButton>
-      </Flex>
-    </Th>
-  );
-
-  return (
-    <Col width={1200} maxWidth="95%" margin="40px 0 120px" my={6}>
-      <H2 mb={3} light>
-        Recent shortened links.
-      </H2>
-      <Table scrollWidth="1000px">
-        <thead>
-          <Tr justifyContent="space-between">
-            <Th flexGrow={1} flexShrink={1}>
-              <Flex as="form" onSubmit={onSubmit}>
-                <TextInput
-                  {...text("search")}
-                  placeholder="Search..."
-                  height={[30, 32]}
-                  placeholderSize={[13, 13, 13, 13]}
-                  fontSize={[14]}
-                  pl={12}
-                  pr={12}
-                  width={[1]}
-                  br="3px"
-                  bbw="2px"
-                />
-
-                {isAdmin && (
-                  <Checkbox
-                    {...label("all")}
-                    {...checkbox("all")}
-                    label="All links"
-                    ml={3}
-                    fontSize={[14, 15]}
-                    width={[15, 16]}
-                    height={[15, 16]}
-                  />
-                )}
-              </Flex>
-            </Th>
-            {Nav}
-          </Tr>
-          <Tr>
-            <Th {...ogLinkFlex}>Original URL</Th>
-            <Th {...createdFlex}>Created</Th>
-            <Th {...shortLinkFlex}>Short URL</Th>
-            <Th {...viewsFlex}>Views</Th>
-            <Th {...actionsFlex}></Th>
-          </Tr>
-        </thead>
-        <tbody style={{ opacity: links.loading ? 0.4 : 1 }}>
-          {!links.items.length ? (
-            <Tr width={1} justifyContent="center">
-              <Td flex="1 1 auto" justifyContent="center">
-                <Text fontSize={18} light>
-                  {links.loading ? "Loading links..." : tableMessage}
-                </Text>
-              </Td>
-            </Tr>
-          ) : (
-            <>
-              {links.items.map((link, index) => (
-                <Row
-                  setDeleteModal={setDeleteModal}
-                  index={index}
-                  link={link}
-                  key={link.id}
-                />
-              ))}
-            </>
-          )}
-        </tbody>
-        <tfoot>
-          <Tr justifyContent="flex-end">{Nav}</Tr>
-        </tfoot>
-      </Table>
-      <Modal
-        id="delete-custom-domain"
-        show={deleteModal > -1}
-        closeHandler={() => setDeleteModal(-1)}
-      >
-        {linkToDelete && (
-          <>
-            <H2 mb={24} textAlign="center" bold>
-              Delete link?
-            </H2>
-            <Text textAlign="center">
-              Are you sure do you want to delete the link{" "}
-              <Span bold>&quot;{removeProtocol(linkToDelete.link)}&quot;</Span>?
-            </Text>
-            <Flex justifyContent="center" mt={44}>
-              {deleteLoading ? (
-                <>
-                  <Icon name="spinner" size={20} stroke={Colors.Spinner} />
-                </>
-              ) : deleteMessage.text ? (
-                <Text fontSize={15} color={deleteMessage.color}>
-                  {deleteMessage.text}
-                </Text>
-              ) : (
-                <>
-                  <Button
-                    color="gray"
-                    mr={3}
-                    onClick={() => setDeleteModal(-1)}
-                  >
-                    Cancel
-                  </Button>
-                  <Button color="red" ml={3} onClick={onDelete}>
-                    <Icon name="trash" stroke="white" mr={2} />
-                    Delete
-                  </Button>
-                </>
-              )}
-            </Flex>
-          </>
-        )}
-      </Modal>
-    </Col>
-  );
-};
-
-export default LinksTable;

+ 0 - 58
client/components/Modal.tsx

@@ -1,58 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import styled from "styled-components";
-import React, { FC } from "react";
-import ReactDOM from "react-dom";
-
-import Animation from "./Animation";
-
-interface Props extends React.ComponentProps<typeof Flex> {
-  show: boolean;
-  id?: string;
-  closeHandler?: () => unknown;
-}
-
-const Wrapper = styled.div`
-  position: fixed;
-  width: 100%;
-  height: 100%;
-  top: 0;
-  left: 0;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background-color: rgba(50, 50, 50, 0.8);
-  z-index: 1000;
-`;
-
-const Modal: FC<Props> = ({ children, id, show, closeHandler, ...rest }) => {
-  if (!show) return null;
-
-  const onClickOutside = (e) => {
-    if (e.target.id === id) closeHandler();
-  };
-
-  return ReactDOM.createPortal(
-    <Wrapper id={id} onClick={onClickOutside}>
-      <Animation
-        offset="-20px"
-        duration="0.2s"
-        minWidth={[400, 450]}
-        maxWidth="90%"
-        py={[32, 32, 48]}
-        px={[24, 24, 32]}
-        style={{ borderRadius: 8, backgroundColor: "white" }}
-        flexDirection="column"
-        {...rest}
-      >
-        {children}
-      </Animation>
-    </Wrapper>,
-    document.body
-  );
-};
-
-Modal.defaultProps = {
-  show: false
-};
-
-export default Modal;

+ 0 - 76
client/components/NeedToLogin.tsx

@@ -1,76 +0,0 @@
-import React from "react";
-import Link from "next/link";
-import styled from "styled-components";
-import { Flex } from "rebass/styled-components";
-
-import { Button } from "./Button";
-import { fadeIn } from "../helpers/animations";
-import { Col } from "./Layout";
-
-const Wrapper = styled(Flex).attrs({
-  width: 1200,
-  maxWidth: "98%",
-  alignItems: "center",
-  margin: "150px 0 0",
-  flexDirection: ["column", "column", "row"]
-})`
-  animation: ${fadeIn} 0.8s ease-out;
-  box-sizing: border-box;
-
-  a {
-    text-decoration: none;
-  }
-`;
-
-const Title = styled.h2`
-  font-size: 28px;
-  font-weight: 300;
-  padding-right: 32px;
-  margin-bottom: 48px;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 22px;
-    text-align: center;
-    padding-right: 0;
-    margin-bottom: 32px;
-    padding: 0 40px;
-  }
-
-  @media only screen and (max-width: 448px) {
-    font-size: 18px;
-    text-align: center;
-    margin-bottom: 24px;
-  }
-`;
-
-const Image = styled.img`
-  flex: 0 0 60%;
-  width: 60%;
-  max-width: 100%;
-  height: auto;
-
-  @media only screen and (max-width: 768px) {
-    flex-basis: 100%;
-    width: 100%;
-  }
-`;
-
-const NeedToLogin = () => (
-  <Wrapper>
-    <Col
-      alignItems={["center", "center", "flex-start"]}
-      mt={-32}
-      mb={[32, 32, 0]}
-    >
-      <Title>
-        Manage links, set custom <b>domains</b> and view <b>stats</b>.
-      </Title>
-      <Link href="/login" title="login / signup">
-        <Button>Login / Signup</Button>
-      </Link>
-    </Col>
-    <Image src="/images/callout.png" alt="callout image" />
-  </Wrapper>
-);
-
-export default NeedToLogin;

+ 0 - 19
client/components/PageLoading.tsx

@@ -1,19 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import React from "react";
-
-import { Colors } from "../consts";
-import Icon from "./Icon";
-
-const PageLoading = () => (
-  <Flex
-    flex="1 1 250px"
-    alignItems="center"
-    alignSelf="center"
-    justifyContent="center"
-    margin="0 0 48px"
-  >
-    <Icon name="spinner" size={24} stroke={Colors.Spinner} />
-  </Flex>
-);
-
-export default PageLoading;

+ 0 - 24
client/components/ReCaptcha.tsx

@@ -1,24 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import getConfig from "next/config";
-import React from "react";
-
-const { publicRuntimeConfig } = getConfig();
-
-const ReCaptcha = () => {
-  if (process.env.NODE_ENV !== "production") return null;
-  if (!publicRuntimeConfig.RECAPTCHA_SITE_KEY) return null;
-
-  return (
-    <Flex
-      margin="54px 0 16px"
-      id="g-recaptcha"
-      className="g-recaptcha"
-      data-sitekey={publicRuntimeConfig.RECAPTCHA_SITE_KEY}
-      data-callback="recaptchaCallback"
-      data-size="invisible"
-      data-badge="inline"
-    />
-  );
-};
-
-export default ReCaptcha;

+ 0 - 108
client/components/Settings/SettingsApi.tsx

@@ -1,108 +0,0 @@
-import { CopyToClipboard } from "react-copy-to-clipboard";
-import { Flex } from "rebass/styled-components";
-import React, { FC, useState } from "react";
-import styled from "styled-components";
-
-import { useStoreState, useStoreActions } from "../../store";
-import { useCopy, useMessage } from "../../hooks";
-import { errorMessage } from "../../utils";
-import { Colors } from "../../consts";
-import Animation from "../Animation";
-import { Button } from "../Button";
-import Text, { H2 } from "../Text";
-import { Col } from "../Layout";
-import ALink from "../ALink";
-import Icon from "../Icon";
-
-const ApiKey = styled(Text).attrs({
-  mt: [0, "2px"],
-  fontSize: [15, 16],
-  bold: true
-})`
-  border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
-  cursor: pointer;
-  word-break: break-word;
-
-  :hover {
-    opacity: 0.8;
-  }
-`;
-
-const SettingsApi: FC = () => {
-  const [copied, setCopied] = useCopy();
-  const [message, setMessage] = useMessage(1500);
-  const [loading, setLoading] = useState(false);
-  const apikey = useStoreState((s) => s.settings.apikey);
-  const generateApiKey = useStoreActions((s) => s.settings.generateApiKey);
-
-  const onSubmit = async () => {
-    if (loading) return;
-    setLoading(true);
-    await generateApiKey().catch((err) => setMessage(errorMessage(err)));
-    setLoading(false);
-  };
-
-  return (
-    <Col alignItems="flex-start" maxWidth="100%">
-      <H2 mb={4} bold>
-        API
-      </H2>
-      <Text mb={4}>
-        In additional to this website, you can use the API to create, delete and
-        get shortened URLs. If
-        {" you're"} not familiar with API, {"don't"} generate the key. DO NOT
-        share this key on the client side of your website.{" "}
-        <ALink href="https://docs.kutt.it" title="API Docs" target="_blank">
-          Read API docs.
-        </ALink>
-      </Text>
-      {apikey && (
-        <Flex alignItems={["flex-start", "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>
-          )}
-          <CopyToClipboard text={apikey} onCopy={setCopied}>
-            <ApiKey>{apikey}</ApiKey>
-          </CopyToClipboard>
-        </Flex>
-      )}
-      <Button mt={3} color="purple" onClick={onSubmit} disabled={loading}>
-        <Icon name={loading ? "spinner" : "zap"} mr={2} stroke="white" />
-        {loading ? "Generating..." : apikey ? "Regenerate" : "Generate"} key
-      </Button>
-      <Text fontSize={15} mt={3} color={message.color}>
-        {message.text}
-      </Text>
-    </Col>
-  );
-};
-
-export default SettingsApi;

+ 0 - 99
client/components/Settings/SettingsChangeEmail.tsx

@@ -1,99 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import React, { FC, useState } from "react";
-import { Flex } from "rebass";
-import axios from "axios";
-
-import { getAxiosConfig } from "../../utils";
-import { useMessage } from "../../hooks";
-import { APIv2 } from "../../consts";
-import { TextInput } from "../Input";
-import Text, { H2 } from "../Text";
-import { Button } from "../Button";
-import { Col } from "../Layout";
-import Icon from "../Icon";
-
-const SettingsChangeEmail: FC = () => {
-  const [loading, setLoading] = useState(false);
-  const [message, setMessage] = useMessage(5000);
-  const [formState, { password, email, label }] = useFormState<{
-    changeemailpass: string;
-    changeemailaddress: string;
-  }>(null, {
-    withIds: true
-  });
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    if (loading) return;
-    setLoading(true);
-    try {
-      const res = await axios.post(
-        APIv2.AuthChangeEmail,
-        {
-          password: formState.values.changeemailpass,
-          email: formState.values.changeemailaddress
-        },
-        getAxiosConfig()
-      );
-      setMessage(res.data.message, "green");
-    } catch (error) {
-      setMessage(error?.response?.data?.error || "Couldn't send email.");
-    }
-    setLoading(false);
-  };
-
-  return (
-    <Col alignItems="flex-start" maxWidth="100%">
-      <H2 mb={4} bold>
-        Change email address
-      </H2>
-      <Col alignItems="flex-start" onSubmit={onSubmit} width={1} as="form">
-        <Flex width={1} flexDirection={["column", "row"]}>
-          <Col mr={[0, 2]} mb={[3, 0]} flex="0 0 auto">
-            <Text
-              {...label("changeemailpass")}
-              as="label"
-              mb={[2, 3]}
-              fontSize={[15, 16]}
-              bold
-            >
-              Password:
-            </Text>
-            <TextInput
-              {...password("changeemailpass")}
-              placeholder="Password..."
-              maxWidth="240px"
-              required
-            />
-          </Col>
-          <Col ml={[0, 2]} flex="0 0 auto">
-            <Text
-              {...label("changeemailaddress")}
-              as="label"
-              mb={[2, 3]}
-              fontSize={[15, 16]}
-              bold
-            >
-              New email address:
-            </Text>
-            <TextInput
-              {...email("changeemailaddress")}
-              placeholder="john@example.com"
-              flex="1 1 auto"
-              maxWidth="240px"
-            />
-          </Col>
-        </Flex>
-        <Button type="submit" color="blue" mt={[24, 3]} disabled={loading}>
-          <Icon name={loading ? "spinner" : "refresh"} mr={2} stroke="white" />
-          {loading ? "Sending..." : "Update"}
-        </Button>
-      </Col>
-      <Text fontSize={15} color={message.color} mt={3}>
-        {message.text}
-      </Text>
-    </Col>
-  );
-};
-
-export default SettingsChangeEmail;

+ 0 - 122
client/components/Settings/SettingsDeleteAccount.tsx

@@ -1,122 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import React, { FC, useState } from "react";
-import getConfig from "next/config";
-import Router from "next/router";
-import axios from "axios";
-
-import { getAxiosConfig } from "../../utils";
-import { Col, RowCenterV, RowCenterH } from "../Layout";
-import Text, { H2, Span } from "../Text";
-import { useMessage } from "../../hooks";
-import { TextInput } from "../Input";
-import { APIv2, Colors } from "../../consts";
-import { Button } from "../Button";
-import Icon from "../Icon";
-import Modal from "../Modal";
-
-const { publicRuntimeConfig } = getConfig();
-
-const SettingsDeleteAccount: FC = () => {
-  const [message, setMessage] = useMessage(1500);
-  const [loading, setLoading] = useState(false);
-  const [modal, setModal] = useState(false);
-  const [formState, { password, label }] = useFormState<{ accpass: string }>(
-    null,
-    {
-      withIds: true
-    }
-  );
-
-  const onSubmit = async e => {
-    e.preventDefault();
-    if (loading) return;
-    setModal(true);
-  };
-
-  const onDelete = async e => {
-    e.preventDefault();
-    if (loading) return;
-    setLoading(true);
-    try {
-      await axios.post(
-        `${APIv2.Users}/delete`,
-        { password: formState.values.accpass },
-        getAxiosConfig()
-      );
-      Router.push("/logout");
-    } catch (error) {
-      setMessage(error.response.data.error);
-    }
-    setLoading(false);
-  };
-
-  return (
-    <Col alignItems="flex-start" maxWidth="100%">
-      <H2 mb={4} bold>
-        Delete account
-      </H2>
-      <Text mb={4}>
-        Delete your account from {publicRuntimeConfig.SITE_NAME}.
-      </Text>
-      <Text
-        {...label("password")}
-        as="label"
-        mb={[2, 3]}
-        fontSize={[15, 16]}
-        bold
-      >
-        Password:
-      </Text>
-      <RowCenterV as="form" onSubmit={onSubmit}>
-        <TextInput
-          {...password("accpass")}
-          placeholder="Password..."
-          autocomplete="off"
-          mr={3}
-        />
-        <Button color="red" type="submit" disabled={loading}>
-          <Icon name={loading ? "spinner" : "trash"} mr={2} stroke="white" />
-          Delete
-        </Button>
-      </RowCenterV>
-      <Modal
-        id="delete-account"
-        show={modal}
-        closeHandler={() => setModal(false)}
-      >
-        <>
-          <H2 mb={24} textAlign="center" bold>
-            Delete account?
-          </H2>
-          <Text textAlign="center">
-            All of your data including your <Span bold>LINKS</Span> and{" "}
-            <Span bold>STATS</Span> will be deleted.
-          </Text>
-          <RowCenterH mt={44}>
-            {loading ? (
-              <>
-                <Icon name="spinner" size={20} stroke={Colors.Spinner} />
-              </>
-            ) : message.text ? (
-              <Text fontSize={15} color={message.color}>
-                {message.text}
-              </Text>
-            ) : (
-              <>
-                <Button color="gray" mr={3} onClick={() => setModal(false)}>
-                  Cancel
-                </Button>
-                <Button color="red" ml={3} onClick={onDelete}>
-                  <Icon name="trash" stroke="white" mr={2} />
-                  Delete
-                </Button>
-              </>
-            )}
-          </RowCenterH>
-        </>
-      </Modal>
-    </Col>
-  );
-};
-
-export default SettingsDeleteAccount;

+ 0 - 204
client/components/Settings/SettingsDomain.tsx

@@ -1,204 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import React, { FC, useState } from "react";
-import styled from "styled-components";
-import getConfig from "next/config";
-
-import { useStoreState, useStoreActions } from "../../store";
-import { Domain } from "../../store/settings";
-import { errorMessage } from "../../utils";
-import { useMessage } from "../../hooks";
-import Text, { H2, Span } from "../Text";
-import { Colors } from "../../consts";
-import { TextInput } from "../Input";
-import { Button } from "../Button";
-import { Col } from "../Layout";
-import Table from "../Table";
-import Modal from "../Modal";
-import Icon from "../Icon";
-
-const { publicRuntimeConfig } = getConfig();
-
-const Th = styled(Flex).attrs({ as: "th", py: 3, px: 3 })`
-  font-size: 15px;
-`;
-const Td = styled(Flex).attrs({ as: "td", py: 12, px: 3 })`
-  font-size: 15px;
-`;
-
-const SettingsDomain: FC = () => {
-  const { saveDomain, deleteDomain } = useStoreActions((s) => s.settings);
-  const [domainToDelete, setDomainToDelete] = useState<Domain>(null);
-  const [deleteLoading, setDeleteLoading] = useState(false);
-  const domains = useStoreState((s) => s.settings.domains);
-  const [message, setMessage] = useMessage(2000);
-  const [loading, setLoading] = useState(false);
-  const [modal, setModal] = useState(false);
-  const [formState, { label, text }] = useFormState<{
-    address: string;
-    homepage: string;
-  }>(null, { withIds: true });
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    setLoading(true);
-
-    try {
-      await saveDomain(formState.values);
-    } catch (err) {
-      setMessage(err?.response?.data?.error || "Couldn't add domain.");
-    }
-    formState.clear();
-    setLoading(false);
-  };
-
-  const closeModal = () => {
-    setDomainToDelete(null);
-    setModal(false);
-  };
-
-  const onDelete = async () => {
-    setDeleteLoading(true);
-    await deleteDomain(domainToDelete.id).catch((err) =>
-      setMessage(errorMessage(err, "Couldn't delete the domain."))
-    );
-    setMessage("Domain has been deleted successfully.", "green");
-    closeModal();
-    setDeleteLoading(false);
-  };
-
-  return (
-    <Col alignItems="flex-start" maxWidth="100%">
-      <H2 mb={4} bold>
-        Custom domain
-      </H2>
-      <Text mb={3}>
-        You can set a custom domain for your short URLs, so instead of{" "}
-        <b>{publicRuntimeConfig.DEFAULT_DOMAIN}/shorturl</b> you can have{" "}
-        <b>example.com/shorturl.</b>
-      </Text>
-      <Text mb={4}>
-        Point your domain A record to <b>192.64.116.170</b> then add the domain
-        via form below:
-      </Text>
-      {domains.length > 0 && (
-        <Table my={3} scrollWidth="550px">
-          <thead>
-            <tr>
-              <Th width={2 / 5}>Domain</Th>
-              <Th width={2 / 5}>Homepage</Th>
-              <Th width={1 / 5}></Th>
-            </tr>
-          </thead>
-          <tbody>
-            {domains.map((d) => (
-              <tr key={d.address}>
-                <Td width={2 / 5}>{d.address}</Td>
-                <Td width={2 / 5}>
-                  {d.homepage || publicRuntimeConfig.DEFAULT_DOMAIN}
-                </Td>
-                <Td width={1 / 5} justifyContent="center">
-                  <Icon
-                    as="button"
-                    name="trash"
-                    stroke={Colors.TrashIcon}
-                    strokeWidth="2.5"
-                    backgroundColor={Colors.TrashIconBg}
-                    py={0}
-                    px={0}
-                    size={[23, 24]}
-                    p={["4px", "5px"]}
-                    onClick={() => {
-                      setDomainToDelete(d);
-                      setModal(true);
-                    }}
-                  />
-                </Td>
-              </tr>
-            ))}
-          </tbody>
-        </Table>
-      )}
-      <Col
-        alignItems="flex-start"
-        onSubmit={onSubmit}
-        width={1}
-        as="form"
-        my={[3, 4]}
-      >
-        <Flex width={1} flexDirection={["column", "row"]}>
-          <Col mr={[0, 2]} mb={[3, 0]} flex="0 0 auto">
-            <Text
-              {...label("address")}
-              as="label"
-              mb={[2, 3]}
-              fontSize={[15, 16]}
-              bold
-            >
-              Domain:
-            </Text>
-            <TextInput
-              {...text("address")}
-              placeholder="example.com"
-              maxWidth="240px"
-              required
-            />
-          </Col>
-          <Col ml={[0, 2]} flex="0 0 auto">
-            <Text
-              {...label("homepage")}
-              as="label"
-              mb={[2, 3]}
-              fontSize={[15, 16]}
-              bold
-            >
-              Homepage (optional):
-            </Text>
-            <TextInput
-              {...text("homepage")}
-              placeholder="Homepage URL"
-              flex="1 1 auto"
-              maxWidth="240px"
-            />
-          </Col>
-        </Flex>
-        <Button type="submit" color="purple" mt={[24, 3]} disabled={loading}>
-          <Icon name={loading ? "spinner" : "plus"} mr={2} stroke="white" />
-          {loading ? "Setting..." : "Set domain"}
-        </Button>
-      </Col>
-      <Text color={message.color}>{message.text}</Text>
-      <Modal id="delete-custom-domain" show={modal} closeHandler={closeModal}>
-        <H2 mb={24} textAlign="center" bold>
-          Delete domain?
-        </H2>
-        <Text textAlign="center">
-          Are you sure do you want to delete the domain{" "}
-          <Span bold>
-            &quot;{domainToDelete && domainToDelete.address}&quot;
-          </Span>
-          ?
-        </Text>
-        <Flex justifyContent="center" mt={44}>
-          {deleteLoading ? (
-            <>
-              <Icon name="spinner" size={20} stroke={Colors.Spinner} />
-            </>
-          ) : (
-            <>
-              <Button color="gray" mr={3} onClick={closeModal}>
-                Cancel
-              </Button>
-              <Button color="red" ml={3} onClick={onDelete}>
-                <Icon name="trash" stroke="white" mr={2} />
-                Delete
-              </Button>
-            </>
-          )}
-        </Flex>
-      </Modal>
-    </Col>
-  );
-};
-
-export default SettingsDomain;

+ 0 - 89
client/components/Settings/SettingsPassword.tsx

@@ -1,89 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import React, { FC, useState } from "react";
-import axios from "axios";
-
-import { getAxiosConfig } from "../../utils";
-import { useMessage } from "../../hooks";
-import { TextInput } from "../Input";
-import { APIv2 } from "../../consts";
-import { Button } from "../Button";
-import Text, { H2 } from "../Text";
-import { Col } from "../Layout";
-import Icon from "../Icon";
-
-const SettingsPassword: FC = () => {
-  const [loading, setLoading] = useState(false);
-  const [message, setMessage] = useMessage(2000);
-  const [formState, { password, label }] = useFormState<{ password: string }>(
-    null,
-    { withIds: true }
-  );
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    if (loading) return;
-    if (!formState.validity.password) {
-      return setMessage(formState.errors.password);
-    }
-    setLoading(true);
-    setMessage();
-    try {
-      const res = await axios.post(
-        APIv2.AuthChangePassword,
-        formState.values,
-        getAxiosConfig()
-      );
-      formState.clear();
-      setMessage(res.data.message, "green");
-    } catch (err) {
-      setMessage(err?.response?.data?.error || "Couldn't update the password.");
-    }
-    setLoading(false);
-  };
-
-  return (
-    <Col alignItems="flex-start" maxWidth="100%">
-      <H2 mb={4} bold>
-        Change password
-      </H2>
-      <Text mb={4}>Enter a new password to change your current password.</Text>
-      <Text
-        {...label("password")}
-        as="label"
-        mb={[2, 3]}
-        fontSize={[15, 16]}
-        bold
-      >
-        New password:
-      </Text>
-      <Flex as="form" onSubmit={onSubmit}>
-        <TextInput
-          {...password({
-            name: "password",
-            validate: (value) => {
-              const val = value.trim();
-              if (!val || val.length < 8) {
-                return "Password must be at least 8 chars.";
-              }
-            }
-          })}
-          autocomplete="off"
-          placeholder="New password..."
-          width={[1, 2 / 3]}
-          mr={3}
-          required
-        />
-        <Button type="submit" disabled={loading}>
-          <Icon name={loading ? "spinner" : "refresh"} mr={2} stroke="white" />
-          {loading ? "Updating..." : "Update"}
-        </Button>
-      </Flex>
-      <Text color={message.color} mt={3} fontSize={15}>
-        {message.text}
-      </Text>
-    </Col>
-  );
-};
-
-export default SettingsPassword;

+ 0 - 380
client/components/Shortener.tsx

@@ -1,380 +0,0 @@
-import { CopyToClipboard } from "react-copy-to-clipboard";
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import React, { useState } from "react";
-import styled from "styled-components";
-import getConfig from "next/config";
-
-import { useStoreActions, useStoreState } from "../store";
-import { Checkbox, Select, TextInput } from "./Input";
-import { Col, RowCenterH, RowCenter } from "./Layout";
-import { useMessage, useCopy } from "../hooks";
-import { removeProtocol } from "../utils";
-import Text, { H1, Span } from "./Text";
-import { Link } from "../store/links";
-import Animation from "./Animation";
-import { Colors } from "../consts";
-import Icon from "./Icon";
-
-const { publicRuntimeConfig } = getConfig();
-
-const SubmitIconWrapper = styled.div`
-  content: "";
-  position: absolute;
-  top: 0;
-  right: 12px;
-  width: 64px;
-  height: 100%;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  cursor: pointer;
-
-  :hover svg {
-    fill: #673ab7;
-  }
-  @media only screen and (max-width: 448px) {
-    right: 8px;
-    width: 40px;
-  }
-`;
-
-const ShortenedLink = styled(H1)`
-  cursor: "pointer";
-  border-bottom: 1px dotted ${Colors.StatsTotalUnderline};
-  cursor: pointer;
-
-  :hover {
-    opacity: 0.8;
-  }
-`;
-
-interface Form {
-  target: string;
-  domain?: string;
-  customurl?: string;
-  password?: string;
-  description?: string;
-  expire_in?: string;
-  showAdvanced?: boolean;
-}
-
-const defaultDomain = publicRuntimeConfig.DEFAULT_DOMAIN;
-
-const Shortener = () => {
-  const { isAuthenticated } = useStoreState((s) => s.auth);
-  const domains = useStoreState((s) => s.settings.domains);
-  const submit = useStoreActions((s) => s.links.submit);
-  const [link, setLink] = useState<Link | null>(null);
-  const [message, setMessage] = useMessage(3000);
-  const [loading, setLoading] = useState(false);
-  const [copied, setCopied] = useCopy();
-  const [formState, { raw, password, text, select, label }] =
-    useFormState<Form>(
-      { showAdvanced: false },
-      {
-        withIds: true,
-        onChange(e, stateValues, nextStateValues) {
-          if (stateValues.showAdvanced && !nextStateValues.showAdvanced) {
-            formState.clear();
-            formState.setField("target", stateValues.target);
-          }
-        }
-      }
-    );
-
-  const submitLink = async (reCaptchaToken?: string) => {
-    try {
-      const link = await submit({ ...formState.values, reCaptchaToken });
-      setLink(link);
-      formState.clear();
-    } catch (err) {
-      setMessage(
-        err?.response?.data?.error || "Couldn't create the short link."
-      );
-    }
-    setLoading(false);
-  };
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    if (loading) return;
-    setCopied(false);
-    setLoading(true);
-
-    if (
-      process.env.NODE_ENV === "production" &&
-      !!publicRuntimeConfig.RECAPTCHA_SITE_KEY &&
-      !isAuthenticated
-    ) {
-      window.grecaptcha.execute(window.captchaId);
-      const getCaptchaToken = () => {
-        setTimeout(() => {
-          if (window.isCaptchaReady) {
-            const reCaptchaToken = window.grecaptcha.getResponse(
-              window.captchaId
-            );
-            window.isCaptchaReady = false;
-            window.grecaptcha.reset(window.captchaId);
-            return submitLink(reCaptchaToken);
-          }
-          return getCaptchaToken();
-        }, 200);
-      };
-      return getCaptchaToken();
-    }
-
-    return submitLink();
-  };
-
-  const title = !link && (
-    <H1 fontSize={[25, 27, 32]} light>
-      Kutt your links{" "}
-      <Span style={{ borderBottom: "2px dotted #999" }} light>
-        shorter
-      </Span>
-      .
-    </H1>
-  );
-
-  const result = link && (
-    <Animation
-      as={RowCenter}
-      offset="-20px"
-      duration="0.4s"
-      style={{ position: "relative" }}
-    >
-      {copied ? (
-        <Animation offset="10px" duration="0.2s" alignItems="center">
-          <Icon
-            size={[30, 35]}
-            py={0}
-            px={0}
-            mr={3}
-            p={["4px", "5px"]}
-            name="check"
-            strokeWidth="3"
-            stroke={Colors.CheckIcon}
-          />
-        </Animation>
-      ) : (
-        <Animation offset="-10px" duration="0.2s">
-          <CopyToClipboard text={link.link} onCopy={setCopied}>
-            <Icon
-              as="button"
-              py={0}
-              px={0}
-              mr={3}
-              size={[30, 35]}
-              p={["6px", "7px"]}
-              name="copy"
-              strokeWidth="2.5"
-              stroke={Colors.CopyIcon}
-              backgroundColor={Colors.CopyIconBg}
-            />
-          </CopyToClipboard>
-        </Animation>
-      )}
-      <CopyToClipboard text={link.link} onCopy={setCopied}>
-        <ShortenedLink fontSize={[24, 26, 30]} pb="2px" light>
-          {removeProtocol(link.link)}
-        </ShortenedLink>
-      </CopyToClipboard>
-    </Animation>
-  );
-
-  return (
-    <Col width={800} maxWidth="100%" px={[3]} flex="0 0 auto" mt={4}>
-      <RowCenterH mb={[4, 48]}>
-        {title}
-        {result}
-      </RowCenterH>
-      <Flex
-        as="form"
-        id="shortenerform"
-        width={1}
-        alignItems="center"
-        justifyContent="center"
-        style={{ position: "relative" }}
-        onSubmit={onSubmit}
-      >
-        <TextInput
-          {...text("target")}
-          placeholder="Paste your long URL"
-          placeholderSize={[16, 17, 18]}
-          fontSize={[18, 20, 22]}
-          aria-label="target"
-          width={1}
-          height={[58, 64, 72]}
-          px={0}
-          pr={[48, 84]}
-          pl={[32, 40]}
-          autoFocus
-          data-lpignore
-        />
-        <SubmitIconWrapper onClick={onSubmit} role="button" aria-label="submit">
-          <Icon
-            name={loading ? "spinner" : "send"}
-            size={[22, 26, 28]}
-            fill={loading ? "none" : "#aaa"}
-            stroke={loading ? Colors.Spinner : "none"}
-            mb={1}
-            mr={1}
-          />
-        </SubmitIconWrapper>
-      </Flex>
-      {message.text && (
-        <Text color={message.color} mt={24} mb={1} textAlign="center">
-          {message.text}
-        </Text>
-      )}
-      <Checkbox
-        {...raw({
-          name: "showAdvanced",
-          onChange: () => {
-            if (!isAuthenticated) {
-              setMessage(
-                "You need to log in or sign up to use advanced options."
-              );
-              return false;
-            }
-            return !formState.values.showAdvanced;
-          }
-        })}
-        checked={formState.values.showAdvanced}
-        label="Show advanced options"
-        mt={[3, 24]}
-        alignSelf="flex-start"
-      />
-      {formState.values.showAdvanced && (
-        <div>
-          <Flex mt={4} flexDirection={["column", "row"]}>
-            <Col mb={[3, 0]}>
-              <Text
-                as="label"
-                {...label("domain")}
-                fontSize={[14, 15]}
-                mb={2}
-                bold
-              >
-                Domain:
-              </Text>
-              <Select
-                {...select("domain")}
-                data-lpignore
-                pl={[3, 24]}
-                pr={[3, 24]}
-                fontSize={[14, 15]}
-                height={[40, 44]}
-                width={[1, 210, 240]}
-                options={[
-                  { key: defaultDomain, value: "" },
-                  ...domains.map((d) => ({
-                    key: d.address,
-                    value: d.address
-                  }))
-                ]}
-              />
-            </Col>
-            <Col mb={[3, 0]} ml={[0, 24]}>
-              <Text
-                as="label"
-                {...label("customurl")}
-                fontSize={[14, 15]}
-                mb={2}
-                bold
-              >
-                {formState.values.domain || defaultDomain}/
-              </Text>
-              <TextInput
-                {...text("customurl")}
-                placeholder="Custom address..."
-                autocomplete="off"
-                data-lpignore
-                pl={[3, 24]}
-                pr={[3, 24]}
-                placeholderSize={[13, 14]}
-                fontSize={[14, 15]}
-                height={[40, 44]}
-                width={[1, 210, 240]}
-              />
-            </Col>
-            <Col ml={[0, 24]}>
-              <Text
-                as="label"
-                {...label("password")}
-                fontSize={[14, 15]}
-                mb={2}
-                bold
-              >
-                Password:
-              </Text>
-              <TextInput
-                {...password("password")}
-                placeholder="Password..."
-                autocomplete="off"
-                data-lpignore
-                pl={[3, 24]}
-                pr={[3, 24]}
-                placeholderSize={[13, 14]}
-                fontSize={[14, 15]}
-                height={[40, 44]}
-                width={[1, 210, 240]}
-              />
-            </Col>
-          </Flex>
-          <Flex mt={[3]} flexDirection={["column", "row"]}>
-            <Col mb={[3, 0]}>
-              <Text
-                as="label"
-                {...label("expire_in")}
-                fontSize={[14, 15]}
-                mb={2}
-                bold
-              >
-                Expire in:
-              </Text>
-              <TextInput
-                {...text("expire_in")}
-                placeholder="2 minutes/hours/days"
-                data-lpignore
-                pl={[3, 24]}
-                pr={[3, 24]}
-                placeholderSize={[13, 14]}
-                fontSize={[14, 15]}
-                height={[40, 44]}
-                width={[1, 210, 240]}
-                maxWidth="100%"
-              />
-            </Col>
-            <Col width={[1, 2 / 3]} ml={[0, 26]}>
-              <Text
-                as="label"
-                {...label("description")}
-                fontSize={[14, 15]}
-                mb={2}
-                bold
-              >
-                Description:
-              </Text>
-              <TextInput
-                {...text("description")}
-                placeholder="Description"
-                data-lpignore
-                pl={[3, 24]}
-                pr={[3, 24]}
-                placeholderSize={[13, 14]}
-                fontSize={[14, 15]}
-                height={[40, 44]}
-                width={1}
-                maxWidth="100%"
-              />
-            </Col>
-          </Flex>
-        </div>
-      )}
-    </Col>
-  );
-};
-
-export default Shortener;

+ 0 - 84
client/components/Table.ts

@@ -1,84 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import styled, { css } from "styled-components";
-import { ifProp, prop } from "styled-tools";
-
-import { Colors } from "../consts";
-
-const Table = styled(Flex)<{ scrollWidth?: string }>`
-  background-color: white;
-  border-radius: 12px;
-  box-shadow: 0 6px 15px ${Colors.TableShadow};
-  text-align: center;
-  overflow: auto;
-
-  tr,
-  th,
-  td,
-  tbody,
-  thead,
-  tfoot {
-    display: flex;
-    overflow: hidden;
-  }
-
-  tbody,
-  thead,
-  tfoot {
-    flex-direction: column;
-  }
-
-  tr {
-    border-bottom: 1px solid ${Colors.TableHeadBorder};
-  }
-
-  tbody {
-    border-bottom-right-radius: 12px;
-    border-bottom-left-radius: 12px;
-    overflow: hidden;
-  }
-
-  tbody + tfoot {
-    border: none;
-  }
-
-  tbody tr:hover {
-    background-color: ${Colors.TableRowHover};
-  }
-
-  thead {
-    background-color: ${Colors.TableHeadBg};
-    border-top-right-radius: 12px;
-    border-top-left-radius: 12px;
-    font-weight: bold;
-
-    tr {
-      border-bottom: 1px solid ${Colors.TableBorder};
-    }
-  }
-
-  tfoot {
-    background-color: ${Colors.TableHeadBg};
-    border-bottom-right-radius: 12px;
-    border-bottom-left-radius: 12px;
-  }
-
-  ${ifProp(
-    "scrollWidth",
-    css`
-      thead,
-      tbody,
-      tfoot {
-        min-width: ${prop("scrollWidth")};
-      }
-    `
-  )}
-`;
-
-Table.defaultProps = {
-  as: "table",
-  flex: "1 1 auto",
-  flexDirection: "column",
-  width: 1
-};
-
-export default Table;

+ 0 - 67
client/components/Text.tsx

@@ -1,67 +0,0 @@
-import React from "react";
-import { switchProp, ifNotProp, ifProp } from "styled-tools";
-import { Box, BoxProps } from "rebass/styled-components";
-import styled, { css } from "styled-components";
-
-import { FC, CSSProperties } from "react";
-import { Colors } from "../consts";
-
-interface Props extends Omit<BoxProps, "as"> {
-  as?: string;
-  htmlFor?: string;
-  light?: boolean;
-  normal?: boolean;
-  bold?: boolean;
-  style?: CSSProperties;
-}
-const Text: FC<Props> = styled(Box)<Props>`
-  font-weight: 400;
-  ${ifNotProp(
-    "fontSize",
-    css`
-      font-size: ${switchProp("a", {
-        p: "1rem",
-        h1: "1.802em",
-        h2: "1.602em",
-        h3: "1.424em",
-        h4: "1.266em",
-        h5: "1.125em"
-      })};
-    `
-  )}
-
-  ${ifProp(
-    "light",
-    css`
-      font-weight: 300;
-    `
-  )}
-
-  ${ifProp(
-    "normal",
-    css`
-      font-weight: 400;
-    `
-  )}
-
-  ${ifProp(
-    "bold",
-    css`
-      font-weight: 700;
-    `
-  )}
-`;
-
-Text.defaultProps = {
-  color: Colors.Text
-};
-
-export default Text;
-
-export const H1: FC<Props> = (props) => <Text as="h1" {...props} />;
-export const H2: FC<Props> = (props) => <Text as="h2" {...props} />;
-export const H3: FC<Props> = (props) => <Text as="h3" {...props} />;
-export const H4: FC<Props> = (props) => <Text as="h4" {...props} />;
-export const H5: FC<Props> = (props) => <Text as="h5" {...props} />;
-export const H6: FC<Props> = (props) => <Text as="h6" {...props} />;
-export const Span: FC<Props> = (props) => <Text as="span" {...props} />;

+ 0 - 14
client/components/Tooltip.tsx

@@ -1,14 +0,0 @@
-import ReactTooltip from "react-tooltip";
-import styled from "styled-components";
-
-const Tooltip = styled(ReactTooltip)`
-  padding: 3px 7px;
-  border-radius: 4px;
-  font-size: 11px;
-`;
-
-Tooltip.defaultProps = {
-  effect: "solid"
-};
-
-export default Tooltip;

+ 0 - 62
client/consts/consts.ts

@@ -1,62 +0,0 @@
-import getConfig from "next/config";
-
-const { publicRuntimeConfig } = getConfig();
-
-export const DISALLOW_ANONYMOUS_LINKS =
-  publicRuntimeConfig.DISALLOW_ANONYMOUS_LINKS === "true";
-
-export const DISALLOW_REGISTRATION =
-  publicRuntimeConfig.DISALLOW_REGISTRATION === "true";
-
-export enum APIv2 {
-  AuthLogin = "/api/v2/auth/login",
-  AuthSignup = "/api/v2/auth/signup",
-  AuthRenew = "/api/v2/auth/renew",
-  AuthResetPassword = "/api/v2/auth/reset-password",
-  AuthChangePassword = "/api/v2/auth/change-password",
-  AuthChangeEmail = "/api/v2/auth/change-email",
-  AuthGenerateApikey = "/api/v2/auth/apikey",
-  Users = "/api/v2/users",
-  Domains = "/api/v2/domains",
-  Links = "/api/v2/links"
-}
-
-export enum Colors {
-  Bg = "hsl(206, 12%, 95%)",
-  CheckIcon = "hsl(144, 50%, 60%)",
-  CopyIcon = "hsl(144, 40%, 57%)",
-  CopyIconBg = "hsl(144, 100%, 96%)",
-  Divider = "hsl(200, 20%, 92%)",
-  EditIcon = "hsl(46, 90%, 50%)",
-  EditIconBg = "hsl(46, 100%, 94%)",
-  ExtensionsBg = "hsl(230, 15%, 20%)",
-  FeaturesBg = "hsl(230, 15%, 92%)",
-  Icon = "hsl(200, 35%, 45%)",
-  IconShadow = "hsla(200, 15%, 60%, 0.12)",
-  Map0 = "hsl(200, 15%, 92%)",
-  Map06 = "hsl(261, 46%, 68%)",
-  Map05 = "hsl(261, 46%, 72%)",
-  Map04 = "hsl(261, 46%, 76%)",
-  Map03 = "hsl(261, 46%, 82%)",
-  Map02 = "hsl(261, 46%, 86%)",
-  Map01 = "hsl(261, 46%, 90%)",
-  PieIcon = "hsl(260, 100%, 69%)",
-  PieIconBg = "hsl(260, 100%, 96%)",
-  QrCodeIcon = "hsl(0, 0%, 35%)",
-  QrCodeIconBg = "hsl(0, 0%, 94%)",
-  Spinner = "hsl(200, 15%, 70%)",
-  StatsLastUpdateText = "hsl(200, 14%, 60%)",
-  StatsTotalUnderline = "hsl(200, 35%, 65%)",
-  StopIcon = "hsl(10, 100%, 40%)",
-  StopIconBg = "hsl(10, 100%, 96%)",
-  TableBorder = "hsl(200, 14%, 90%)",
-  TableHeadBg = "hsl(200, 12%, 95%)",
-  TableHeadBorder = "hsl(200, 14%, 94%)",
-  TableRowHover = "hsl(200, 14%, 98%)",
-  TableRowBanned = "hsl(0, 100%, 98%)",
-  TableRowBannedHower = "hsl(0, 100%, 96%)",
-  TableShadow = "hsla(200, 20%, 70%, 0.3)",
-  Text = "hsl(200, 35%, 25%)",
-  TrashIcon = "hsl(0, 100%, 69%)",
-  TrashIconBg = "hsl(0, 100%, 96%)"
-}

+ 0 - 1
client/consts/index.ts

@@ -1 +0,0 @@
-export * from "./consts";

+ 0 - 30
client/helpers/animations.ts

@@ -1,30 +0,0 @@
-import { keyframes } from "styled-components";
-
-export const fadeInVertical = vertical => keyframes`
-  from {
-    opacity: 0;
-    transform: translateY(${vertical});
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-`;
-
-export const fadeIn = keyframes`
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
-`;
-
-export const spin = keyframes`
-  from {
-    transform: rotate(0);
-  }
-  to {
-    transform: rotate(-360deg);
-  }
-`;

+ 0 - 11
client/helpers/recaptcha.js

@@ -1,11 +0,0 @@
-export default function showRecaptcha() {
-  const captcha = document.getElementById('g-recaptcha');
-  if (!captcha) return null;
-  if (!window.grecaptcha || !window.grecaptcha.render) {
-    return setTimeout(() => showRecaptcha(), 200);
-  }
-  if (!captcha.childNodes.length) {
-    window.captchaId = window.grecaptcha.render(captcha);
-  }
-  return null;
-}

+ 0 - 31
client/hooks.ts

@@ -1,31 +0,0 @@
-import { useState } from "react";
-
-const initialMessage = { color: "red", text: "" };
-
-export const useMessage = (timeout?: number) => {
-  const [message, set] = useState(initialMessage);
-
-  const setMessage = (text = "", color = "red") => {
-    set({ text, color });
-
-    if (timeout) {
-      setTimeout(() => set(initialMessage), timeout);
-    }
-  };
-
-  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;
-};

+ 0 - 19
client/module.d.ts

@@ -1,19 +0,0 @@
-import "next";
-import { initializeStore } from "./store";
-
-declare module "*.svg";
-
-declare global {
-  interface Window {
-    GA_INITIALIZED: boolean;
-    grecaptcha: any;
-    isCaptchaReady: boolean;
-    captchaId: boolean;
-  }
-}
-
-declare module "next" {
-  export interface NextPageContext {
-    store: ReturnType<typeof initializeStore>;
-  }
-}

+ 0 - 5
client/next-env.d.ts

@@ -1,5 +0,0 @@
-/// <reference types="next" />
-/// <reference types="next/image-types/global" />
-
-// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.

+ 0 - 81
client/pages/_app.tsx

@@ -1,81 +0,0 @@
-import App, { AppContext } from "next/app";
-import { StoreProvider } from "easy-peasy";
-import getConfig from "next/config";
-import Router from "next/router";
-import decode from "jwt-decode";
-import cookie from "js-cookie";
-import Head from "next/head";
-import React from "react";
-
-import { initializeStore } from "../store";
-import { TokenPayload } from "../types";
-
-const { publicRuntimeConfig } = getConfig();
-
-// TODO: types
-class MyApp extends App<any> {
-  static async getInitialProps({ Component, ctx }: AppContext) {
-    const store = initializeStore();
-    ctx.store = store;
-
-    let pageProps = {};
-    if (Component.getInitialProps) {
-      pageProps = await Component.getInitialProps(ctx);
-    }
-
-    const token =
-      ctx.req && (ctx.req as any).cookies && (ctx.req as any).cookies.token;
-    const tokenPayload: TokenPayload = token ? decode(token) : null;
-
-    if (tokenPayload) {
-      store.dispatch.auth.add(tokenPayload);
-    }
-
-    return { pageProps, tokenPayload, initialState: store.getState() };
-  }
-
-  store: ReturnType<typeof initializeStore>;
-  constructor(props) {
-    super(props);
-    this.store = initializeStore(props.initialState);
-  }
-
-  componentDidMount() {
-    const { loading, auth } = this.store.dispatch;
-    const token = cookie.get("token");
-    const isVerifyEmailPage =
-      typeof window !== "undefined" &&
-      window.location.pathname.includes("verify-email");
-
-    if (token && !isVerifyEmailPage) {
-      auth.renew().catch(() => {
-        auth.logout();
-      });
-    }
-
-    Router.events.on("routeChangeStart", () => loading.show());
-    Router.events.on("routeChangeComplete", () => {
-      loading.hide();
-    });
-    Router.events.on("routeChangeError", () => loading.hide());
-  }
-
-  render() {
-    const { Component, pageProps } = this.props;
-
-    return (
-      <>
-        <Head>
-          <title>
-            {publicRuntimeConfig.SITE_NAME} | Modern Open Source URL shortener.
-          </title>
-        </Head>
-        <StoreProvider store={this.store}>
-          <Component {...pageProps} />
-        </StoreProvider>
-      </>
-    );
-  }
-}
-
-export default MyApp;

+ 0 - 110
client/pages/_document.tsx

@@ -1,110 +0,0 @@
-import Document, { Head, Main, NextScript } from "next/document";
-import { ServerStyleSheet } from "styled-components";
-import getConfig from "next/config";
-import React from "react";
-
-import { Colors } from "../consts";
-
-const { publicRuntimeConfig } = getConfig();
-
-interface Props {
-  styleTags: any;
-}
-
-class AppDocument extends Document<Props> {
-  static async getInitialProps(ctx) {
-    const initialProps = await Document.getInitialProps(ctx);
-    const sheet = new ServerStyleSheet();
-    const page = ctx.renderPage(
-      (App) => (props) => sheet.collectStyles(<App {...props} />)
-    );
-    const styleTags = sheet.getStyleElement();
-    return { ...initialProps, ...page, styleTags };
-  }
-
-  render() {
-    return (
-      <html lang="en">
-        <Head>
-          <meta charSet="utf-8" />
-          <meta
-            name="viewport"
-            content="width=device-width, initial-scale=1, viewport-fit=cover"
-          />
-          <meta
-            name="description"
-            content={`${publicRuntimeConfig.SITE_NAME} is a free and open source URL shortener with custom domains and stats.`}
-          />
-          <link
-            href="https://fonts.googleapis.com/css?family=Nunito:300,400,700&display=optional"
-            rel="stylesheet"
-          />
-          <link rel="icon" sizes="196x196" href="/images/favicon-196x196.png" />
-          <link rel="icon" sizes="32x32" href="/images/favicon-32x32.png" />
-          <link rel="icon" sizes="16x16" href="/images/favicon-16x16.png" />
-          <link rel="apple-touch-icon" href="/images/favicon-196x196.png" />
-          <link rel="mask-icon" href="/images/icon.svg" color="blue" />
-          <link rel="manifest" href="manifest.webmanifest" />
-          <meta name="theme-color" content="#f3f3f3" />
-
-          <meta property="fb:app_id" content="123456789" />
-          <meta
-            property="og:url"
-            content={`https://${publicRuntimeConfig.DEFAULT_DOMAIN}`}
-          />
-          <meta property="og:type" content="website" />
-          <meta property="og:title" content={publicRuntimeConfig.SITE_NAME} />
-          <meta
-            property="og:image"
-            content={`https://${publicRuntimeConfig.DEFAULT_DOMAIN}/images/card.png`}
-          />
-          <meta
-            property="og:description"
-            content="Free & Open Source Modern URL Shortener"
-          />
-          <meta
-            name="twitter:url"
-            content={`https://${publicRuntimeConfig.DEFAULT_DOMAIN}`}
-          />
-          <meta name="twitter:title" content={publicRuntimeConfig.SITE_NAME} />
-          <meta
-            name="twitter:description"
-            content="Free & Open Source Modern URL Shortener"
-          />
-          <meta
-            name="twitter:image"
-            content={`https://${publicRuntimeConfig.DEFAULT_DOMAIN}/images/card.png`}
-          />
-
-          {this.props.styleTags}
-
-          <script
-            dangerouslySetInnerHTML={{
-              __html: `window.recaptchaCallback = function() { window.isCaptchaReady = true; }`
-            }}
-          />
-
-          <script
-            src="https://www.google.com/recaptcha/api.js?render=explicit"
-            async
-            defer
-          />
-        </Head>
-        <body
-          style={{
-            margin: 0,
-            backgroundColor: Colors.Bg,
-            font: '16px/1.45 "Nunito", sans-serif',
-            overflowX: "hidden",
-            color: Colors.Text
-          }}
-        >
-          <Main />
-          <NextScript />
-        </body>
-      </html>
-    );
-  }
-}
-
-export default AppDocument;

+ 0 - 37
client/pages/banned.tsx

@@ -1,37 +0,0 @@
-import getConfig from "next/config";
-import React from "react";
-
-import AppWrapper from "../components/AppWrapper";
-import { H2, H4, Span } from "../components/Text";
-import Footer from "../components/Footer";
-import ALink from "../components/ALink";
-import { Col } from "../components/Layout";
-
-const { publicRuntimeConfig } = getConfig();
-
-const BannedPage = () => {
-  return (
-    <AppWrapper>
-      <Col flex="1 1 100%" alignItems="center">
-        <H2 textAlign="center" my={3} normal>
-          Link has been banned and removed because of{" "}
-          <Span style={{ borderBottom: "1px dotted rgba(0, 0, 0, 0.4)" }} bold>
-            malware or scam
-          </Span>
-          .
-        </H2>
-        <H4 textAlign="center" normal>
-          If you noticed a malware/scam link shortened by{" "}
-          {publicRuntimeConfig.SITE_NAME},{" "}
-          <ALink href="/report" title="Send report" isNextLink>
-            send us a report
-          </ALink>
-          .
-        </H4>
-      </Col>
-      <Footer />
-    </AppWrapper>
-  );
-};
-
-export default BannedPage;

+ 0 - 38
client/pages/index.tsx

@@ -1,38 +0,0 @@
-import React from "react";
-import Router from "next/router";
-
-import { DISALLOW_ANONYMOUS_LINKS } from "../consts";
-import NeedToLogin from "../components/NeedToLogin";
-import Extensions from "../components/Extensions";
-import LinksTable from "../components/LinksTable";
-import AppWrapper from "../components/AppWrapper";
-import Shortener from "../components/Shortener";
-import Features from "../components/Features";
-import Footer from "../components/Footer";
-import { useStoreState } from "../store";
-
-const Homepage = () => {
-  const isAuthenticated = useStoreState(s => s.auth.isAuthenticated);
-
-  if (
-    !isAuthenticated &&
-    DISALLOW_ANONYMOUS_LINKS &&
-    typeof window !== "undefined"
-  ) {
-    Router.push("/login");
-    return null;
-  }
-
-  return (
-    <AppWrapper>
-      <Shortener />
-      {!isAuthenticated && <NeedToLogin />}
-      {isAuthenticated && <LinksTable />}
-      <Features />
-      <Extensions />
-      <Footer />
-    </AppWrapper>
-  );
-};
-
-export default Homepage;

+ 0 - 185
client/pages/login.tsx

@@ -1,185 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import React, { useEffect, useState } from "react";
-import { Flex } from "rebass/styled-components";
-import emailValidator from "email-validator";
-import styled from "styled-components";
-import Router from "next/router";
-import axios from "axios";
-
-import { useStoreState, useStoreActions } from "../store";
-import { APIv2, DISALLOW_REGISTRATION } from "../consts";
-import { ColCenterV } from "../components/Layout";
-import AppWrapper from "../components/AppWrapper";
-import { TextInput } from "../components/Input";
-import { fadeIn } from "../helpers/animations";
-import { Button } from "../components/Button";
-import Text, { H2 } from "../components/Text";
-import ALink from "../components/ALink";
-import Icon from "../components/Icon";
-
-const LoginForm = styled(Flex).attrs({
-  as: "form",
-  flexDirection: "column"
-})`
-  animation: ${fadeIn} 0.8s ease-out;
-`;
-
-const Email = styled.span`
-  font-weight: normal;
-  color: #512da8;
-  border-bottom: 1px dotted #999;
-`;
-
-const LoginPage = () => {
-  const { isAuthenticated } = useStoreState((s) => s.auth);
-  const login = useStoreActions((s) => s.auth.login);
-  const [error, setError] = useState("");
-  const [verifying, setVerifying] = useState(false);
-  const [loading, setLoading] = useState({ login: false, signup: false });
-  const [formState, { email, password, label }] = useFormState<{
-    email: string;
-    password: string;
-  }>(null, { withIds: true });
-
-  useEffect(() => {
-    if (isAuthenticated) Router.push("/");
-  }, [isAuthenticated]);
-
-  function onSubmit(type: "login" | "signup") {
-    return async (e) => {
-      e.preventDefault();
-      const { email, password } = formState.values;
-
-      if (loading.login || loading.signup) return null;
-
-      if (!email) {
-        return setError("Email address must not be empty.");
-      }
-
-      if (!emailValidator.validate(email)) {
-        return setError("Email address is not valid.");
-      }
-
-      if (password.trim().length < 8) {
-        return setError("Password must be at least 8 chars long.");
-      }
-
-      setError("");
-
-      if (type === "login") {
-        setLoading((s) => ({ ...s, login: true }));
-        try {
-          await login(formState.values);
-          Router.push("/");
-        } catch (error) {
-          setError(error.response.data.error);
-        }
-      }
-
-      if (type === "signup" && !DISALLOW_REGISTRATION) {
-        setLoading((s) => ({ ...s, signup: true }));
-        try {
-          await axios.post(APIv2.AuthSignup, { email, password });
-          setVerifying(true);
-        } catch (error) {
-          setError(error.response.data.error);
-        }
-      }
-
-      setLoading({ login: false, signup: false });
-    };
-  }
-
-  if (isAuthenticated) {
-    return null;
-  }
-
-  return (
-    <AppWrapper>
-      <ColCenterV maxWidth="100%" px={3} flex="0 0 auto" mt={4}>
-        {verifying ? (
-          <H2 textAlign="center" light>
-            A verification email has been sent to{" "}
-            <Email>{formState.values.email}</Email>.
-          </H2>
-        ) : (
-          <LoginForm id="login-form" onSubmit={onSubmit("login")}>
-            <Text {...label("email")} as="label" mb={2} bold>
-              Email address:
-            </Text>
-            <TextInput
-              {...email("email")}
-              placeholder="Email address..."
-              height={[56, 64, 72]}
-              fontSize={[15, 16]}
-              px={[4, 40]}
-              mb={[24, 4]}
-              width={[300, 400]}
-              maxWidth="100%"
-              autoFocus
-            />
-            <Text {...label("password")} as="label" mb={2} bold>
-              Password{!DISALLOW_REGISTRATION ? " (min chars: 8)" : ""}:
-            </Text>
-            <TextInput
-              {...password("password")}
-              placeholder="Password..."
-              px={[4, 40]}
-              height={[56, 64, 72]}
-              fontSize={[15, 16]}
-              width={[300, 400]}
-              maxWidth="100%"
-              mb={[24, 4]}
-            />
-            <Flex justifyContent="center">
-              <Button
-                flex="1 1 auto"
-                mr={!DISALLOW_REGISTRATION ? ["8px", 16] : 0}
-                height={[44, 56]}
-                onClick={onSubmit("login")}
-              >
-                <Icon
-                  name={loading.login ? "spinner" : "login"}
-                  stroke="white"
-                  mr={2}
-                />
-                Log in
-              </Button>
-              {!DISALLOW_REGISTRATION && (
-                <Button
-                  flex="1 1 auto"
-                  ml={["8px", 16]}
-                  height={[44, 56]}
-                  color="purple"
-                  onClick={onSubmit("signup")}
-                >
-                  <Icon
-                    name={loading.signup ? "spinner" : "signup"}
-                    stroke="white"
-                    mr={2}
-                  />
-                  Sign up
-                </Button>
-              )}
-            </Flex>
-            <ALink
-              href="/reset-password"
-              title="Forget password"
-              fontSize={14}
-              alignSelf="flex-start"
-              my={16}
-              isNextLink
-            >
-              Forgot your password?
-            </ALink>
-            <Text color="red" mt={1} normal>
-              {error}
-            </Text>
-          </LoginForm>
-        )}
-      </ColCenterV>
-    </AppWrapper>
-  );
-};
-
-export default LoginPage;

+ 0 - 19
client/pages/logout.tsx

@@ -1,19 +0,0 @@
-import React, { FC, useEffect } from "react";
-import Router from "next/router";
-
-import { useStoreActions } from "../store";
-
-const LogoutPage: FC = () => {
-  const logout = useStoreActions((s) => s.auth.logout);
-  const reset = useStoreActions((s) => s.reset);
-
-  useEffect(() => {
-    logout();
-    reset();
-    Router.push("/");
-  }, [logout, reset]);
-
-  return <div />;
-};
-
-export default LogoutPage;

+ 0 - 98
client/pages/protected/[id].tsx

@@ -1,98 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import React, { useState } from "react";
-import { NextPage } from "next";
-import { useRouter } from "next/router";
-import axios from "axios";
-
-import AppWrapper from "../../components/AppWrapper";
-import { TextInput } from "../../components/Input";
-import { Button } from "../../components/Button";
-import Text, { H2 } from "../../components/Text";
-import { Col } from "../../components/Layout";
-import Icon from "../../components/Icon";
-import { APIv2 } from "../../consts";
-
-interface Props {
-  protectedLink?: string;
-}
-
-const ProtectedPage: NextPage<Props> = () => {
-  const router = useRouter();
-  const [loading, setLoading] = useState(false);
-  const [formState, { password }] = useFormState<{ password: string }>();
-  const [error, setError] = useState<string>();
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    const { password } = formState.values;
-
-    if (!password) {
-      return setError("Password must not be empty.");
-    }
-
-    setError("");
-    setLoading(true);
-    try {
-      const { data } = await axios.post(
-        `${APIv2.Links}/${router.query.id}/protected`,
-        {
-          password
-        }
-      );
-      window.location.replace(data.target);
-    } catch ({ response }) {
-      setError(response.data.error);
-    }
-    setLoading(false);
-  };
-
-  return (
-    <AppWrapper>
-      {!router.query.id ? (
-        <H2 my={4} light>
-          404 | Link could not be found.
-        </H2>
-      ) : (
-        <Col width={500} maxWidth="97%">
-          <H2 my={3} bold>
-            Protected link
-          </H2>
-          <Text mb={4}>Enter the password to be redirected to the link.</Text>
-          <Flex
-            as="form"
-            alignItems="center"
-            onSubmit={onSubmit}
-            style={{ position: "relative" }}
-          >
-            <TextInput
-              {...password("password")}
-              placeholder="Password"
-              autocomplete="off"
-              height={[44, 54]}
-              width={[1, 1 / 2]}
-              mr={3}
-              autoFocus
-              required
-            />
-            <Button type="submit" height={[40, 44]}>
-              {loading && <Icon name={"spinner"} stroke="white" mr={2} />}
-              Go
-            </Button>
-          </Flex>
-          <Text fontSize={14} color="red" mt={3} normal>
-            {error}
-          </Text>
-        </Col>
-      )}
-    </AppWrapper>
-  );
-};
-
-ProtectedPage.getInitialProps = async ({ req }) => {
-  return {
-    protectedLink: req && (req as any).protectedLink
-  };
-};
-
-export default ProtectedPage;

+ 0 - 84
client/pages/report.tsx

@@ -1,84 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import { Flex } from "rebass/styled-components";
-import React, { useState } from "react";
-import axios from "axios";
-
-import Text, { H2, Span } from "../components/Text";
-import AppWrapper from "../components/AppWrapper";
-import { TextInput } from "../components/Input";
-import { Button } from "../components/Button";
-import { Col } from "../components/Layout";
-import Icon from "../components/Icon";
-import { useMessage } from "../hooks";
-import { APIv2 } from "../consts";
-
-import getConfig from "next/config";
-
-const { publicRuntimeConfig } = getConfig();
-
-const ReportPage = () => {
-  const [formState, { text }] = useFormState<{ url: string }>();
-  const [loading, setLoading] = useState(false);
-  const [message, setMessage] = useMessage(5000);
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    setLoading(true);
-    setMessage();
-    try {
-      await axios.post(`${APIv2.Links}/report`, { link: formState.values.url });
-      setMessage("Thanks for the report, we'll take actions shortly.", "green");
-      formState.clear();
-    } catch (error) {
-      setMessage(error?.response?.data?.error || "Couldn't send report.");
-    }
-
-    setLoading(false);
-  };
-
-  return (
-    <AppWrapper>
-      <Col width={600} maxWidth="97%" alignItems="flex-start">
-        <H2 my={3} bold>
-          Report abuse
-        </H2>
-        <Text mb={3}>
-          Report abuses, malware and phishing links to the below email address
-          or use the form. We will take actions shortly.
-        </Text>
-        <Text mb={4}>
-          {(publicRuntimeConfig.REPORT_EMAIL || "").replace("@", "[at]")}
-        </Text>
-        <Text mb={3}>
-          <Span bold>URL containing malware/scam:</Span>
-        </Text>
-        <Flex
-          as="form"
-          flexDirection={["column", "row"]}
-          alignItems={["flex-start", "center"]}
-          justifyContent="flex-start"
-          onSubmit={onSubmit}
-        >
-          <TextInput
-            {...text("url")}
-            placeholder={`${publicRuntimeConfig.DEFAULT_DOMAIN}/example`}
-            height={[44, 54]}
-            width={[1, 1 / 2]}
-            flex="0 0 auto"
-            mr={3}
-            required
-          />
-          <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>
-        <Text fontSize={14} mt={3} color={message.color}>
-          {message.text}
-        </Text>
-      </Col>
-    </AppWrapper>
-  );
-};
-
-export default ReportPage;

+ 0 - 110
client/pages/reset-password.tsx

@@ -1,110 +0,0 @@
-import { useFormState } from "react-use-form-state";
-import React, { useEffect, useState } from "react";
-import { Flex } from "rebass/styled-components";
-import Router from "next/router";
-import decode from "jwt-decode";
-import { NextPage } from "next";
-import cookie from "js-cookie";
-import axios from "axios";
-
-import { useStoreState, useStoreActions } from "../store";
-import AppWrapper from "../components/AppWrapper";
-import { TextInput } from "../components/Input";
-import { Button } from "../components/Button";
-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 { APIv2 } from "../consts";
-
-interface Props {
-  token?: string;
-}
-
-const ResetPassword: NextPage<Props> = ({ token }) => {
-  const auth = useStoreState((s) => s.auth);
-  const addAuth = useStoreActions((s) => s.auth.add);
-  const [loading, setLoading] = useState(false);
-  const [message, setMessage] = useMessage();
-  const [formState, { email, label }] = useFormState<{ email: string }>(null, {
-    withIds: true
-  });
-
-  useEffect(() => {
-    if (auth.isAuthenticated) {
-      Router.push("/settings");
-    }
-
-    if (token) {
-      cookie.set("token", token, { expires: 7 });
-      const decoded: TokenPayload = decode(token);
-      addAuth(decoded);
-      Router.push("/settings");
-    }
-  }, [auth, token, addAuth]);
-
-  const onSubmit = async (e) => {
-    e.preventDefault();
-    if (!formState.validity.email) return;
-
-    setLoading(true);
-    setMessage();
-    try {
-      await axios.post(APIv2.AuthResetPassword, {
-        email: formState.values.email
-      });
-      setMessage("Reset password email has been sent.", "green");
-    } catch (error) {
-      setMessage(error?.response?.data?.error || "Couldn't reset password.");
-    }
-    setLoading(false);
-  };
-
-  // FIXME: make a container for width
-  return (
-    <AppWrapper>
-      <Col width={600} maxWidth="100%" px={3}>
-        <H2 my={3} bold>
-          Reset password
-        </H2>
-        <Text mb={4}>
-          If you forgot you password you can use the form below to get reset
-          password link.
-        </Text>
-        <Text {...label("homepage")} as="label" mt={2} fontSize={[15, 16]} bold>
-          Email address
-        </Text>
-        <Flex
-          as="form"
-          alignItems="center"
-          justifyContent="flex-start"
-          onSubmit={onSubmit}
-        >
-          <TextInput
-            {...email("email")}
-            placeholder="Email address..."
-            height={[44, 54]}
-            width={[1, 1 / 2]}
-            mr={3}
-            autoFocus
-            required
-          />
-          <Button type="submit" height={[40, 44]} my={3}>
-            {loading && <Icon name={"spinner"} stroke="white" mr={2} />}
-            Reset password
-          </Button>
-        </Flex>
-        <Text fontSize={14} color={message.color} mt={2} normal>
-          {message.text}
-        </Text>
-      </Col>
-    </AppWrapper>
-  );
-};
-
-ResetPassword.getInitialProps = async (ctx) => {
-  return { token: ctx.req && (ctx.req as any).token };
-};
-
-export default ResetPassword;

+ 0 - 45
client/pages/settings.tsx

@@ -1,45 +0,0 @@
-import { NextPage } from "next";
-import React from "react";
-
-import SettingsDeleteAccount from "../components/Settings/SettingsDeleteAccount";
-import SettingsChangeEmail from "../components/Settings/SettingsChangeEmail";
-import SettingsPassword from "../components/Settings/SettingsPassword";
-import SettingsDomain from "../components/Settings/SettingsDomain";
-import SettingsApi from "../components/Settings/SettingsApi";
-import AppWrapper from "../components/AppWrapper";
-import { H1, Span } from "../components/Text";
-import Divider from "../components/Divider";
-import { Col } from "../components/Layout";
-import Footer from "../components/Footer";
-import { useStoreState } from "../store";
-
-const SettingsPage: NextPage = () => {
-  const email = useStoreState(s => s.auth.email);
-
-  return (
-    <AppWrapper>
-      <Col width={600} maxWidth="90%" alignItems="flex-start" pb={80} mt={4}>
-        <H1 alignItems="center" fontSize={[24, 28]} light>
-          Welcome,{" "}
-          <Span pb="2px" style={{ borderBottom: "2px dotted #999" }}>
-            {email}
-          </Span>
-          .
-        </H1>
-        <Divider mt={4} mb={48} />
-        <SettingsDomain />
-        <Divider mt={4} mb={48} />
-        <SettingsApi />
-        <Divider mt={4} mb={48} />
-        <SettingsPassword />
-        <Divider mt={4} mb={48} />
-        <SettingsChangeEmail />
-        <Divider mt={4} mb={48} />
-        <SettingsDeleteAccount />
-      </Col>
-      <Footer />
-    </AppWrapper>
-  );
-};
-
-export default SettingsPage;

+ 0 - 207
client/pages/stats.tsx

@@ -1,207 +0,0 @@
-import { Box, Flex } from "rebass/styled-components";
-import React, { useState, useEffect } from "react";
-import formatDate from "date-fns/format";
-import { NextPage } from "next";
-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, Map } from "../components/Charts";
-import PageLoading from "../components/PageLoading";
-import AppWrapper from "../components/AppWrapper";
-import Divider from "../components/Divider";
-import { APIv2, Colors } from "../consts";
-import { useStoreState } from "../store";
-import ALink from "../components/ALink";
-import Icon from "../components/Icon";
-
-interface Props {
-  id?: string;
-}
-
-const StatsPage: NextPage<Props> = ({ id }) => {
-  const { isAuthenticated } = useStoreState((s) => s.auth);
-  const [loading, setLoading] = useState(true);
-  const [error, setError] = useState(false);
-  const [data, setData] = useState<Record<string, any> | undefined>();
-  const [period, setPeriod] = useState("lastDay");
-
-  const stats = data && data[period];
-
-  useEffect(() => {
-    if (!id || !isAuthenticated) return;
-    axios
-      .get(`${APIv2.Links}/${id}/stats`, getAxiosConfig())
-      .then(({ data }) => {
-        setLoading(false);
-        setError(!data);
-        setData(data);
-      })
-      .catch(() => {
-        setLoading(false);
-        setError(true);
-      });
-  }, [id, isAuthenticated]);
-
-  let errorMessage;
-
-  if (!isAuthenticated) {
-    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) {
-    errorMessage = (
-      <Flex mt={3}>
-        <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
-        <H2>Couldn&apos;t get stats.</H2>
-      </Flex>
-    );
-  }
-
-  const loader = loading && <PageLoading />;
-
-  const total = stats && stats.views.reduce((sum, view) => sum + view, 0);
-  const periodText = period.includes("last")
-    ? `the last ${period.replace("last", "").toLocaleLowerCase()}`
-    : "all time";
-
-  return (
-    <AppWrapper>
-      {errorMessage ||
-        loader ||
-        (data && (
-          <Col width={1200} maxWidth="95%" alignItems="stretch" m="40px 0">
-            <Flex justifyContent="space-between" alignItems="center" mb={3}>
-              <H1 fontSize={[18, 20, 24]} light>
-                Stats for:{" "}
-                <ALink href={data.link} title="Short link">
-                  {removeProtocol(data.link)}
-                </ALink>
-              </H1>
-              <Text fontSize={[13, 14]} textAlign="right">
-                {data.target.length > 80
-                  ? `${data.target.split("").slice(0, 80).join("")}...`
-                  : data.target}
-              </Text>
-            </Flex>
-            <Col
-              backgroundColor="white"
-              style={{
-                borderRadius: 12,
-                boxShadow: "0 6px 15px hsla(200, 20%, 70%, 0.3)",
-                overflow: "hidden"
-              }}
-            >
-              <RowCenterV
-                flex="1 1 auto"
-                backgroundColor={Colors.TableHeadBg}
-                justifyContent="space-between"
-                py={[3, 3, 24]}
-                px={[3, 4]}
-              >
-                <H4>
-                  Total clicks: <Span bold>{data.total}</Span>
-                </H4>
-                <Flex>
-                  {[
-                    ["allTime", "All Time"],
-                    ["lastMonth", "Month"],
-                    ["lastWeek", "Week"],
-                    ["lastDay", "Day"]
-                  ].map(([p, n]) => (
-                    <NavButton
-                      ml={10}
-                      disabled={p === period}
-                      onClick={() => setPeriod(p as any)}
-                      key={p}
-                    >
-                      {n}
-                    </NavButton>
-                  ))}
-                </Flex>
-              </RowCenterV>
-              <Col p={[3, 4]}>
-                <H2 mb={2} light>
-                  <Span
-                    style={{
-                      borderBottom: `1px dotted ${Colors.StatsTotalUnderline}`
-                    }}
-                    bold
-                  >
-                    {total}
-                  </Span>{" "}
-                  tracked clicks in {periodText}.
-                </H2>
-                <Text fontSize={[13, 14]} color={Colors.StatsLastUpdateText}>
-                  Last update in{" "}
-                  {formatDate(new Date(data.updatedAt), "hh:mm aa")}
-                </Text>
-                <Flex width={1} mt={4}>
-                  <Area data={stats.views} period={period} />
-                </Flex>
-                {total > 0 && (
-                  <>
-                    <Divider my={4} />
-                    <Flex width={1}>
-                      <Col flex="1 1 0">
-                        <H2 mb={3} light>
-                          Referrals.
-                        </H2>
-                        <Pie data={stats.stats.referrer} />
-                      </Col>
-                      <Col flex="1 1 0">
-                        <H2 mb={3} light>
-                          Browsers.
-                        </H2>
-                        <Bar data={stats.stats.browser} />
-                      </Col>
-                    </Flex>
-                    <Divider my={4} />
-                    <Flex width={1}>
-                      <Col flex="1 1 0">
-                        <H2 mb={3} light>
-                          Country.
-                        </H2>
-                        <Map data={stats.stats.country} />
-                      </Col>
-                      <Col flex="1 1 0">
-                        <H2 mb={3} light>
-                          OS.
-                        </H2>
-                        <Bar data={stats.stats.os} />
-                      </Col>
-                    </Flex>
-                  </>
-                )}
-              </Col>
-            </Col>
-            <Box alignSelf="center" my={64}>
-              <ALink href="/" title="Back to homepage" forButton isNextLink>
-                <Button>
-                  <Icon name="arrowLeft" stroke="white" mr={2} />
-                  Back to homepage
-                </Button>
-              </ALink>
-            </Box>
-          </Col>
-        ))}
-    </AppWrapper>
-  );
-};
-
-StatsPage.getInitialProps = ({ query }) => {
-  return Promise.resolve(query);
-};
-
-StatsPage.defaultProps = {
-  id: ""
-};
-
-export default StatsPage;

+ 0 - 66
client/pages/terms.tsx

@@ -1,66 +0,0 @@
-import getConfig from "next/config";
-import React from "react";
-
-import AppWrapper from "../components/AppWrapper";
-import { Col } from "../components/Layout";
-
-const { publicRuntimeConfig } = getConfig();
-
-const TermsPage = () => (
-  <AppWrapper>
-    {/* TODO: better container */}
-    <Col width={600} maxWidth="97%" alignItems="flex-start">
-      <h3>{publicRuntimeConfig.SITE_NAME} Terms of Service</h3>
-      <p>
-        By accessing the website at{" "}
-        <a href={`https://${publicRuntimeConfig.DEFAULT_DOMAIN}`}>
-          https://{publicRuntimeConfig.DEFAULT_DOMAIN}
-        </a>
-        , you are agreeing to be bound by these terms of service, all applicable
-        laws and regulations, and agree that you are responsible for compliance
-        with any applicable local laws. If you do not agree with any of these
-        terms, you are prohibited from using or accessing this site. The
-        materials contained in this website are protected by applicable
-        copyright and trademark law.
-      </p>
-      <p>
-        In no event shall {publicRuntimeConfig.SITE_NAME} or its suppliers be
-        liable for any damages (including, without limitation, damages for loss
-        of data or profit, or due to business interruption) arising out of the
-        use or inability to use the materials on{" "}
-        {publicRuntimeConfig.DEFAULT_DOMAIN} website, even if{" "}
-        {publicRuntimeConfig.SITE_NAME} or a {publicRuntimeConfig.SITE_NAME}{" "}
-        authorized representative has been notified orally or in writing of the
-        possibility of such damage. Because some jurisdictions do not allow
-        limitations on implied warranties, or limitations of liability for
-        consequential or incidental damages, these limitations may not apply to
-        you.
-      </p>
-      <p>
-        The materials appearing on {publicRuntimeConfig.SITE_NAME} website could
-        include technical, typographical, or photographic errors.{" "}
-        {publicRuntimeConfig.SITE_NAME} does not warrant that any of the
-        materials on its website are accurate, complete or current.{" "}
-        {publicRuntimeConfig.SITE_NAME} may make changes to the materials
-        contained on its website at any time without notice. However{" "}
-        {publicRuntimeConfig.SITE_NAME} does not make any commitment to update
-        the materials.
-      </p>
-      <p>
-        {publicRuntimeConfig.SITE_NAME} has not reviewed all of the sites linked
-        to its website and is not responsible for the contents of any such
-        linked site. The inclusion of any link does not imply endorsement by{" "}
-        {publicRuntimeConfig.SITE_NAME} of the site. Use of any such linked
-        website is at the {"user's"} own risk.
-      </p>
-      <p>
-        {publicRuntimeConfig.SITE_NAME} may revise these terms of service for
-        its website at any time without notice. By using this website you are
-        agreeing to be bound by the then current version of these terms of
-        service.
-      </p>
-    </Col>
-  </AppWrapper>
-);
-
-export default TermsPage;

+ 0 - 32
client/pages/url-info.tsx

@@ -1,32 +0,0 @@
-import { useRouter } from "next/router";
-import React from "react";
-
-import AppWrapper from "../components/AppWrapper";
-import Footer from "../components/Footer";
-import { H2, H4 } from "../components/Text";
-import { Col } from "../components/Layout";
-
-const UrlInfoPage = () => {
-  const { query } = useRouter();
-  return (
-    <AppWrapper>
-      {!query.target ? (
-        <H2 my={4} light>
-          404 | Link could not be found.
-        </H2>
-      ) : (
-        <>
-          <Col flex="1 1 100%" alignItems="center">
-            <H2 my={3} light>
-              Target:
-            </H2>
-            <H4 bold>{query.target}</H4>
-          </Col>
-          <Footer />
-        </>
-      )}
-    </AppWrapper>
-  );
-};
-
-export default UrlInfoPage;

+ 0 - 55
client/pages/verify-email.tsx

@@ -1,55 +0,0 @@
-import React, { useEffect } from "react";
-import { Flex } from "rebass/styled-components";
-import decode from "jwt-decode";
-import { NextPage } from "next";
-import cookie from "js-cookie";
-
-import { useStoreActions } from "../store";
-import AppWrapper from "../components/AppWrapper";
-import { H2 } from "../components/Text";
-import { TokenPayload } from "../types";
-import Icon from "../components/Icon";
-import { Colors } from "../consts";
-import Footer from "../components/Footer";
-
-interface Props {
-  token?: string;
-}
-
-const VerifyEmail: NextPage<Props> = ({ token }) => {
-  const addAuth = useStoreActions((s) => s.auth.add);
-
-  useEffect(() => {
-    if (token) {
-      cookie.set("token", token, { expires: 7 });
-      const decoded: TokenPayload = decode(token);
-      addAuth(decoded);
-    }
-  }, [addAuth, token]);
-
-  return (
-    <AppWrapper>
-      <Flex flex="1 1 100%" justifyContent="center" mt={4}>
-        <Icon
-          name={token ? "check" : "x"}
-          size={26}
-          stroke={token ? Colors.CheckIcon : Colors.TrashIcon}
-          mr={3}
-          mt={1}
-        />
-        <H2 textAlign="center" normal>
-          {token
-            ? "Email address verified successfully."
-            : "Couldn't verify the email address."}
-        </H2>
-      </Flex>
-      <Footer />
-    </AppWrapper>
-  );
-};
-
-VerifyEmail.getInitialProps = async (ctx) => {
-  return { token: (ctx?.req as any)?.token };
-};
-
-export default VerifyEmail;

+ 0 - 84
client/pages/verify.tsx

@@ -1,84 +0,0 @@
-import { Flex } from "rebass/styled-components";
-import React, { useEffect } from "react";
-import styled from "styled-components";
-import decode from "jwt-decode";
-import cookie from "js-cookie";
-
-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 { Colors } from "../consts";
-import ALink from "../components/ALink";
-
-interface Props {
-  token?: string;
-}
-
-const MessageWrapper = styled(Flex).attrs({
-  justifyContent: "center",
-  alignItems: "center",
-  my: 32
-})``;
-
-const Message = styled.p`
-  font-size: 24px;
-  font-weight: 300;
-
-  @media only screen and (max-width: 768px) {
-    font-size: 18px;
-  }
-`;
-
-const Verify: NextPage<Props> = ({ token }) => {
-  const addAuth = useStoreActions((s) => s.auth.add);
-
-  useEffect(() => {
-    if (token) {
-      cookie.set("token", token, { expires: 7 });
-      const payload: TokenPayload = decode(token);
-      addAuth(payload);
-    }
-  }, [token, addAuth]);
-
-  return (
-    <AppWrapper>
-      {token ? (
-        <Col alignItems="center">
-          <MessageWrapper>
-            <Icon name="check" size={32} mr={3} stroke={Colors.CheckIcon} />
-            <Message>Your account has been verified successfully!</Message>
-          </MessageWrapper>
-          <ALink href="/" forButton isNextLink>
-            <Button>
-              <Icon name="arrowLeft" stroke="white" mr={2} />
-              Back to homepage
-            </Button>
-          </ALink>
-        </Col>
-      ) : (
-        <Col alignItems="center">
-          <MessageWrapper>
-            <Icon name="x" size={32} mr={3} stroke={Colors.TrashIcon} />
-            <Message>Invalid verification.</Message>
-          </MessageWrapper>
-          <ALink href="/login" forButton isNextLink>
-            <Button color="purple">
-              <Icon name="arrowLeft" stroke="white" mr={2} />
-              Back to signup
-            </Button>
-          </ALink>
-        </Col>
-      )}
-    </AppWrapper>
-  );
-};
-
-Verify.getInitialProps = async ({ req }) => {
-  return { token: req && (req as any).token }; // TODO: types bro
-};
-
-export default Verify;

+ 0 - 51
client/store/auth.ts

@@ -1,51 +0,0 @@
-import { action, Action, thunk, Thunk, computed, Computed } from "easy-peasy";
-import decode from "jwt-decode";
-import cookie from "js-cookie";
-import axios from "axios";
-
-import { TokenPayload } from "../types";
-import { APIv2 } from "../consts";
-import { getAxiosConfig } from "../utils";
-
-export interface Auth {
-  domain?: string;
-  email: string;
-  isAdmin: boolean;
-  isAuthenticated: Computed<Auth, boolean>;
-  add: Action<Auth, TokenPayload>;
-  logout: Action<Auth>;
-  login: Thunk<Auth, { email: string; password: string }>;
-  renew: Thunk<Auth>;
-}
-
-export const auth: Auth = {
-  domain: null,
-  email: null,
-  isAdmin: false,
-  isAuthenticated: computed(s => !!s.email),
-  add: action((state, payload) => {
-    state.domain = payload.domain;
-    state.email = payload.sub;
-    state.isAdmin = payload.admin;
-  }),
-  logout: action(state => {
-    cookie.remove("token");
-    state.domain = null;
-    state.email = null;
-    state.isAdmin = false;
-  }),
-  login: thunk(async (actions, payload) => {
-    const res = await axios.post(APIv2.AuthLogin, payload);
-    const { token } = res.data;
-    cookie.set("token", token, { expires: 7 });
-    const tokenPayload: TokenPayload = decode(token);
-    actions.add(tokenPayload);
-  }),
-  renew: thunk(async actions => {
-    const res = await axios.post(APIv2.AuthRenew, null, getAxiosConfig());
-    const { token } = res.data;
-    cookie.set("token", token, { expires: 7 });
-    const tokenPayload: TokenPayload = decode(token);
-    actions.add(tokenPayload);
-  })
-};

+ 0 - 1
client/store/index.ts

@@ -1 +0,0 @@
-export * from "./store";

+ 0 - 142
client/store/links.ts

@@ -1,142 +0,0 @@
-import { action, Action, thunk, Thunk } from "easy-peasy";
-import axios from "axios";
-import query from "query-string";
-
-import { getAxiosConfig } from "../utils";
-import { APIv2 } from "../consts";
-
-export interface Link {
-  id: string;
-  address: string;
-  banned: boolean;
-  banned_by_id?: number;
-  created_at: string;
-  link: string;
-  domain?: string;
-  domain_id?: number;
-  password?: string;
-  description?: string;
-  expire_in?: string;
-  target: string;
-  updated_at: string;
-  user_id?: number;
-  visit_count: number;
-}
-
-export interface NewLink {
-  target: string;
-  customurl?: string;
-  password?: string;
-  domain?: string;
-  reuse?: boolean;
-  reCaptchaToken?: string;
-}
-
-export interface BanLink {
-  id: string;
-  host?: boolean;
-  domain?: boolean;
-  user?: boolean;
-  userLinks?: boolean;
-}
-
-export interface EditLink {
-  id: string;
-  target: string;
-  address: string;
-  description?: string;
-  expire_in?: string;
-}
-
-export interface LinksQuery {
-  limit: string;
-  skip: string;
-  search: string;
-  all: boolean;
-}
-
-export interface LinksListRes {
-  data: Link[];
-  total: number;
-  limit: number;
-  skip: number;
-}
-
-export interface Links {
-  link?: Link;
-  items: Link[];
-  total: number;
-  loading: boolean;
-  submit: Thunk<Links, NewLink>;
-  get: Thunk<Links, LinksQuery>;
-  add: Action<Links, Link>;
-  set: Action<Links, LinksListRes>;
-  update: Action<Links, Partial<Link>>;
-  remove: Thunk<Links, string>;
-  edit: Thunk<Links, EditLink>;
-  ban: Thunk<Links, BanLink>;
-  setLoading: Action<Links, boolean>;
-}
-
-export const links: Links = {
-  link: null,
-  items: [],
-  total: 0,
-  loading: true,
-  submit: thunk(async (actions, payload) => {
-    const data = Object.fromEntries(
-      Object.entries(payload).filter(([, value]) => value !== "")
-    );
-    const res = await axios.post(APIv2.Links, data, getAxiosConfig());
-    actions.add(res.data);
-    return res.data;
-  }),
-  get: thunk(async (actions, payload) => {
-    actions.setLoading(true);
-    const res = await axios.get(
-      `${APIv2.Links}?${query.stringify(payload)}`,
-      getAxiosConfig()
-    );
-    actions.set(res.data);
-    actions.setLoading(false);
-    return res.data;
-  }),
-  remove: thunk(async (actions, id) => {
-    await axios.delete(`${APIv2.Links}/${id}`, getAxiosConfig());
-  }),
-  ban: thunk(async (actions, { id, ...payload }) => {
-    const res = await axios.post(
-      `${APIv2.Links}/admin/ban/${id}`,
-      payload,
-      getAxiosConfig()
-    );
-    actions.update({ id, banned: true });
-    return res.data;
-  }),
-  edit: thunk(async (actions, { id, ...payload }) => {
-    const res = await axios.patch(
-      `${APIv2.Links}/${id}`,
-      payload,
-      getAxiosConfig()
-    );
-    actions.update(res.data);
-  }),
-  add: action((state, payload) => {
-    if (state.items.length >= 10) {
-      state.items.pop();
-    }
-    state.items.unshift(payload);
-  }),
-  set: action((state, payload) => {
-    state.items = payload.data;
-    state.total = payload.total;
-  }),
-  update: action((state, payload) => {
-    state.items = state.items.map(item =>
-      item.id === payload.id ? { ...item, ...payload } : item
-    );
-  }),
-  setLoading: action((state, payload) => {
-    state.loading = payload;
-  })
-};

+ 0 - 17
client/store/loading.ts

@@ -1,17 +0,0 @@
-import { action, Action } from "easy-peasy";
-
-export interface Loading {
-  loading: boolean;
-  show: Action<Loading>;
-  hide: Action<Loading>;
-}
-
-export const loading: Loading = {
-  loading: false,
-  show: action(state => {
-    state.loading = true;
-  }),
-  hide: action(state => {
-    state.loading = false;
-  })
-};

+ 0 - 85
client/store/settings.ts

@@ -1,85 +0,0 @@
-import { action, Action, thunk, Thunk } from "easy-peasy";
-import axios from "axios";
-
-import { getAxiosConfig } from "../utils";
-import { StoreModel } from "./store";
-import { APIv2 } from "../consts";
-
-export interface Domain {
-  id: string;
-  address: string;
-  banned: boolean;
-  created_at: string;
-  homepage?: string;
-  updated_at: string;
-}
-
-export interface NewDomain {
-  address: string;
-  homepage?: string;
-}
-
-export interface SettingsResp {
-  apikey: string;
-  email: string;
-  domains: Domain[];
-}
-
-export interface Settings {
-  domains: Array<Domain>;
-  apikey: string;
-  email: string;
-  fetched: boolean;
-  setSettings: Action<Settings, SettingsResp>;
-  getSettings: Thunk<Settings, null, null, StoreModel>;
-  setApiKey: Action<Settings, string>;
-  generateApiKey: Thunk<Settings>;
-  addDomain: Action<Settings, Domain>;
-  removeDomain: Action<Settings, string>;
-  saveDomain: Thunk<Settings, NewDomain>;
-  deleteDomain: Thunk<Settings, string>;
-}
-
-export const settings: Settings = {
-  domains: [],
-  email: null,
-  apikey: null,
-  fetched: false,
-  getSettings: thunk(async (actions, payload, { getStoreActions }) => {
-    getStoreActions().loading.show();
-    const res = await axios.get(APIv2.Users, getAxiosConfig());
-    actions.setSettings(res.data);
-    getStoreActions().loading.hide();
-  }),
-  generateApiKey: thunk(async actions => {
-    const res = await axios.post(
-      APIv2.AuthGenerateApikey,
-      null,
-      getAxiosConfig()
-    );
-    actions.setApiKey(res.data.apikey);
-  }),
-  deleteDomain: thunk(async (actions, id) => {
-    await axios.delete(`${APIv2.Domains}/${id}`, getAxiosConfig());
-    actions.removeDomain(id);
-  }),
-  setSettings: action((state, payload) => {
-    state.apikey = payload.apikey;
-    state.domains = payload.domains;
-    state.email = payload.email;
-    state.fetched = true;
-  }),
-  setApiKey: action((state, payload) => {
-    state.apikey = payload;
-  }),
-  addDomain: action((state, payload) => {
-    state.domains.push(payload);
-  }),
-  removeDomain: action((state, id) => {
-    state.domains = state.domains.filter(d => d.id !== id);
-  }),
-  saveDomain: thunk(async (actions, payload) => {
-    const res = await axios.post(APIv2.Domains, payload, getAxiosConfig());
-    actions.addDomain(res.data);
-  })
-};

+ 0 - 35
client/store/store.ts

@@ -1,35 +0,0 @@
-import { Action, createStore, createTypedHooks, action } from "easy-peasy";
-
-import { settings, Settings } from "./settings";
-import { loading, Loading } from "./loading";
-import { links, Links } from "./links";
-import { auth, Auth } from "./auth";
-
-export interface StoreModel {
-  auth: Auth;
-  links: Links;
-  loading: Loading;
-  settings: Settings;
-  reset: Action<StoreModel>;
-}
-
-let initState: any = {};
-
-export const store: StoreModel = {
-  auth,
-  links,
-  loading,
-  settings,
-  reset: action(() => initState)
-};
-
-const typedHooks = createTypedHooks<StoreModel>();
-
-export const useStoreActions = typedHooks.useStoreActions;
-export const useStoreDispatch = typedHooks.useStoreDispatch;
-export const useStoreState = typedHooks.useStoreState;
-
-export const initializeStore = (initialState?: StoreModel) => {
-  initState = initialState;
-  return createStore(store, { initialState });
-};

+ 0 - 31
client/tsconfig.json

@@ -1,31 +0,0 @@
-{
-  "compilerOptions": {
-    "target": "es5",
-    "lib": [
-      "dom",
-      "dom.iterable",
-      "esnext"
-    ],
-    "allowJs": true,
-    "skipLibCheck": true,
-    "strict": false,
-    "forceConsistentCasingInFileNames": true,
-    "noEmit": true,
-    "esModuleInterop": true,
-    "module": "esnext",
-    "moduleResolution": "node",
-    "resolveJsonModule": true,
-    "isolatedModules": true,
-    "jsx": "preserve",
-    "incremental": true
-  },
-  "exclude": [
-    "node_modules"
-  ],
-  "include": [
-    "next-env.d.ts",
-    "**/*.ts",
-    "**/*.tsx",
-    "./module.d.ts"
-  ]
-}

+ 0 - 8
client/types.ts

@@ -1,8 +0,0 @@
-export interface TokenPayload {
-  iss: "ApiAuth";
-  sub: string;
-  domain: string;
-  admin: boolean;
-  iat: number;
-  exp: number;
-}

+ 0 - 23
client/utils.ts

@@ -1,23 +0,0 @@
-import cookie from "js-cookie";
-import { AxiosRequestConfig, AxiosError } from "axios";
-
-export const removeProtocol = (link: string) =>
-  link.replace(/^https?:\/\//, "");
-
-export const withComma = (num: number) =>
-  num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
-
-export const getAxiosConfig = (
-  options: AxiosRequestConfig = {}
-): AxiosRequestConfig => ({
-  ...options,
-  headers: {
-    ...options.headers,
-    Authorization: cookie.get("token")
-  }
-});
-
-export const errorMessage = (err: AxiosError, defaultMessage?: string) => {
-  const data = err?.response?.data as Record<string, any>;
-  return data?.message || data?.error || defaultMessage || "";
-};

+ 0 - 631
docs/api/api.ts

@@ -1,631 +0,0 @@
-import * as p from "../../package.json";
-
-export default {
-  openapi: "3.0.0",
-  info: {
-    title: "Kutt.it",
-    description: "API reference for [http://kutt.it](http://kutt.it).\n",
-    version: p.version
-  },
-  servers: [
-    {
-      url: "https://kutt.it/api/v2"
-    }
-  ],
-  tags: [
-    {
-      name: "health"
-    },
-    {
-      name: "links"
-    },
-    {
-      name: "domains"
-    },
-    {
-      name: "users"
-    }
-  ],
-  paths: {
-    "/health": {
-      get: {
-        tags: ["health"],
-        summary: "API health",
-        responses: {
-          "200": {
-            description: "Health",
-            content: {
-              "text/html": {
-                example: "OK"
-              }
-            }
-          }
-        }
-      }
-    },
-    "/links": {
-      get: {
-        tags: ["links"],
-        description: "Get list of links",
-        parameters: [
-          {
-            name: "limit",
-            in: "query",
-            description: "Limit",
-            required: false,
-            style: "form",
-            explode: true,
-            schema: {
-              type: "number",
-              example: 10
-            }
-          },
-          {
-            name: "skip",
-            in: "query",
-            description: "Skip",
-            required: false,
-            style: "form",
-            explode: true,
-            schema: {
-              type: "number",
-              example: 0
-            }
-          },
-          {
-            name: "all",
-            in: "query",
-            description: "All links (ADMIN only)",
-            required: false,
-            style: "form",
-            explode: true,
-            schema: {
-              type: "boolean",
-              example: false
-            }
-          }
-        ],
-        responses: {
-          "200": {
-            description: "List of links",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/inline_response_200"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      },
-      post: {
-        tags: ["links"],
-        description: "Create a short link",
-        requestBody: {
-          content: {
-            "application/json": {
-              schema: {
-                $ref: "#/components/schemas/body"
-              }
-            }
-          }
-        },
-        responses: {
-          "200": {
-            description: "Created link",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/Link"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    },
-    "/links/{id}": {
-      delete: {
-        tags: ["links"],
-        description: "Delete a link",
-        parameters: [
-          {
-            name: "id",
-            in: "path",
-            required: true,
-            style: "simple",
-            explode: false,
-            schema: {
-              type: "string",
-              format: "uuid"
-            }
-          }
-        ],
-        responses: {
-          "200": {
-            description: "Deleted link successfully",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/inline_response_200_1"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      },
-      patch: {
-        tags: ["links"],
-        description: "Update a link",
-        parameters: [
-          {
-            name: "id",
-            in: "path",
-            required: true,
-            style: "simple",
-            explode: false,
-            schema: {
-              type: "string",
-              format: "uuid"
-            }
-          }
-        ],
-        requestBody: {
-          content: {
-            "application/json": {
-              schema: {
-                $ref: "#/components/schemas/body_1"
-              }
-            }
-          }
-        },
-        responses: {
-          "200": {
-            description: "Updated link successfully",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/Link"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    },
-    "/links/{id}/stats": {
-      get: {
-        tags: ["links"],
-        description: "Get link stats",
-        parameters: [
-          {
-            name: "id",
-            in: "path",
-            required: true,
-            style: "simple",
-            explode: false,
-            schema: {
-              type: "string",
-              format: "uuid"
-            }
-          }
-        ],
-        responses: {
-          "200": {
-            description: "Link stats",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/Stats"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    },
-    "/domains": {
-      post: {
-        tags: ["domains"],
-        description: "Create a domain",
-        requestBody: {
-          content: {
-            "application/json": {
-              schema: {
-                $ref: "#/components/schemas/body_2"
-              }
-            }
-          }
-        },
-        responses: {
-          "200": {
-            description: "Created domain",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/Domain"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    },
-    "/domains/{id}": {
-      delete: {
-        tags: ["domains"],
-        description: "Delete a domain",
-        parameters: [
-          {
-            name: "id",
-            in: "path",
-            required: true,
-            style: "simple",
-            explode: false,
-            schema: {
-              type: "string",
-              format: "uuid"
-            }
-          }
-        ],
-        responses: {
-          "200": {
-            description: "Deleted domain successfully",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/inline_response_200_1"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    },
-    "/users": {
-      get: {
-        tags: ["users"],
-        description: "Get user info",
-        responses: {
-          "200": {
-            description: "User info",
-            content: {
-              "application/json": {
-                schema: {
-                  $ref: "#/components/schemas/User"
-                }
-              }
-            }
-          }
-        },
-        security: [
-          {
-            APIKeyAuth: []
-          }
-        ]
-      }
-    }
-  },
-  components: {
-    schemas: {
-      Link: {
-        type: "object",
-        properties: {
-          address: {
-            type: "string"
-          },
-          banned: {
-            type: "boolean",
-            default: false
-          },
-          created_at: {
-            type: "string",
-            format: "date-time"
-          },
-          id: {
-            type: "string",
-            format: "uuid"
-          },
-          link: {
-            type: "string"
-          },
-          password: {
-            type: "boolean",
-            default: false
-          },
-          target: {
-            type: "string"
-          },
-          description: {
-            type: "string"
-          },
-          updated_at: {
-            type: "string",
-            format: "date-time"
-          },
-          visit_count: {
-            type: "number"
-          }
-        }
-      },
-      Domain: {
-        type: "object",
-        properties: {
-          address: {
-            type: "string"
-          },
-          banned: {
-            type: "boolean",
-            default: false
-          },
-          created_at: {
-            type: "string",
-            format: "date-time"
-          },
-          id: {
-            type: "string",
-            format: "uuid"
-          },
-          homepage: {
-            type: "string"
-          },
-          updated_at: {
-            type: "string",
-            format: "date-time"
-          }
-        }
-      },
-      User: {
-        type: "object",
-        properties: {
-          apikey: {
-            type: "string"
-          },
-          email: {
-            type: "string"
-          },
-          domains: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/Domain"
-            }
-          }
-        }
-      },
-      StatsItem: {
-        type: "object",
-        properties: {
-          stats: {
-            $ref: "#/components/schemas/StatsItem_stats"
-          },
-          views: {
-            type: "array",
-            items: {
-              type: "number"
-            }
-          }
-        }
-      },
-      Stats: {
-        type: "object",
-        properties: {
-          lastDay: {
-            $ref: "#/components/schemas/StatsItem"
-          },
-          lastMonth: {
-            $ref: "#/components/schemas/StatsItem"
-          },
-          lastWeek: {
-            $ref: "#/components/schemas/StatsItem"
-          },
-          lastYear: {
-            $ref: "#/components/schemas/StatsItem"
-          },
-          updatedAt: {
-            type: "string"
-          },
-          address: {
-            type: "string"
-          },
-          banned: {
-            type: "boolean",
-            default: false
-          },
-          created_at: {
-            type: "string",
-            format: "date-time"
-          },
-          id: {
-            type: "string",
-            format: "uuid"
-          },
-          link: {
-            type: "string"
-          },
-          password: {
-            type: "boolean",
-            default: false
-          },
-          target: {
-            type: "string"
-          },
-          updated_at: {
-            type: "string",
-            format: "date-time"
-          },
-          visit_count: {
-            type: "number"
-          }
-        }
-      },
-      inline_response_200: {
-        properties: {
-          limit: {
-            type: "number",
-            default: 10
-          },
-          skip: {
-            type: "number",
-            default: 0
-          },
-          total: {
-            type: "number",
-            default: 0
-          },
-          data: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/Link"
-            }
-          }
-        }
-      },
-      body: {
-        required: ["target"],
-        properties: {
-          target: {
-            type: "string"
-          },
-          description: {
-            type: "string"
-          },
-          expire_in: {
-            type: "string",
-            example: "2 minutes/hours/days"
-          },
-          password: {
-            type: "string"
-          },
-          customurl: {
-            type: "string"
-          },
-          reuse: {
-            type: "boolean",
-            default: false
-          },
-          domain: {
-            type: "string"
-          }
-        }
-      },
-      inline_response_200_1: {
-        properties: {
-          message: {
-            type: "string"
-          }
-        }
-      },
-      body_1: {
-        required: ["target", "address"],
-        properties: {
-          target: {
-            type: "string"
-          },
-          address: {
-            type: "string"
-          },
-          description: {
-            type: "string"
-          },
-          expire_in: {
-            type: "string",
-            example: "2 minutes/hours/days"
-          }
-        }
-      },
-      body_2: {
-        required: ["address"],
-        properties: {
-          address: {
-            type: "string"
-          },
-          homepage: {
-            type: "string"
-          }
-        }
-      },
-      StatsItem_stats_browser: {
-        type: "object",
-        properties: {
-          name: {
-            type: "string"
-          },
-          value: {
-            type: "number"
-          }
-        }
-      },
-      StatsItem_stats: {
-        type: "object",
-        properties: {
-          browser: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/StatsItem_stats_browser"
-            }
-          },
-          os: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/StatsItem_stats_browser"
-            }
-          },
-          country: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/StatsItem_stats_browser"
-            }
-          },
-          referrer: {
-            type: "array",
-            items: {
-              $ref: "#/components/schemas/StatsItem_stats_browser"
-            }
-          }
-        }
-      }
-    },
-    securitySchemes: {
-      APIKeyAuth: {
-        type: "apiKey",
-        name: "X-API-KEY",
-        in: "header"
-      }
-    }
-  }
-};

+ 0 - 48
docs/api/generate.ts

@@ -1,48 +0,0 @@
-import { join, dirname } from 'path';
-
-import { promises as fs } from 'fs';
-
-import api from './api';
-
-const Template = (output, { api, title, redoc }) =>
-	fs.writeFile(output,
-`<DOCTYPE html>
-<html>
-	<head>
-		<meta charset="UTF-8" />
-		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
-		<meta http-equiv="X-UA-Compatible" content="ie=edge" />
-		<title>${title}</title>
-	</head>
-	<body>
-		<redoc spec-url="${api}" />
-		<script src="${redoc}"></script>
-	</body>
-</html>
-`);
-
-const Api = output =>
-	fs.writeFile(output, JSON.stringify(api));
-
-const Redoc = output =>
-	fs.copyFile(join(
-		dirname(require.resolve('redoc')), 
-		'redoc.standalone.js'),
-		output);
-
-export default (async () => {
-	const out = join(__dirname, 'static');
-	const apiFile = 'api.json';
-	const redocFile = 'redoc.js';
-	await fs.mkdir(out, { recursive: true });
-	return Promise.all([
-		Api(join(out, apiFile)),
-		Redoc(join(out, redocFile)),
-		Template(join(out, 'index.html'), {
-			api: apiFile,
-			title: api.info.title,
-			redoc: redocFile
-		}),
-
-	]);
-})();

+ 0 - 160
global.d.ts

@@ -1,160 +0,0 @@
-type Raw = import("knex").Knex.Raw;
-
-type Match<T> = {
-  [K in keyof T]?: T[K] | [">" | ">=" | "<=" | "<", T[K]];
-};
-
-interface User {
-  apikey?: string;
-  banned_by_id?: number;
-  banned: boolean;
-  change_email_address?: string;
-  change_email_expires?: string;
-  change_email_token?: string;
-  cooldowns?: string[];
-  created_at: string;
-  email: string;
-  id: number;
-  password: string;
-  reset_password_expires?: string;
-  reset_password_token?: string;
-  updated_at: string;
-  verification_expires?: string;
-  verification_token?: string;
-  verified?: boolean;
-}
-
-interface UserJoined extends User {
-  admin?: boolean;
-  homepage?: string;
-  domain?: string;
-  domain_id?: number;
-}
-
-interface Domain {
-  id: number;
-  uuid: string;
-  address: string;
-  banned: boolean;
-  banned_by_id?: number;
-  created_at: string;
-  homepage?: string;
-  updated_at: string;
-  user_id?: number;
-}
-
-interface DomainSanitized {
-  id: string;
-  uuid: undefined;
-  address: string;
-  banned: boolean;
-  banned_by_id?: undefined;
-  created_at: string;
-  homepage?: string;
-  updated_at: string;
-  user_id?: undefined;
-}
-
-interface Host {
-  id: number;
-  address: string;
-  banned: boolean;
-  banned_by_id?: number;
-  created_at: string;
-  updated_at: string;
-}
-
-interface IP {
-  id: number;
-  created_at: string;
-  updated_at: string;
-  ip: string;
-}
-
-interface Link {
-  address: string;
-  banned_by_id?: number;
-  banned: boolean;
-  created_at: string;
-  description?: string;
-  domain_id?: number;
-  expire_in: string;
-  id: number;
-  password?: string;
-  target: string;
-  updated_at: string;
-  user_id?: number;
-  uuid: string;
-  visit_count: number;
-}
-
-interface LinkSanitized {
-  address: string;
-  banned_by_id?: undefined;
-  banned: boolean;
-  created_at: string;
-  domain_id?: undefined;
-  id: string;
-  link: string;
-  password: boolean;
-  target: string;
-  updated_at: string;
-  user_id?: undefined;
-  uuid?: undefined;
-  visit_count: number;
-}
-
-interface LinkJoinedDomain extends Link {
-  domain?: string;
-}
-
-interface Visit {
-  id: number;
-  countries: Record<string, number>;
-  created_at: string;
-  link_id: number;
-  referrers: Record<string, number>;
-  total: number;
-  br_chrome: number;
-  br_edge: number;
-  br_firefox: number;
-  br_ie: number;
-  br_opera: number;
-  br_other: number;
-  br_safari: number;
-  os_android: number;
-  os_ios: number;
-  os_linux: number;
-  os_macos: number;
-  os_other: number;
-  os_windows: number;
-}
-
-interface Stats {
-  browser: Record<
-    "chrome" | "edge" | "firefox" | "ie" | "opera" | "other" | "safari",
-    number
-  >;
-  os: Record<
-    "android" | "ios" | "linux" | "macos" | "other" | "windows",
-    number
-  >;
-  country: Record<string, number>;
-  referrer: Record<string, number>;
-}
-
-declare namespace Express {
-  export interface Request {
-    realIP?: string;
-    pageType?: string;
-    linkTarget?: string;
-    protectedLink?: string;
-    token?: string;
-    user: UserJoined;
-    context?: {
-      limit: number;
-      skip: number;
-      all: boolean;
-    };
-  }
-}

+ 0 - 5
jest-setup.ts

@@ -1,5 +0,0 @@
-import "@testing-library/jest-dom";
-import nextConfig from "./next.config";
-
-jest.mock('next/config', () => () => nextConfig);
-

+ 0 - 13
next.config.js

@@ -1,13 +0,0 @@
-const { parsed: localEnv } = require("dotenv").config();
-
-module.exports = {
-  publicRuntimeConfig: {
-    CONTACT_EMAIL: localEnv && localEnv.CONTACT_EMAIL,
-    SITE_NAME: localEnv && localEnv.SITE_NAME,
-    DEFAULT_DOMAIN: localEnv && localEnv.DEFAULT_DOMAIN,
-    RECAPTCHA_SITE_KEY: localEnv && localEnv.RECAPTCHA_SITE_KEY,
-    REPORT_EMAIL: localEnv && localEnv.REPORT_EMAIL,
-    DISALLOW_ANONYMOUS_LINKS: localEnv && localEnv.DISALLOW_ANONYMOUS_LINKS,
-    DISALLOW_REGISTRATION: localEnv && localEnv.DISALLOW_REGISTRATION
-  }
-};

ファイルの差分が大きいため隠しています
+ 999 - 4410
package-lock.json


+ 56 - 110
package.json

@@ -2,22 +2,19 @@
   "name": "kutt",
   "version": "2.7.4",
   "description": "Modern URL shortener.",
-  "main": "./production-server/server.js",
+  "main": "./server/server.js",
   "scripts": {
-    "test": "jest --passWithNoTests",
     "docker:build": "docker build -t kutt .",
     "docker:run": "docker run -p 3000:3000 --env-file .env -d kutt:latest",
     "dev": "node --watch-path=./server server/server.js",
-    "dev:backup": "npm run migrate && cross-env NODE_ENV=development nodemon server/server.ts",
-    "build": "rimraf production-server && tsc --project tsconfig.json && copyfiles -f \"server/mail/*.html\" production-server/mail && next build client/ ",
-    "start": "npm run migrate && cross-env NODE_ENV=production node production-server/server.js",
+    "start": "npm run migrate && cross-env NODE_ENV=production node server/server.js",
     "migrate": "knex migrate:latest --env production",
     "migrate:make": "knex migrate:make --env production",
-    "docs:build": "cd docs/api && tsc generate.ts --resolveJsonModule && node generate && cd ../.."
+    "docs:build": "cd docs/api && node generate && cd ../.."
   },
   "repository": {
     "type": "git",
-    "url": "git+https://github.com/TheDevs-Network/kutt.git"
+    "url": "git+https://github.com/thedevs-network/kutt.git"
   },
   "keywords": [
     "url-shortener"
@@ -25,112 +22,61 @@
   "author": "Pouria Ezzati <ezzati.upt@gmail.com>",
   "license": "MIT",
   "bugs": {
-    "url": "https://github.com/TheDevs-Network/kutt/issues"
+    "url": "https://github.com/thedevs-network/kutt/issues"
   },
-  "homepage": "https://github.com/TheDevs-Network/kutt#readme",
+  "homepage": "https://github.com/thedevs-network/kutt#readme",
   "dependencies": {
-    "app-root-path": "^3.1.0",
-    "axios": "^1.1.3",
-    "bcryptjs": "^2.4.3",
-    "bull": "^4.16.2",
-    "compression": "^1.7.4",
-    "cookie-parser": "^1.4.6",
-    "cors": "^2.8.5",
-    "cross-env": "^7.0.3",
-    "d3-color": "^3.1.0",
-    "date-fns": "^2.29.3",
-    "dotenv": "^16.0.3",
-    "easy-peasy": "^5.1.0",
-    "email-validator": "^2.0.4",
-    "envalid": "^7.3.1",
-    "express": "^4.18.2",
-    "express-async-handler": "1.1.4",
-    "express-validator": "^6.14.2",
-    "geoip-lite": "^1.4.6",
-    "hbs": "^4.2.0",
-    "helmet": "^6.0.0",
-    "ioredis": "^5.2.4",
-    "isbot": "^3.6.3",
-    "js-cookie": "^3.0.1",
-    "jsonwebtoken": "^8.5.1",
-    "jwt-decode": "^3.1.2",
-    "knex": "^2.3.0",
-    "morgan": "^1.10.0",
-    "ms": "^2.1.3",
-    "nanoid": "^2.1.11",
-    "node-cron": "^3.0.2",
-    "nodemailer": "^6.8.0",
-    "p-queue": "^7.3.0",
-    "passport": "^0.6.0",
-    "passport-jwt": "^4.0.0",
-    "passport-local": "^1.0.0",
-    "passport-localapikey-update": "^0.6.0",
-    "pg": "^8.8.0",
-    "pg-query-stream": "^4.2.4",
-    "qrcode.react": "^3.1.0",
-    "query-string": "^7.1.1",
-    "rebass": "^4.0.7",
-    "recharts": "^2.1.16",
-    "redis": "^4.5.0",
-    "signale": "^1.4.0",
-    "styled-components": "^5.3.6",
-    "styled-tools": "^1.7.2",
-    "url-regex-safe": "^3.0.0",
-    "use-media": "^1.4.0",
-    "useragent": "^2.2.1",
-    "uuid": "^3.4.0",
-    "winston": "^3.3.3",
-    "winston-daily-rotate-file": "^4.7.1"
+    "app-root-path": "3.1.0",
+    "axios": "1.7.7",
+    "bcryptjs": "2.4.3",
+    "bull": "4.16.2",
+    "cookie-parser": "1.4.6",
+    "cors": "2.8.5",
+    "cross-env": "7.0.3",
+    "date-fns": "2.30.0",
+    "dotenv": "16.0.3",
+    "envalid": "8.0.0",
+    "express": "4.19.2",
+    "express-validator": "6.14.2",
+    "geoip-lite": "1.4.10",
+    "hbs": "4.2.0",
+    "helmet": "7.1.0",
+    "ioredis": "5.2.4",
+    "isbot": "5.1.17",
+    "jsonwebtoken": "9.0.2",
+    "knex": "3.1.0",
+    "morgan": "1.10.0",
+    "ms": "2.1.3",
+    "nanoid": "2.1.11",
+    "node-cron": "3.0.2",
+    "nodemailer": "^6.9.15",
+    "passport": "0.7.0",
+    "passport-jwt": "4.0.1",
+    "passport-local": "1.0.0",
+    "passport-localapikey-update": "0.6.0",
+    "pg": "8.12.0",
+    "pg-query-stream": "4.6.0",
+    "signale": "1.4.0",
+    "useragent": "2.3.0",
+    "uuid": "10.0.0",
+    "winston": "3.3.3",
+    "winston-daily-rotate-file": "4.7.1"
   },
   "devDependencies": {
-    "@types/bcryptjs": "^2.4.2",
-    "@types/cookie-parser": "^1.4.3",
-    "@types/cors": "^2.8.12",
-    "@types/express": "^4.17.14",
-    "@types/hbs": "^4.0.4",
-    "@types/jest": "^26.0.20",
-    "@types/jsonwebtoken": "^7.2.8",
-    "@types/morgan": "^1.7.37",
-    "@types/ms": "^0.7.31",
-    "@types/nanoid": "^3.0.0",
-    "@types/node": "^18.11.9",
-    "@types/node-cron": "^2.0.2",
-    "@types/nodemailer": "^6.4.6",
-    "@types/pg": "^8.6.5",
-    "@types/rebass": "^4.0.10",
-    "@types/signale": "^1.4.4",
-    "@types/styled-components": "^5.1.7",
-    "copyfiles": "^2.4.1",
-    "husky": "^8.0.2",
-    "jest": "^29.3.1",
-    "nodemon": "^2.0.20",
-    "prettier": "^2.7.1",
-    "redoc": "^2.0.0",
-    "rimraf": "^3.0.2",
-    "ts-node": "^10.9.1",
-    "typescript": "^4.8.4"
-  },
-  "overrides": {
-    "react-use-form-state": {
-      "react": "*",
-      "react-dom": "*"
-    },
-    "redoc": {
-      "react": "*",
-      "react-dom": "*"
-    },
-    "use-media": {
-      "react": "*",
-      "react-dom": "*"
-    },
-    "react-transition-group": {
-      "react": "*",
-      "react-dom": "*"
-    },
-    "recharts": {
-      "react": "*",
-      "react-dom": "*",
-      "d3-color": "*"
-    }
+    "@types/bcryptjs": "2.4.2",
+    "@types/cookie-parser": "1.4.3",
+    "@types/cors": "2.8.12",
+    "@types/express": "4.17.14",
+    "@types/hbs": "4.0.4",
+    "@types/jsonwebtoken": "7.2.8",
+    "@types/morgan": "1.7.37",
+    "@types/ms": "0.7.31",
+    "@types/node": "18.11.9",
+    "@types/node-cron": "2.0.2",
+    "@types/nodemailer": "6.4.6",
+    "@types/pg": "8.6.5",
+    "@types/rebass": "4.0.10",
+    "@types/signale": "1.4.4",
+    "redoc": "2.0.0"
   }
 }

+ 14 - 0
server/cron.js

@@ -0,0 +1,14 @@
+const cron = require("node-cron");
+
+const query = require("./queries");
+const env = require("./env");
+
+if (env.NON_USER_COOLDOWN) {
+  cron.schedule("* */24 * * *", function() {
+    query.ip.clear().catch();
+  });
+}
+
+cron.schedule("*/15 * * * * *", function() {
+  query.link.batchRemove({ expire_in: ["<", new Date().toISOString()] }).catch();
+});

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません