Add community dialog (#30)

This commit is contained in:
Oleksandr 2021-10-04 13:05:41 +02:00 committed by GitHub
parent 654c0c29d7
commit a4e2aee6f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 560 additions and 76 deletions

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from "react";
import styled from "styled-components";
import { Theme } from "../../styles/themes";
const userAgent = window.navigator.userAgent;
const platform = window.navigator.platform;
const macosPlatforms = ["Macintosh", "MacIntel", "MacPPC", "Mac68K"];
const windowsPlatforms = ["Win32", "Win64", "Windows", "WinCE"];
const iosPlatforms = ["iPhone", "iPad", "iPod"];
export const DownloadButton = ({ theme }: { theme: Theme }) => {
const [link, setlink] = useState("https://status.im/get/");
const [os, setOs] = useState<string | null>(null);
useEffect(() => {
if (macosPlatforms.includes(platform)) {
setlink(
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.dmg"
);
setOs("Mac");
} else if (iosPlatforms.includes(platform)) {
setlink(
"https://apps.apple.com/us/app/status-private-communication/id1178893006"
);
setOs("iOS");
} else if (windowsPlatforms.includes(platform)) {
setlink(
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.exe"
);
setOs("Windows");
} else if (/Android/.test(userAgent)) {
setlink(
"https://play.google.com/store/apps/details?id=im.status.ethereum"
);
setOs("Android");
} else if (/Linux/.test(platform)) {
setlink(
"https://status-im-files.ams3.cdn.digitaloceanspaces.com/StatusIm-Desktop-v0.3.0-beta-a8c37d.tar.gz"
);
setOs("Linux");
}
}, []);
return (
<Link href={link} theme={theme} target="_blank" rel="noopener noreferrer">
{os ? `Download Status for ${os}` : "Download Status"}
</Link>
);
};
const Link = styled.a`
margin-top: 24px;
padding: 11px 32px;
font-weight: 500;
font-size: 15px;
line-height: 22px;
text-align: center;
color: ${({ theme }) => theme.tertiary};
background: ${({ theme }) => theme.buttonBg};
border-radius: 8px;
`;

View File

@ -5,8 +5,9 @@ import { ChannelData, channels } from "../helpers/channelsMock";
import { CommunityData } from "../helpers/communityMock"; import { CommunityData } from "../helpers/communityMock";
import { Theme } from "../styles/themes"; import { Theme } from "../styles/themes";
import { Community, MembersAmount } from "./Community"; import { Community } from "./Community";
import { MutedIcon } from "./Icons/MutedIcon"; import { MutedIcon } from "./Icons/MutedIcon";
import { textMediumStyles } from "./Text";
interface ChannelsProps { interface ChannelsProps {
theme: Theme; theme: Theme;
@ -37,7 +38,7 @@ export function Channels({
return ( return (
<ChannelsWrapper theme={theme}> <ChannelsWrapper theme={theme}>
<Community community={community} theme={theme} /> <StyledCommunity theme={theme} community={community} />
<ChannelList> <ChannelList>
{channels.map((channel) => ( {channels.map((channel) => (
<Channel <Channel
@ -139,6 +140,18 @@ const ChannelsWrapper = styled.div<ThemeProps>`
flex-direction: column; flex-direction: column;
`; `;
const StyledCommunity = styled(Community)`
padding-left: 0 0 0 10px;
margin: 0 0 16px;
`;
const MembersAmount = styled.p<ThemeProps>`
font-size: 12px;
line-height: 16px;
letter-spacing: 0.1px;
color: ${({ theme }) => theme.secondary};
`;
const ChannelList = styled.div` const ChannelList = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -193,11 +206,10 @@ const ChannelLogo = styled.div<ThemeProps>`
`; `;
const ChannelName = styled.p<ThemeProps>` const ChannelName = styled.p<ThemeProps>`
color: ${({ theme }) => theme.textPrimaryColor};
font-weight: 500; font-weight: 500;
font-size: 15px;
line-height: 22px;
opacity: 0.7; opacity: 0.7;
color: ${({ theme }) => theme.primary};
${textMediumStyles}
&.active, &.active,
&.notified { &.notified {

View File

@ -34,13 +34,13 @@ export function ChatInput({ theme, addMessage }: ChatInputProps) {
onSelect={addEmoji} onSelect={addEmoji}
theme={theme === lightTheme ? "light" : "dark"} theme={theme === lightTheme ? "light" : "dark"}
set="apple" set="apple"
color={theme.memberNameColor} color={theme.tertiary}
emojiSize={26} emojiSize={26}
style={{ style={{
position: "absolute", position: "absolute",
bottom: "100%", bottom: "100%",
right: "0", right: "0",
color: theme.textSecondaryColor, color: theme.secondary,
}} }}
showPreview={false} showPreview={false}
showSkinTones={false} showSkinTones={false}
@ -106,7 +106,7 @@ const Input = styled.textarea<ThemeProps>`
background: ${({ theme }) => theme.inputColor}; background: ${({ theme }) => theme.inputColor};
border-radius: 36px 16px 4px 36px; border-radius: 36px 16px 4px 36px;
border: 1px solid ${({ theme }) => theme.inputColor}; border: 1px solid ${({ theme }) => theme.inputColor};
color: ${({ theme }) => theme.textPrimaryColor}; color: ${({ theme }) => theme.primary};
margin-left: 10px; margin-left: 10px;
padding-top: 9px; padding-top: 9px;
padding-bottom: 9px; padding-bottom: 9px;

View File

@ -4,6 +4,7 @@ import styled from "styled-components";
import { ChatMessage } from "../../models/ChatMessage"; import { ChatMessage } from "../../models/ChatMessage";
import { Theme } from "../../styles/themes"; import { Theme } from "../../styles/themes";
import { UserIcon } from "../Icons/UserIcon"; import { UserIcon } from "../Icons/UserIcon";
import { textSmallStyles } from "../Text";
import { ChatMessageContent } from "./ChatMessageContent"; import { ChatMessageContent } from "./ChatMessageContent";
@ -94,11 +95,11 @@ const DateSeparator = styled.div`
font-family: Inter; font-family: Inter;
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-size: 13px;
line-height: 18px;
color: #939ba1; color: #939ba1;
margin-top: 16px; margin-top: 16px;
margin-bottom: 16px; margin-bottom: 16px;
${textSmallStyles}
`; `;
const ContentWrapper = styled.div` const ContentWrapper = styled.div`
@ -126,7 +127,7 @@ export const Icon = styled.div`
const UserNameWrapper = styled.div<ThemeProps>` const UserNameWrapper = styled.div<ThemeProps>`
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
color: ${({ theme }) => theme.memberNameColor}; color: ${({ theme }) => theme.tertiary};
`; `;
const TimeWrapper = styled.div<ThemeProps>` const TimeWrapper = styled.div<ThemeProps>`
@ -134,7 +135,7 @@ const TimeWrapper = styled.div<ThemeProps>`
line-height: 14px; line-height: 14px;
letter-spacing: 0.2px; letter-spacing: 0.2px;
text-transform: uppercase; text-transform: uppercase;
color: ${({ theme }) => theme.textSecondaryColor}; color: ${({ theme }) => theme.secondary};
margin-left: 4px; margin-left: 4px;
`; `;
@ -142,5 +143,5 @@ const MessageText = styled.div<ThemeProps>`
overflow-wrap: anywhere; overflow-wrap: anywhere;
width: 100%; width: 100%;
white-space: pre; white-space: pre;
color: ${({ theme }) => theme.textPrimaryColor}; color: ${({ theme }) => theme.primary};
`; `;

View File

@ -1,57 +1,42 @@
import React from "react"; import React, { useState } from "react";
import styled from "styled-components";
import { CommunityData } from "../helpers/communityMock"; import { CommunityData } from "../helpers/communityMock";
import { Theme } from "../styles/themes"; import { Theme } from "../styles/themes";
import { CommunityIdentity } from "./CommunityIdentity";
import { CommunityModal } from "./Modals/CommunityModal";
interface CommunityProps { interface CommunityProps {
theme: Theme; theme: Theme;
community: CommunityData; community: CommunityData;
className?: string;
} }
export function Community({ theme, community }: CommunityProps) { export function Community({ theme, community, className }: CommunityProps) {
const { name, icon, members, description } = community;
const [isModalVisible, setIsModalVisible] = useState(false);
return ( return (
<CommunityWrap> <>
<CommunityLogo src={community.icon} alt={`${community.name} logo`} /> <button className={className} onClick={() => setIsModalVisible(true)}>
<CommunityInfo> <CommunityIdentity
<CommunityName theme={theme}>{community.name}</CommunityName> name={name}
<MembersAmount theme={theme}>{community.members} members</MembersAmount> icon={icon}
</CommunityInfo> subtitle={`${members} members`}
</CommunityWrap> theme={theme}
/>
</button>
<CommunityModal
isVisible={isModalVisible}
onClose={() => setIsModalVisible(false)}
icon={icon}
name={name}
theme={theme}
subtitle="Public Community"
description={description}
publicKey="0xD95DBdaB08A9FED2D71ac9C3028AAc40905d8CF3"
/>
</>
); );
} }
interface ThemeProps {
theme: Theme;
}
const CommunityWrap = styled.div`
display: flex;
`;
const CommunityLogo = styled.img`
width: 36px;
height: 36px;
border-radius: 50%;
margin-left: 10px;
`;
const CommunityInfo = styled.div`
display: flex;
flex-direction: column;
margin-left: 8px;
`;
const CommunityName = styled.h1<ThemeProps>`
font-weight: 500;
font-size: 15px;
line-height: 22px;
color: ${({ theme }) => theme.textPrimaryColor};
`;
export const MembersAmount = styled.p<ThemeProps>`
font-size: 12px;
line-height: 16px;
letter-spacing: 0.1px;
color: ${({ theme }) => theme.textSecondaryColor};
`;

View File

@ -0,0 +1,57 @@
import React from "react";
import styled from "styled-components";
import { Theme } from "../styles/themes";
import { textMediumStyles } from "./Text";
export interface CommunityIdentityProps {
icon: string;
name: string;
subtitle: string;
theme: Theme;
className?: string;
}
export const CommunityIdentity = ({
icon,
name,
subtitle,
className,
theme,
}: CommunityIdentityProps) => {
return (
<Row className={className}>
<Logo src={icon} alt={`${name} logo`} />
<div>
<Name theme={theme}>{name}</Name>
<Subtitle theme={theme}>{subtitle}</Subtitle>
</div>
</Row>
);
};
const Row = styled.div`
display: flex;
`;
const Logo = styled.img`
width: 36px;
height: 36px;
border-radius: 50%;
margin-right: 8px;
`;
const Name = styled.p`
font-weight: 500;
color: ${({ theme }) => theme.primary};
${textMediumStyles}
`;
const Subtitle = styled.p`
font-size: 12px;
line-height: 16px;
letter-spacing: 0.1px;
color: ${({ theme }) => theme.secondary};
`;

View File

@ -0,0 +1,75 @@
import React from "react";
import styled from "styled-components";
import { Theme } from "../../styles/themes";
import { copy } from "../../utils/copy";
import { reduceString } from "../../utils/reduceString";
import { textMediumStyles, textSmallStyles } from "../Text";
interface CopyInputProps {
label: string;
value: string;
theme: Theme;
}
export const CopyInput = ({ label, value, theme }: CopyInputProps) => (
<div>
<Label theme={theme}>{label}</Label>
<Wrapper theme={theme}>
<Text theme={theme}>{reduceString(value, 15, 15)}</Text>
<CopyButtonWrapper>
<CopyButton theme={theme} onClick={() => copy(value)}>
Copy
</CopyButton>
</CopyButtonWrapper>
</Wrapper>
</div>
);
const Label = styled.p`
margin-bottom: 7px;
font-weight: 500;
display: flex;
align-items: center;
color: ${({ theme }) => theme.primary};
${textSmallStyles}
`;
const Wrapper = styled.div`
position: relative;
padding: 14px 70px 14px 8px;
background: ${({ theme }) => theme.inputColor};
border-radius: 8px;
`;
const Text = styled.p`
color: ${({ theme }) => theme.primary};
${textMediumStyles}
`;
const CopyButtonWrapper = styled.div`
position: absolute;
top: 50%;
right: 0;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 70px;
transform: translateY(-50%);
background: ${({ theme }) => theme.inputColor};
border-radius: 8px;
`;
const CopyButton = styled.button`
padding: 6px 12px;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.1px;
color: ${({ theme }) => theme.tertiary};
background: ${({ theme }) => theme.buttonBg};
border: 1px solid ${({ theme }) => theme.tertiary};
border-radius: 6px;
`;

View File

@ -0,0 +1,28 @@
import React from "react";
import styled from "styled-components";
import { Theme } from "../../styles/themes";
export const CrossIcon = ({ theme }: { theme: Theme }) => (
<Icon
theme={theme}
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M6 4.57404L1.72275 0.296796C1.32616 -0.0997918 0.689941 -0.0975927 0.296174 0.296174C-0.100338 0.692686 -0.0973145 1.32864 0.296796 1.72275L4.57404 6L0.296796 10.2772C-0.0997918 10.6738 -0.0975927 11.3101 0.296174 11.7038C0.692686 12.1003 1.32864 12.0973 1.72275 11.7032L6 7.42596L10.2772 11.7032C10.6738 12.0998 11.3101 12.0976 11.7038 11.7038C12.1003 11.3073 12.0973 10.6714 11.7032 10.2772L7.42596 6L11.7032 1.72275C12.0998 1.32616 12.0976 0.689941 11.7038 0.296174C11.3073 -0.100338 10.6714 -0.0973145 10.2772 0.296796L6 4.57404Z"
fill="black"
/>
</Icon>
);
const Icon = styled.svg`
& > path {
fill: ${({ theme }) => theme.primary};
}
`;

View File

@ -41,10 +41,10 @@ export const EmojiIcon = ({ theme, isActive }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textSecondaryColor}; fill: ${({ theme }) => theme.secondary};
} }
& > path.active { & > path.active {
fill: ${({ theme }) => theme.memberNameColor}; fill: ${({ theme }) => theme.tertiary};
} }
`; `;

View File

@ -41,10 +41,10 @@ export const GifIcon = ({ theme, isActive }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textSecondaryColor}; fill: ${({ theme }) => theme.secondary};
} }
& > path.active { & > path.active {
fill: ${({ theme }) => theme.memberNameColor}; fill: ${({ theme }) => theme.tertiary};
} }
`; `;

View File

@ -28,6 +28,6 @@ export const MembersIcon = ({ theme }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textPrimaryColor}; fill: ${({ theme }) => theme.primary};
} }
`; `;

View File

@ -30,6 +30,6 @@ export const MutedIcon = ({ theme }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textPrimaryColor}; fill: ${({ theme }) => theme.primary};
} }
`; `;

View File

@ -32,6 +32,6 @@ export const PictureIcon = ({ theme }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textSecondaryColor}; fill: ${({ theme }) => theme.secondary};
} }
`; `;

File diff suppressed because one or more lines are too long

View File

@ -33,10 +33,10 @@ export const StickerIcon = ({ theme, isActive }: ThemeProps) => {
const Icon = styled.svg<ThemeProps>` const Icon = styled.svg<ThemeProps>`
& > path { & > path {
fill: ${({ theme }) => theme.textSecondaryColor}; fill: ${({ theme }) => theme.secondary};
} }
& > path.active { & > path.active {
fill: ${({ theme }) => theme.memberNameColor}; fill: ${({ theme }) => theme.tertiary};
} }
`; `;

View File

@ -0,0 +1,87 @@
import React from "react";
import styled from "styled-components";
import { DownloadButton } from "../Buttons/DownloadButton";
import {
CommunityIdentity,
CommunityIdentityProps,
} from "../CommunityIdentity";
import { CopyInput } from "../Form/CopyInput";
import { StatusLogo } from "../Icons/StatusLogo";
import { textMediumStyles, textSmallStyles } from "../Text";
import { BasicModalProps, Modal } from "./Modal";
interface CommunityModalProps extends BasicModalProps, CommunityIdentityProps {
description: string;
publicKey: string;
}
export const CommunityModal = ({
isVisible,
onClose,
icon,
name,
subtitle,
description,
publicKey,
theme,
}: CommunityModalProps) => {
return (
<Modal theme={theme} isVisible={isVisible} onClose={onClose}>
<Section theme={theme}>
<CommunityIdentity
theme={theme}
icon={icon}
name={name}
subtitle={subtitle}
/>
</Section>
<Section theme={theme}>
<Text theme={theme}>{description}</Text>
</Section>
<Section theme={theme}>
<CopyInput
theme={theme}
value={publicKey}
label="Community public key"
/>
<Hint theme={theme}>
To access this community, paste community public key in Status desktop
or mobile app
</Hint>
</Section>
<BottomSection theme={theme}>
<StatusLogo theme={theme} />
<DownloadButton theme={theme} />
</BottomSection>
</Modal>
);
};
const Section = styled.div`
padding: 20px 16px;
& + & {
border-top: 1px solid ${({ theme }) => theme.border};
}
`;
const Text = styled.p`
color: ${({ theme }) => theme.primary};
${textMediumStyles}
`;
const Hint = styled.p`
margin-top: 16px;
color: ${({ theme }) => theme.secondary};
${textSmallStyles}
`;
const BottomSection = styled(Section)`
display: flex;
flex-direction: column;
align-items: center;
`;

View File

@ -0,0 +1,100 @@
import React, { ReactNode, useCallback, useEffect } from "react";
import { createPortal } from "react-dom";
import styled from "styled-components";
import { Theme } from "../../styles/themes";
import { CrossIcon } from "../Icons/CrossIcon";
export interface BasicModalProps {
isVisible: boolean;
onClose: () => void;
className?: string;
theme: Theme;
}
export interface ModalProps extends BasicModalProps {
children: ReactNode;
}
export const Modal = ({
isVisible,
onClose,
children,
className,
theme,
}: ModalProps) => {
const listenKeyboard = useCallback(
(event: KeyboardEvent) => {
if (event.key === "Escape" || event.keyCode === 27) {
onClose();
}
},
[onClose]
);
useEffect(() => {
if (isVisible) {
window.addEventListener("keydown", listenKeyboard, true);
return () => {
window.removeEventListener("keydown", listenKeyboard, true);
};
}
}, [isVisible, listenKeyboard]);
if (!isVisible) return null;
return createPortal(
<ModalView>
<ModalOverlay onClick={onClose} theme={theme} />
<ModalBody theme={theme} className={className}>
<CloseButton onClick={onClose}>
<CrossIcon theme={theme} />
</CloseButton>
{children}
</ModalBody>
</ModalView>,
document.body
);
};
const ModalView = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 9999;
`;
const ModalBody = styled.div`
position: absolute;
top: 50%;
left: 50%;
max-width: 480px;
width: 100%;
transform: translate(-50%, -50%);
background: ${({ theme }) => theme.bodyBackgroundColor};
border-radius: 8px;
overflow-y: auto;
`;
const ModalOverlay = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: ${({ theme }) => theme.primary};
opacity: 0.4;
`;
const CloseButton = styled.button`
position: absolute;
top: 12px;
right: 12px;
padding: 10px;
`;

View File

@ -0,0 +1,11 @@
import { css } from "styled-components";
export const textSmallStyles = css`
font-size: 13px;
line-height: 18px;
`;
export const textMediumStyles = css`
font-size: 15px;
line-height: 22px;
`;

View File

@ -3,6 +3,7 @@ export type CommunityData = {
name: string; name: string;
icon: string; icon: string;
members: number; members: number;
description: string;
}; };
export const community = { export const community = {
@ -10,4 +11,5 @@ export const community = {
name: "CryptoKitties", name: "CryptoKitties",
icon: "https://www.cryptokitties.co/icons/logo.svg", icon: "https://www.cryptokitties.co/icons/logo.svg",
members: 186, members: 186,
description: "A community of cat lovers, meow!",
}; };

View File

@ -1,9 +1,9 @@
export type Theme = { export type Theme = {
textPrimaryColor: string; primary: string;
textSecondaryColor: string; secondary: string;
bodyBackgroundColor: string; bodyBackgroundColor: string;
sectionBackgroundColor: string; sectionBackgroundColor: string;
memberNameColor: string; tertiary: string;
guestNameColor: string; guestNameColor: string;
iconColor: string; iconColor: string;
iconUserColor: string; iconUserColor: string;
@ -11,14 +11,16 @@ export type Theme = {
activeChannelBackground: string; activeChannelBackground: string;
notificationColor: string; notificationColor: string;
inputColor: string; inputColor: string;
border: string;
buttonBg: string;
}; };
export const lightTheme: Theme = { export const lightTheme: Theme = {
textPrimaryColor: "#000", primary: "#000",
textSecondaryColor: "#939BA1", secondary: "#939BA1",
tertiary: "#4360DF",
bodyBackgroundColor: "#fff", bodyBackgroundColor: "#fff",
sectionBackgroundColor: "#F6F8FA", sectionBackgroundColor: "#F6F8FA",
memberNameColor: "#4360DF",
guestNameColor: "#887AF9", guestNameColor: "#887AF9",
iconColor: "#D37EF4", iconColor: "#D37EF4",
iconUserColor: "#717199", iconUserColor: "#717199",
@ -26,14 +28,16 @@ export const lightTheme: Theme = {
activeChannelBackground: "#E9EDF1", activeChannelBackground: "#E9EDF1",
notificationColor: "#4360DF", notificationColor: "#4360DF",
inputColor: "#EEF2F5", inputColor: "#EEF2F5",
border: "#EEF2F5",
buttonBg: "rgba(67, 96, 223, 0.2)",
}; };
export const darkTheme: Theme = { export const darkTheme: Theme = {
textPrimaryColor: "#fff", primary: "#fff",
textSecondaryColor: "#909090", secondary: "#909090",
tertiary: "#88B0FF",
bodyBackgroundColor: "#000", bodyBackgroundColor: "#000",
sectionBackgroundColor: "#252525", sectionBackgroundColor: "#252525",
memberNameColor: "#88B0FF",
guestNameColor: "#887AF9", guestNameColor: "#887AF9",
iconColor: "#D37EF4", iconColor: "#D37EF4",
iconUserColor: "#717199", iconUserColor: "#717199",
@ -41,6 +45,8 @@ export const darkTheme: Theme = {
activeChannelBackground: "#2C2C2C", activeChannelBackground: "#2C2C2C",
notificationColor: "#887AF9", notificationColor: "#887AF9",
inputColor: "#373737", inputColor: "#373737",
border: "#373737",
buttonBg: "rgba(134, 158, 255, 0.3)",
}; };
export default { lightTheme, darkTheme }; export default { lightTheme, darkTheme };

View File

@ -0,0 +1,5 @@
export const copy = (text: string) => {
navigator.clipboard.writeText(text).catch((error) => {
console.log(error);
});
};

View File

@ -0,0 +1,9 @@
export const reduceString = (
string: string,
limitBefore: number,
limitAfter: number
) => {
return `${string.substring(0, limitBefore)}...${string.substring(
string.length - limitAfter
)}`;
};