mirror of
https://github.com/apotdevin/thunderhub.git
synced 2026-08-13 12:33:08 +02:00
chore: migrate to tailwind (#655)
This commit is contained in:
parent
9e2efb02c7
commit
89e3e4cfbc
199 changed files with 6631 additions and 8272 deletions
1492
package-lock.json
generated
1492
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -85,6 +85,7 @@
|
|||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-grid-layout": "^2.2.2",
|
||||
|
|
@ -95,8 +96,6 @@
|
|||
"rxjs": "^7.8.2",
|
||||
"secp256k1": "^5.0.1",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"styled-components": "^6.3.11",
|
||||
"styled-theming": "^2.2.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"tiny-secp256k1": "^2.2.4",
|
||||
|
|
@ -129,11 +128,9 @@
|
|||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/secp256k1": "^4.0.7",
|
||||
"@types/styled-theming": "^2.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.0",
|
||||
"@typescript-eslint/parser": "^8.57.0",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"babel-plugin-styled-components": "^2.1.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^10.0.3",
|
||||
|
|
|
|||
18
schema.gql
18
schema.gql
|
|
@ -333,11 +333,6 @@ type GetInvoicesType {
|
|||
next: String
|
||||
}
|
||||
|
||||
type GetMessages {
|
||||
messages: [Message!]!
|
||||
token: String
|
||||
}
|
||||
|
||||
type GetPaymentsType {
|
||||
next: String
|
||||
payments: [PaymentType!]!
|
||||
|
|
@ -404,17 +399,6 @@ type LightningNodeSocialInfo {
|
|||
|
||||
union LnUrlRequest = ChannelRequest | PayRequest | WithdrawRequest
|
||||
|
||||
type Message {
|
||||
alias: String
|
||||
contentType: String
|
||||
date: String!
|
||||
id: String!
|
||||
message: String
|
||||
sender: String
|
||||
tokens: Float
|
||||
verified: Boolean!
|
||||
}
|
||||
|
||||
type MessageType {
|
||||
message: String
|
||||
}
|
||||
|
|
@ -443,7 +427,6 @@ type Mutation {
|
|||
pushBackup: Boolean!
|
||||
removePeer(publicKey: String): Boolean!
|
||||
removeTwofaSecret(token: String!): Boolean!
|
||||
sendMessage(maxFee: Float, message: String!, messageType: String, publicKey: String!, tokens: Float): Float!
|
||||
sendToAddress(address: String!, fee: Float, sendAll: Boolean, target: Float, tokens: Float): ChainAddressSend!
|
||||
toggleConfig(field: ConfigFields!): Boolean!
|
||||
updateFees(base_fee_tokens: Float, cltv_delta: Float, fee_rate: Float, max_htlc_mtokens: String, min_htlc_mtokens: String, transaction_id: String, transaction_vout: Float): Boolean!
|
||||
|
|
@ -697,7 +680,6 @@ type Query {
|
|||
getLatestVersion: String!
|
||||
getLightningAddressInfo(address: String!): PayRequest!
|
||||
getLiquidityPerUsd: String!
|
||||
getMessages(initialize: Boolean): GetMessages!
|
||||
getNetworkInfo: NetworkInfo!
|
||||
getNode(publicKey: String!, withoutChannels: Boolean): Node!
|
||||
getNodeBalances: Balances!
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
content="Manage and monitor your lightning network node right inside your browser"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet" />
|
||||
<title>ThunderHub - Lightning Node Manager</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,18 @@
|
|||
import { FC, ReactNode, useEffect, lazy, Suspense } from 'react';
|
||||
import { StyleSheetManager, ThemeProvider } from 'styled-components';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||
import Cookies from 'js-cookie';
|
||||
import isPropValid from '@emotion/is-prop-valid';
|
||||
import { useApollo } from '../config/client';
|
||||
import { ContextProvider } from './context/ContextProvider';
|
||||
import { useConfigState, ConfigProvider } from './context/ConfigContext';
|
||||
import { GlobalStyles } from './styles/GlobalStyle';
|
||||
import { Header } from './layouts/header/Header';
|
||||
import { Footer } from './layouts/footer/Footer';
|
||||
import { PageWrapper, HeaderBodyWrapper } from './layouts/Layout.styled';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { useListener } from './hooks/UseListener';
|
||||
import { SseProvider } from './context/SseContext';
|
||||
import { config } from './config/thunderhubConfig';
|
||||
import { LoadingCard } from './components/loading/LoadingCard';
|
||||
import { useGetNodeInfoQuery } from './graphql/queries/__generated__/getNodeInfo.generated';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
import 'react-resizable/css/styles.css';
|
||||
|
||||
|
|
@ -35,7 +29,6 @@ import ChainPage from './pages/ChainPage';
|
|||
import ToolsPage from './pages/ToolsPage';
|
||||
import StatsPage from './pages/StatsPage';
|
||||
import SwapPage from './pages/SwapPage';
|
||||
import ChatPage from './pages/ChatPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import AmbossPage from './pages/AmbossPage';
|
||||
|
||||
|
|
@ -49,19 +42,6 @@ const LoadingCompSmall = () => (
|
|||
<LoadingCard noCard={true} loadingHeight={'30vh'} />
|
||||
);
|
||||
|
||||
const S = {
|
||||
wrapper: styled.div`
|
||||
position: relative;
|
||||
`,
|
||||
};
|
||||
|
||||
function shouldForwardProp(propName: string, target: any) {
|
||||
if (typeof target === 'string') {
|
||||
return isPropValid(propName);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const NotAuthenticated: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -95,24 +75,21 @@ const Wrapper: FC<{ children?: ReactNode }> = ({ children }) => {
|
|||
const checking = !isRoot && loading;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={{ mode: isRoot ? 'light' : theme }}>
|
||||
<GlobalStyles />
|
||||
<PageWrapper>
|
||||
<HeaderBodyWrapper>
|
||||
<Header />
|
||||
<Listener isRoot={isRoot} />
|
||||
{checking ? (
|
||||
<LoadingCard noCard={true} loadingHeight={'80vh'} />
|
||||
) : isRoot || authenticated ? (
|
||||
children
|
||||
) : (
|
||||
<NotAuthenticated />
|
||||
)}
|
||||
</HeaderBodyWrapper>
|
||||
<Footer />
|
||||
<Toaster position="top-right" />
|
||||
</PageWrapper>
|
||||
</ThemeProvider>
|
||||
<div className="relative min-h-screen">
|
||||
<div className="pb-[120px]">
|
||||
<Header />
|
||||
<Listener isRoot={isRoot} />
|
||||
{checking ? (
|
||||
<LoadingCard noCard={true} loadingHeight={'80vh'} />
|
||||
) : isRoot || authenticated ? (
|
||||
children
|
||||
) : (
|
||||
<NotAuthenticated />
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Toaster position="top-right" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -123,9 +100,9 @@ const AuthenticatedRoutes = () => (
|
|||
path="/dashboard"
|
||||
element={
|
||||
<Suspense fallback={<LoadingComp />}>
|
||||
<S.wrapper>
|
||||
<div className="relative">
|
||||
<DashboardPage />
|
||||
</S.wrapper>
|
||||
</div>
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
|
|
@ -138,7 +115,6 @@ const AuthenticatedRoutes = () => (
|
|||
<Route path="/tools" element={<ToolsPage />} />
|
||||
<Route path="/stats" element={<StatsPage />} />
|
||||
<Route path="/swap" element={<SwapPage />} />
|
||||
<Route path="/chat" element={<ChatPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
<Route
|
||||
path="/settings/dashboard"
|
||||
|
|
@ -161,18 +137,16 @@ export default function App() {
|
|||
const apolloClient = useApollo('', null);
|
||||
|
||||
return (
|
||||
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ConfigProvider initialConfig={{ theme: themeCookie }}>
|
||||
<SseProvider>
|
||||
<ContextProvider>
|
||||
<Wrapper>
|
||||
<AuthenticatedRoutes />
|
||||
</Wrapper>
|
||||
</ContextProvider>
|
||||
</SseProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
</StyleSheetManager>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<ConfigProvider initialConfig={{ theme: themeCookie }}>
|
||||
<SseProvider>
|
||||
<ContextProvider>
|
||||
<Wrapper>
|
||||
<AuthenticatedRoutes />
|
||||
</Wrapper>
|
||||
</ContextProvider>
|
||||
</SseProvider>
|
||||
</ConfigProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +1,7 @@
|
|||
import { Fragment } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { ProgressBar } from '../generic/CardGeneric';
|
||||
|
||||
type BalanceLineProps = {
|
||||
withBorderColor?: boolean;
|
||||
};
|
||||
|
||||
const BalanceLine = styled.div<BalanceLineProps>`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
${({ withBorderColor }) =>
|
||||
withBorderColor &&
|
||||
css`
|
||||
border: 1px solid gold;
|
||||
`}
|
||||
`;
|
||||
|
||||
const SingleLine = styled(BalanceLine)`
|
||||
margin-bottom: 4px;
|
||||
`;
|
||||
|
||||
const ValueBox = styled.div`
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
padding: 0 8px;
|
||||
font-weight: bolder;
|
||||
`;
|
||||
|
||||
const Value = styled(ValueBox)`
|
||||
right: 50%;
|
||||
text-align: right;
|
||||
`;
|
||||
|
||||
const RightValue = styled(ValueBox)`
|
||||
left: 50%;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
type BalanceProps = {
|
||||
local: number;
|
||||
remote: number;
|
||||
|
|
@ -71,14 +34,27 @@ export const BalanceBars = ({
|
|||
formatRemote !== '0.00';
|
||||
|
||||
return (
|
||||
<BalanceLine withBorderColor={withBorderColor}>
|
||||
{hasLocal && <Value>{formatLocal}</Value>}
|
||||
{hasRemote && <RightValue>{formatRemote}</RightValue>}
|
||||
<div
|
||||
className={cn(
|
||||
'w-full flex relative',
|
||||
withBorderColor && 'border border-[gold]'
|
||||
)}
|
||||
>
|
||||
{hasLocal && (
|
||||
<div className="absolute text-sm px-2 font-bold right-1/2 text-right">
|
||||
{formatLocal}
|
||||
</div>
|
||||
)}
|
||||
{hasRemote && (
|
||||
<div className="absolute text-sm px-2 font-bold left-1/2 text-left">
|
||||
{formatRemote}
|
||||
</div>
|
||||
)}
|
||||
<ProgressBar barHeight={height} order={4} percent={localOpposite} />
|
||||
<ProgressBar barHeight={height} order={1} percent={local} />
|
||||
<ProgressBar barHeight={height} order={2} percent={remote} />
|
||||
<ProgressBar barHeight={height} order={4} percent={remoteOpposite} />
|
||||
</BalanceLine>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -107,10 +83,10 @@ export const SingleBar = ({ value, height }: SingleBarType) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<SingleLine>
|
||||
<div className="w-full flex relative mb-1">
|
||||
<ProgressBar barHeight={height} order={color} percent={value} />
|
||||
<ProgressBar barHeight={height} order={8} percent={opposite} />
|
||||
</SingleLine>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -125,13 +101,13 @@ export const SumBar = ({ values, height = 20 }: SumBarProps) => {
|
|||
const missing = Math.max(100, total) - total;
|
||||
|
||||
return (
|
||||
<SingleLine>
|
||||
<div className="w-full flex relative mb-1">
|
||||
{values.map((value, index) => (
|
||||
<Fragment key={index}>
|
||||
<ProgressBar barHeight={height} order={index % 4} percent={value} />
|
||||
</Fragment>
|
||||
))}
|
||||
<ProgressBar barHeight={height} order={4} percent={missing} />
|
||||
</SingleLine>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +1,8 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { burgerColor } from '../../styles/Themes';
|
||||
import { NodeInfo } from '../../layouts/navigation/nodeInfo/NodeInfo';
|
||||
import { SideSettings } from '../../layouts/navigation/sideSettings/SideSettings';
|
||||
import { Navigation } from '../../layouts/navigation/Navigation';
|
||||
import { LogoutWrapper } from '../logoutButton';
|
||||
import { ColorButton } from '../buttons/colorButton/ColorButton';
|
||||
|
||||
type StyledProps = {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
const StyledBurger = styled.div<StyledProps>`
|
||||
padding: 16px 16px 0;
|
||||
background-color: ${burgerColor};
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
${({ open }) =>
|
||||
open &&
|
||||
css`
|
||||
margin-bottom: 16px;
|
||||
`}
|
||||
`;
|
||||
import { LogoutButton } from '../logoutButton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface BurgerProps {
|
||||
open: boolean;
|
||||
|
|
@ -28,15 +11,21 @@ interface BurgerProps {
|
|||
|
||||
export const BurgerMenu = ({ open, setOpen }: BurgerProps) => {
|
||||
return (
|
||||
<StyledBurger open={open}>
|
||||
<div
|
||||
className={cn(
|
||||
'px-4 pt-4 bg-white dark:bg-[#1a1f35] shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)]',
|
||||
open && 'mb-4'
|
||||
)}
|
||||
>
|
||||
<NodeInfo isBurger={true} />
|
||||
<SideSettings isBurger={true} />
|
||||
<Navigation isBurger={true} setOpen={setOpen} />
|
||||
<LogoutWrapper>
|
||||
<ColorButton fullWidth={true} withMargin={'16px 0'}>
|
||||
Logout
|
||||
</ColorButton>
|
||||
</LogoutWrapper>
|
||||
</StyledBurger>
|
||||
<LogoutButton
|
||||
variant="outline"
|
||||
size="default"
|
||||
className="w-full my-4"
|
||||
label="Logout"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import {
|
||||
textColor,
|
||||
colorButtonBackground,
|
||||
disabledButtonBackground,
|
||||
disabledButtonBorder,
|
||||
disabledTextColor,
|
||||
colorButtonBorder,
|
||||
colorButtonBorderTwo,
|
||||
hoverTextColor,
|
||||
themeColors,
|
||||
mediaWidths,
|
||||
} from '../../../styles/Themes';
|
||||
|
||||
interface GeneralProps {
|
||||
fullWidth?: boolean;
|
||||
mobileFullWidth?: boolean;
|
||||
buttonWidth?: string;
|
||||
withMargin?: string;
|
||||
mobileMargin?: string;
|
||||
}
|
||||
|
||||
const GeneralButton = styled.button<GeneralProps>`
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 8px 16px;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
|
||||
width: ${({ fullWidth, buttonWidth }) =>
|
||||
fullWidth ? '100%' : buttonWidth ? buttonWidth : 'auto'};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
${({ withMargin, mobileMargin }) =>
|
||||
mobileMargin
|
||||
? css`
|
||||
margin: ${mobileMargin};
|
||||
`
|
||||
: withMargin
|
||||
? css`
|
||||
margin: ${withMargin};
|
||||
`
|
||||
: '0'};
|
||||
${({ fullWidth, mobileFullWidth }) =>
|
||||
mobileFullWidth
|
||||
? css`
|
||||
width: 100%;
|
||||
`
|
||||
: fullWidth
|
||||
? css`
|
||||
width: 100%;
|
||||
`
|
||||
: ''};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledArrow = styled.div`
|
||||
margin: 0 -8px -5px 4px;
|
||||
`;
|
||||
|
||||
interface BorderProps {
|
||||
borderColor?: string;
|
||||
selected?: boolean;
|
||||
withBorder?: boolean;
|
||||
backgroundColor?: string | ThemeSet;
|
||||
}
|
||||
|
||||
const BorderButton = styled(GeneralButton)<BorderProps>`
|
||||
${({ selected }) => selected && 'font-weight: 800'};
|
||||
background-color: ${({ backgroundColor }) =>
|
||||
backgroundColor || colorButtonBackground};
|
||||
color: ${textColor};
|
||||
border: 1px solid
|
||||
${({ borderColor, selected, withBorder }) =>
|
||||
withBorder
|
||||
? borderColor
|
||||
? borderColor
|
||||
: colorButtonBorder
|
||||
: selected
|
||||
? colorButtonBorder
|
||||
: colorButtonBorderTwo};
|
||||
|
||||
&:hover {
|
||||
${({ borderColor, selected }: BorderProps) =>
|
||||
!selected
|
||||
? css`
|
||||
border: 1px solid ${colorButtonBackground};
|
||||
background-color: ${borderColor ? borderColor : colorButtonBorder};
|
||||
color: ${hoverTextColor};
|
||||
`
|
||||
: ''};
|
||||
}
|
||||
`;
|
||||
|
||||
const DisabledButton = styled(GeneralButton)`
|
||||
border: none;
|
||||
background-color: ${disabledButtonBackground};
|
||||
color: ${disabledTextColor};
|
||||
border: 1px solid ${disabledButtonBorder};
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const renderArrow = () => (
|
||||
<StyledArrow>
|
||||
<ChevronRight size={18} />
|
||||
</StyledArrow>
|
||||
);
|
||||
|
||||
export interface ColorButtonProps {
|
||||
loading?: boolean;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
arrow?: boolean;
|
||||
onClick?: () => void;
|
||||
withMargin?: string;
|
||||
mobileMargin?: string;
|
||||
withBorder?: boolean;
|
||||
fullWidth?: boolean;
|
||||
mobileFullWidth?: boolean;
|
||||
width?: string;
|
||||
backgroundColor?: string | ThemeSet;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const ColorButton: FC<ColorButtonProps> = ({
|
||||
loading,
|
||||
color,
|
||||
disabled,
|
||||
children,
|
||||
selected,
|
||||
arrow,
|
||||
withMargin,
|
||||
mobileMargin,
|
||||
withBorder,
|
||||
fullWidth,
|
||||
mobileFullWidth,
|
||||
width,
|
||||
onClick,
|
||||
backgroundColor,
|
||||
}) => {
|
||||
if (disabled && !loading) {
|
||||
return (
|
||||
<DisabledButton
|
||||
withMargin={withMargin}
|
||||
mobileMargin={mobileMargin}
|
||||
fullWidth={fullWidth}
|
||||
mobileFullWidth={mobileFullWidth}
|
||||
buttonWidth={width}
|
||||
>
|
||||
{children}
|
||||
{arrow && renderArrow()}
|
||||
</DisabledButton>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<DisabledButton
|
||||
withMargin={withMargin}
|
||||
mobileMargin={mobileMargin}
|
||||
fullWidth={fullWidth}
|
||||
mobileFullWidth={mobileFullWidth}
|
||||
buttonWidth={width}
|
||||
>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={16}
|
||||
style={{ color: themeColors.blue2 }}
|
||||
/>
|
||||
</DisabledButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BorderButton
|
||||
borderColor={color}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
withMargin={withMargin}
|
||||
mobileMargin={mobileMargin}
|
||||
withBorder={withBorder}
|
||||
fullWidth={fullWidth}
|
||||
mobileFullWidth={mobileFullWidth}
|
||||
buttonWidth={width}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{children}
|
||||
{arrow && renderArrow()}
|
||||
</BorderButton>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
multiSelectColor,
|
||||
colorButtonBorder,
|
||||
multiButtonColor,
|
||||
themeColors,
|
||||
} from '../../../styles/Themes';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface StyledSingleProps {
|
||||
selected?: boolean;
|
||||
buttonColor?: string;
|
||||
withPadding?: string;
|
||||
}
|
||||
|
||||
const StyledSingleButton = styled.button<StyledSingleProps>`
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
padding: ${({ withPadding }) => (withPadding ? withPadding : '8px 16px')};
|
||||
background-color: transparent;
|
||||
color: ${multiSelectColor};
|
||||
flex-grow: 1;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
${({ selected, buttonColor }) =>
|
||||
selected
|
||||
? css`
|
||||
color: white;
|
||||
background-color: ${buttonColor ? buttonColor : colorButtonBorder};
|
||||
`
|
||||
: ''};
|
||||
`;
|
||||
|
||||
interface SingleButtonProps {
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
color?: string;
|
||||
withPadding?: string;
|
||||
onClick?: () => void;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const SingleButton: FC<SingleButtonProps> = ({
|
||||
children,
|
||||
disabled,
|
||||
selected,
|
||||
color,
|
||||
withPadding,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<StyledSingleButton
|
||||
disabled={disabled}
|
||||
selected={selected}
|
||||
buttonColor={color}
|
||||
withPadding={withPadding}
|
||||
onClick={() => {
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledSingleButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface MultiBackProps {
|
||||
margin?: string;
|
||||
}
|
||||
|
||||
const MultiBackground = styled.div<MultiBackProps>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
background: ${multiButtonColor};
|
||||
flex-wrap: wrap;
|
||||
|
||||
${({ margin }) => margin && `margin: ${margin}`}
|
||||
`;
|
||||
|
||||
interface MultiButtonProps {
|
||||
margin?: string;
|
||||
loading?: boolean;
|
||||
width?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const MultiButton: FC<MultiButtonProps> = ({
|
||||
children,
|
||||
margin,
|
||||
loading,
|
||||
width = 'auto',
|
||||
}) => {
|
||||
return (
|
||||
<MultiBackground margin={margin}>
|
||||
{loading ? (
|
||||
<div style={{ width, textAlign: 'center' }}>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={21}
|
||||
style={{ color: themeColors.blue3 }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</MultiBackground>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { BarChart as EBarChart } from 'echarts/charts';
|
||||
import {
|
||||
GraphicComponent,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
import * as echarts from 'echarts/core';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
import { timeFormat, timeParse } from 'd3-time-format';
|
||||
import { formatSats } from '../../utils/helpers';
|
||||
import { COMMON_CHART_STYLES } from './common';
|
||||
|
|
@ -40,7 +40,7 @@ export const BarChart = ({
|
|||
title,
|
||||
dataKey,
|
||||
}: BarChartProps) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const themeMode = useThemeMode();
|
||||
|
||||
const seriesData = useMemo(() => {
|
||||
if (data.length === 0) return { dates: [], series: [] };
|
||||
|
|
@ -60,7 +60,7 @@ export const BarChart = ({
|
|||
}, [data, title]);
|
||||
|
||||
const option = useMemo(() => {
|
||||
const fontColor = themeContext?.mode === 'light' ? 'black' : 'white';
|
||||
const fontColor = themeMode === 'light' ? 'black' : 'white';
|
||||
|
||||
return {
|
||||
color: colorRange,
|
||||
|
|
@ -118,7 +118,7 @@ export const BarChart = ({
|
|||
},
|
||||
series: seriesData.series,
|
||||
};
|
||||
}, [colorRange, themeContext, seriesData, title]);
|
||||
}, [colorRange, themeMode, seriesData, title]);
|
||||
|
||||
return (
|
||||
<ReactEChartsCore
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import toast from 'react-hot-toast';
|
|||
import { getErrorContent } from '../../utils/error';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { useMemo } from 'react';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
import { LineChart } from 'echarts/charts';
|
||||
import { Card } from '../generic/Styled';
|
||||
import { chartColors } from '../../styles/Themes';
|
||||
|
|
@ -23,7 +23,7 @@ const getMaxHeight = (arr: number[], rounding?: number): number => {
|
|||
* lnd currently not support filter for channelId, so now it impossible to optimize query.
|
||||
*/
|
||||
export const ChannelCart = ({ channelId, days }: ChannelCartProps) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const themeMode = useThemeMode();
|
||||
const { data } = useGetForwardsQuery({
|
||||
variables: { days: days },
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
|
|
@ -38,8 +38,8 @@ export const ChannelCart = ({ channelId, days }: ChannelCartProps) => {
|
|||
: [];
|
||||
|
||||
// Helper data
|
||||
const fontColor = themeContext?.mode === 'light' ? 'black' : 'white';
|
||||
const oppositeColor = themeContext?.mode === 'light' ? 'white' : 'black';
|
||||
const fontColor = themeMode === 'light' ? 'black' : 'white';
|
||||
const oppositeColor = themeMode === 'light' ? 'white' : 'black';
|
||||
const columnSize = days === 1 ? 24 : days;
|
||||
const now = new Date();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { chartColors } from '../../styles/Themes';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
import { useMemo } from 'react';
|
||||
import { BarChart } from 'echarts/charts';
|
||||
import {
|
||||
GraphicComponent,
|
||||
|
|
@ -44,7 +44,7 @@ export const HorizontalBarChart = ({
|
|||
colorRange = defaultColorRange,
|
||||
dataKey,
|
||||
}: HorizontalBarChartProps) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const themeMode = useThemeMode();
|
||||
|
||||
const keys = Object.keys(data[0] || {}).filter(d => d !== 'label');
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ export const HorizontalBarChart = ({
|
|||
}, [data]);
|
||||
|
||||
const option = useMemo(() => {
|
||||
const themeColor = themeContext?.mode === 'light' ? 'black' : 'white';
|
||||
const themeColor = themeMode === 'light' ? 'black' : 'white';
|
||||
|
||||
return {
|
||||
color: colorRange,
|
||||
|
|
@ -136,7 +136,7 @@ export const HorizontalBarChart = ({
|
|||
legend: { show: true },
|
||||
series: seriesData,
|
||||
};
|
||||
}, [colorRange, themeContext, seriesData, yLabels]);
|
||||
}, [colorRange, themeMode, seriesData, yLabels]);
|
||||
|
||||
if (!keys.length) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,72 +0,0 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useGetMessagesQuery } from '@/graphql/queries/__generated__/getMessages.generated';
|
||||
import { useAccount } from '@/hooks/UseAccount';
|
||||
import { useChatState, useChatDispatch } from '../../context/ChatContext';
|
||||
import { getErrorContent } from '../../utils/error';
|
||||
import { useConfigState } from '../../context/ConfigContext';
|
||||
|
||||
export const ChatFetcher: FC = () => {
|
||||
const newChatToastId = 'newChatToastId';
|
||||
|
||||
const { chatPollingSpeed } = useConfigState();
|
||||
|
||||
const account = useAccount();
|
||||
const { pathname } = useLocation();
|
||||
const { lastChat, chats, sentChats, initialized } = useChatState();
|
||||
const dispatch = useChatDispatch();
|
||||
|
||||
const noChatsAvailable = chats.length <= 0 && sentChats.length <= 0;
|
||||
|
||||
const { data, loading, error } = useGetMessagesQuery({
|
||||
skip: initialized || noChatsAvailable || !account,
|
||||
pollInterval: chatPollingSpeed,
|
||||
fetchPolicy: 'network-only',
|
||||
variables: { initialize: !noChatsAvailable },
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.getMessages?.messages) {
|
||||
const messages = [...data.getMessages.messages];
|
||||
let index = -1;
|
||||
|
||||
if (lastChat !== '') {
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
if (index < 0) {
|
||||
const element = messages[i];
|
||||
if (element?.id === lastChat) {
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
index = 100;
|
||||
}
|
||||
|
||||
if (index < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname !== '/chat') {
|
||||
if (!toast.isActive(newChatToastId)) {
|
||||
toast.success('You have a new message', { position: 'bottom-right' });
|
||||
}
|
||||
}
|
||||
|
||||
const newMessages = messages.slice(0, index);
|
||||
|
||||
if (newMessages?.length) {
|
||||
const last = newMessages[0]?.id || '';
|
||||
dispatch({
|
||||
type: 'additional',
|
||||
chats: newMessages || [],
|
||||
lastChat: last,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [data, loading, error, dispatch, lastChat, pathname]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useGetMessagesLazyQuery } from '@/graphql/queries/__generated__/getMessages.generated';
|
||||
import { useAccount } from '@/hooks/UseAccount';
|
||||
import { useChatDispatch } from '../../context/ChatContext';
|
||||
import { getErrorContent } from '../../utils/error';
|
||||
|
||||
export const ChatInit: FC = () => {
|
||||
const dispatch = useChatDispatch();
|
||||
|
||||
const [
|
||||
getMessages,
|
||||
{ data: initData, loading: initLoading, error: initError },
|
||||
] = useGetMessagesLazyQuery({
|
||||
variables: { initialize: true },
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
|
||||
const account = useAccount();
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
const storageChats =
|
||||
localStorage.getItem(`${account.id}-sentChats`) || '';
|
||||
|
||||
if (storageChats !== '') {
|
||||
try {
|
||||
const savedChats = JSON.parse(storageChats);
|
||||
if (savedChats.length > 0) {
|
||||
const sender = savedChats[0].sender;
|
||||
dispatch({
|
||||
type: 'initialized',
|
||||
sentChats: savedChats,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
localStorage.removeItem('sentChats');
|
||||
}
|
||||
}
|
||||
getMessages();
|
||||
}
|
||||
}, [dispatch, getMessages, account]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initLoading && !initError && initData && initData.getMessages) {
|
||||
const { messages } = initData.getMessages;
|
||||
|
||||
if (!messages?.length) {
|
||||
dispatch({ type: 'initialized' });
|
||||
return;
|
||||
}
|
||||
|
||||
const lastChat = messages[0]?.id || '';
|
||||
const sender = messages[0]?.sender || '';
|
||||
|
||||
dispatch({
|
||||
type: 'initialized',
|
||||
chats: messages,
|
||||
lastChat,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
}, [initLoading, initError, initData, dispatch]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
colorButtonBackground,
|
||||
buttonBorderColor,
|
||||
themeColors,
|
||||
} from '../../styles/Themes';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const FixedWidth = styled.div`
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin: 0px;
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.div<{ checked: boolean }>`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin: 0;
|
||||
border: 1px solid ${buttonBorderColor};
|
||||
border-radius: 4px;
|
||||
outline: none;
|
||||
transition-duration: 0.3s;
|
||||
background-color: ${colorButtonBackground};
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
|
||||
${({ checked }) => checked && `background-color: ${themeColors.blue2}`}
|
||||
`;
|
||||
|
||||
type CheckboxProps = {
|
||||
checked: boolean;
|
||||
onChange: (state: boolean) => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Checkbox: FC<CheckboxProps> = ({
|
||||
children,
|
||||
checked,
|
||||
onChange,
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer onClick={() => onChange(!checked)}>
|
||||
<FixedWidth>
|
||||
<StyledCheckbox checked={checked} />
|
||||
</FixedWidth>
|
||||
{children}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -2,9 +2,9 @@ import { useState } from 'react';
|
|||
import toast from 'react-hot-toast';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import { useUpdateFeesMutation } from '@/graphql/mutations/__generated__/updateFees.generated';
|
||||
import { Input } from '@/components/input';
|
||||
import { InputWithDeco } from '@/components/input/InputWithDeco';
|
||||
import { ColorButton } from '@/components/buttons/colorButton/ColorButton';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Price } from '@/components/price/Price';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { getErrorContent } from '@/utils/error';
|
||||
import { RightAlign } from '../../generic/Styled';
|
||||
|
||||
|
|
@ -39,62 +39,93 @@ export const DetailsChange = ({ callback }: DetailsChangeProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<InputWithDeco
|
||||
title={'BaseFee'}
|
||||
customAmount={baseFeeDirty ? `${baseFee} sats` : ''}
|
||||
noInput={true}
|
||||
>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>BaseFee</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
{baseFeeDirty ? `${baseFee} sats` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={'sats'}
|
||||
maxWidth={`500px`}
|
||||
withMargin={'0 0 0 8px'}
|
||||
mobileMargin={'0'}
|
||||
type={'number'}
|
||||
onChange={e => {
|
||||
setBaseFeeDirty(true);
|
||||
setBaseFee(Number(e.target.value));
|
||||
}}
|
||||
value={baseFee || undefined}
|
||||
value={baseFee || ''}
|
||||
/>
|
||||
</InputWithDeco>
|
||||
<InputWithDeco
|
||||
title={'Fee Rate'}
|
||||
value={feeRate}
|
||||
placeholder={'ppm'}
|
||||
amount={feeRate}
|
||||
override={'ppm'}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setFeeRate(Number(value))}
|
||||
/>
|
||||
<InputWithDeco
|
||||
title={'CLTV Delta'}
|
||||
value={cltv}
|
||||
placeholder={'cltv delta'}
|
||||
customAmount={cltv ? cltv.toString() : ''}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setCLTV(Number(value))}
|
||||
/>
|
||||
<InputWithDeco
|
||||
title={'Max HTLC'}
|
||||
value={max}
|
||||
placeholder={'sats'}
|
||||
amount={max}
|
||||
override={'sat'}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setMax(Number(value))}
|
||||
/>
|
||||
<InputWithDeco
|
||||
title={'Min HTLC'}
|
||||
value={min}
|
||||
placeholder={'sats'}
|
||||
amount={min}
|
||||
override={'sat'}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setMin(Number(value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Fee Rate</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
<Price amount={feeRate} override={'ppm'} />
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={'ppm'}
|
||||
type={'number'}
|
||||
value={feeRate && feeRate > 0 ? feeRate : ''}
|
||||
onChange={e => setFeeRate(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>CLTV Delta</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
{cltv ? cltv.toString() : ''}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={'cltv delta'}
|
||||
type={'number'}
|
||||
value={cltv && cltv > 0 ? cltv : ''}
|
||||
onChange={e => setCLTV(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Max HTLC</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
<Price amount={max} override={'sat'} />
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={'sats'}
|
||||
type={'number'}
|
||||
value={max && max > 0 ? max : ''}
|
||||
onChange={e => setMax(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Min HTLC</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
<Price amount={min} override={'sat'} />
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={'sats'}
|
||||
type={'number'}
|
||||
value={min && min > 0 ? min : ''}
|
||||
onChange={e => setMin(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RightAlign>
|
||||
<ColorButton
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateFees({
|
||||
variables: {
|
||||
|
|
@ -114,12 +145,12 @@ export const DetailsChange = ({ callback }: DetailsChangeProps) => {
|
|||
disabled={
|
||||
baseFee < 0 && feeRate === 0 && cltv === 0 && max === 0 && min === 0
|
||||
}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
className="w-full"
|
||||
style={{ margin: '16px 0 0' }}
|
||||
>
|
||||
Update All Channels
|
||||
<ChevronRight size={18} />
|
||||
</ColorButton>
|
||||
</Button>
|
||||
</RightAlign>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,131 +1,167 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import {
|
||||
progressBackground,
|
||||
mediaWidths,
|
||||
cardColor,
|
||||
cardBorderColor,
|
||||
chartColors,
|
||||
} from '../../styles/Themes';
|
||||
import { forwardRef, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const Progress = styled.div`
|
||||
margin: 5px;
|
||||
background: ${progressBackground};
|
||||
`;
|
||||
// ─── Progress ────────────────────────────────────────────
|
||||
|
||||
type ProgressBar = {
|
||||
export const Progress = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('m-[5px] bg-[#e1e6ed] dark:bg-[#212735]', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
// ─── ProgressBar ─────────────────────────────────────────
|
||||
|
||||
const chartColors = {
|
||||
purple: '#6938f1',
|
||||
lightblue: '#1890ff',
|
||||
green: '#a0d911',
|
||||
orange: '#ffa940',
|
||||
orange2: '#FD5F00',
|
||||
darkyellow: '#ffd300',
|
||||
red: 'red',
|
||||
};
|
||||
|
||||
const orderColors: Record<number, string | null> = {
|
||||
0: chartColors.purple,
|
||||
1: chartColors.lightblue,
|
||||
2: chartColors.green,
|
||||
3: chartColors.orange,
|
||||
4: null, // theme-dependent — handled via class
|
||||
5: chartColors.orange2,
|
||||
6: chartColors.darkyellow,
|
||||
7: chartColors.red,
|
||||
8: 'transparent',
|
||||
};
|
||||
|
||||
interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
|
||||
percent: number;
|
||||
order?: number;
|
||||
barHeight?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const ProgressBar = styled.div.attrs<ProgressBar>(
|
||||
({ order, percent, barHeight }) => {
|
||||
let color: string | ThemeSet = chartColors.purple;
|
||||
switch (order) {
|
||||
case 1:
|
||||
color = chartColors.lightblue;
|
||||
break;
|
||||
case 2:
|
||||
color = chartColors.green;
|
||||
break;
|
||||
case 3:
|
||||
color = chartColors.orange;
|
||||
break;
|
||||
case 4:
|
||||
color = progressBackground;
|
||||
break;
|
||||
case 5:
|
||||
color = chartColors.orange2;
|
||||
break;
|
||||
case 6:
|
||||
color = chartColors.darkyellow;
|
||||
break;
|
||||
case 7:
|
||||
color = chartColors.red;
|
||||
break;
|
||||
case 8:
|
||||
color = 'transparent';
|
||||
break;
|
||||
}
|
||||
export const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(
|
||||
({ percent, order = 0, barHeight, className, style, ...props }, ref) => {
|
||||
const color = orderColors[order] ?? chartColors.purple;
|
||||
const isThemeDependent = order === 4;
|
||||
|
||||
return {
|
||||
style: {
|
||||
'background-color': color,
|
||||
height: barHeight ? `${barHeight}px` : '10px',
|
||||
width: `${percent}%`,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
isThemeDependent && 'bg-[#e1e6ed] dark:bg-[#212735]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: `${percent}%`,
|
||||
height: barHeight ? `${barHeight}px` : '10px',
|
||||
...(!isThemeDependent ? { backgroundColor: color } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)``;
|
||||
);
|
||||
|
||||
export const NodeTitle = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
width: 240px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
// ─── Node / Status ───────────────────────────────────────
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: unset;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
export const NodeTitle = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-base font-bold w-auto mb-2 whitespace-nowrap overflow-hidden text-ellipsis md:w-[240px] md:mb-0 flex items-center',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const StatusLine = styled.div`
|
||||
width: 100%;
|
||||
position: relative;
|
||||
right: -12px;
|
||||
top: -12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 0 0 -8px 0;
|
||||
`;
|
||||
export const StatusLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'w-full relative -right-3 -top-3 flex justify-end mb-[-8px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
type MainProps = {
|
||||
interface MainInfoProps extends HTMLAttributes<HTMLDivElement> {
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const MainInfo = styled.div<MainProps>`
|
||||
${({ disabled }) =>
|
||||
!disabled &&
|
||||
css`
|
||||
cursor: pointer;
|
||||
`}
|
||||
`;
|
||||
export const MainInfo = forwardRef<HTMLDivElement, MainInfoProps>(
|
||||
({ disabled, className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(!disabled && 'cursor-pointer', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const StatusDot = styled.div<{ color: string }>`
|
||||
margin: 0 2px;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
border-radius: 100%;
|
||||
background-color: ${({ color }) => color};
|
||||
`;
|
||||
interface StatusDotProps extends HTMLAttributes<HTMLDivElement> {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export const DetailLine = styled.div`
|
||||
margin: 4px 0;
|
||||
font-size: 14px;
|
||||
word-wrap: break-word;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
export const StatusDot = forwardRef<HTMLDivElement, StatusDotProps>(
|
||||
({ color, className, style, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('mx-0.5 h-2 w-2 rounded-full', className)}
|
||||
style={{ backgroundColor: color, ...style }}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
`;
|
||||
export const DetailLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'my-1 text-sm wrap-break-words flex justify-between flex-wrap md:flex-nowrap',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export interface CardProps {
|
||||
// ─── Card (simpler version without mobile overrides) ─────
|
||||
|
||||
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
bottom?: string;
|
||||
cardPadding?: string;
|
||||
}
|
||||
|
||||
export const Card = styled.div<CardProps>`
|
||||
padding: ${({ cardPadding }) => cardPadding ?? '16px'};
|
||||
background: ${cardColor};
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${cardBorderColor};
|
||||
margin-bottom: ${({ bottom }) => (bottom ? bottom : '25px')};
|
||||
width: 100%;
|
||||
`;
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ bottom, cardPadding, className, style, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-white dark:bg-[#1a1f35] shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)] rounded border border-[#f0f2f8] dark:border-[#20263d] w-full',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
padding: cardPadding ?? '16px',
|
||||
marginBottom: bottom ?? '25px',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,259 +1,359 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import {
|
||||
cardColor,
|
||||
cardBorderColor,
|
||||
subCardColor,
|
||||
smallLinkColor,
|
||||
unSelectedNavButton,
|
||||
textColor,
|
||||
chartLinkColor,
|
||||
inverseTextColor,
|
||||
separationColor,
|
||||
mediaWidths,
|
||||
colorButtonBackground,
|
||||
colorButtonBorder,
|
||||
hoverTextColor,
|
||||
themeColors,
|
||||
} from '../../styles/Themes';
|
||||
forwardRef,
|
||||
HTMLAttributes,
|
||||
AnchorHTMLAttributes,
|
||||
ComponentProps,
|
||||
} from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
export const CardWithTitle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
// ─── Layout ──────────────────────────────────────────────
|
||||
|
||||
export const CardTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
export const CardWithTitle = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex flex-col w-full', className)} {...props} />
|
||||
));
|
||||
|
||||
export interface CardProps {
|
||||
export const CardTitle = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('flex justify-between', className)} {...props} />
|
||||
));
|
||||
|
||||
export const SingleLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex justify-between items-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const RightAlign = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('w-full flex justify-end items-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ColumnLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col w-full md:w-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
interface ResponsiveLineProps extends HTMLAttributes<HTMLDivElement> {
|
||||
withWrap?: boolean;
|
||||
}
|
||||
|
||||
export const ResponsiveLine = forwardRef<HTMLDivElement, ResponsiveLineProps>(
|
||||
({ withWrap, className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-col justify-between items-center w-full md:flex-row',
|
||||
withWrap && 'flex-wrap',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const ResponsiveCol = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('grow w-full md:w-auto', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ResponsiveSingle = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex justify-between items-center grow min-w-[200px] w-full md:w-auto',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
// ─── Card ────────────────────────────────────────────────
|
||||
|
||||
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
bottom?: string;
|
||||
cardPadding?: string;
|
||||
mobileCardPadding?: string;
|
||||
mobileNoBackground?: boolean;
|
||||
}
|
||||
|
||||
export const Card = styled.div<CardProps>`
|
||||
padding: ${({ cardPadding }) => cardPadding ?? '16px'};
|
||||
background: ${cardColor};
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${cardBorderColor};
|
||||
margin-bottom: ${({ bottom }) => (bottom ? bottom : '25px')};
|
||||
width: 100%;
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
(
|
||||
{
|
||||
bottom,
|
||||
cardPadding,
|
||||
mobileCardPadding,
|
||||
mobileNoBackground,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-white dark:bg-[#1a1f35] shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)] rounded border border-[#f0f2f8] dark:border-[#20263d] w-full',
|
||||
mobileNoBackground &&
|
||||
'bg-transparent border-none shadow-none md:bg-white md:dark:bg-[#1a1f35] md:shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)] md:border md:border-[#f0f2f8] md:dark:border-[#20263d]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
padding: cardPadding ?? '16px',
|
||||
marginBottom: bottom ?? '25px',
|
||||
...(mobileCardPadding
|
||||
? ({ '--mobile-padding': mobileCardPadding } as React.CSSProperties)
|
||||
: {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
${({ mobileNoBackground }) =>
|
||||
mobileNoBackground &&
|
||||
css`
|
||||
background: unset;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
`}
|
||||
${({ cardPadding, mobileCardPadding }) =>
|
||||
mobileCardPadding
|
||||
? css`
|
||||
padding: ${mobileCardPadding};
|
||||
`
|
||||
: cardPadding
|
||||
? css`
|
||||
padding: ${cardPadding};
|
||||
`
|
||||
: ''};
|
||||
// Inject a tiny style rule for mobile card padding override
|
||||
if (typeof document !== 'undefined') {
|
||||
const id = 'card-mobile-padding';
|
||||
if (!document.getElementById(id)) {
|
||||
const style = document.createElement('style');
|
||||
style.id = id;
|
||||
style.textContent = `@media (max-width:767px){[style*="--mobile-padding"]{padding:var(--mobile-padding)!important}}`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
`;
|
||||
|
||||
interface SeparationProps {
|
||||
height?: number;
|
||||
lineColor?: string | ThemeSet;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
export const Separation = styled.div<SeparationProps>`
|
||||
height: ${({ height }) => (height ? height : '1')}px;
|
||||
background-color: ${({ lineColor }) => lineColor ?? separationColor};
|
||||
width: 100%;
|
||||
margin: ${({ withMargin }) => withMargin || '16px 0'};
|
||||
`;
|
||||
// ─── SubCard ─────────────────────────────────────────────
|
||||
|
||||
interface SubCardProps {
|
||||
subColor?: string | null;
|
||||
interface SubCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
padding?: string;
|
||||
withMargin?: string;
|
||||
noCard?: boolean;
|
||||
noBackground?: boolean;
|
||||
}
|
||||
|
||||
export const SubCard = styled.div<SubCardProps>`
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
|
||||
padding: ${({ padding }) => (padding ? padding : '16px')};
|
||||
${({ noBackground }) =>
|
||||
!noBackground &&
|
||||
css`
|
||||
background: ${subCardColor};
|
||||
border: 1px solid ${cardBorderColor};
|
||||
`}
|
||||
border-left: ${({ color }) => (color ? `2px solid ${color}` : '')};
|
||||
export const SubCard = forwardRef<HTMLDivElement, SubCardProps>(
|
||||
(
|
||||
{ padding, withMargin, noBackground, color, className, style, ...props },
|
||||
ref
|
||||
) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
!noBackground &&
|
||||
'bg-white dark:bg-[#151727] border border-[#f0f2f8] dark:border-[#20263d]',
|
||||
'hover:shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
margin: withMargin ?? '0 0 10px 0',
|
||||
padding: padding ?? '16px',
|
||||
...(color ? { borderLeft: `2px solid ${color}` } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
// ─── Separation ──────────────────────────────────────────
|
||||
|
||||
export const SmallLink = styled.a`
|
||||
text-decoration: none;
|
||||
color: ${smallLinkColor};
|
||||
interface SeparationProps extends HTMLAttributes<HTMLDivElement> {
|
||||
height?: number;
|
||||
lineColor?: string;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
export const Separation = forwardRef<HTMLDivElement, SeparationProps>(
|
||||
({ height, lineColor, withMargin, className, style, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'w-full',
|
||||
!lineColor && 'bg-[#f0f2f8] dark:bg-[#212735]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
height: `${height ?? 1}px`,
|
||||
margin: withMargin ?? '16px 0',
|
||||
...(lineColor ? { backgroundColor: lineColor } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
type SubTitleProps = {
|
||||
subtitleColor?: string | ThemeSet;
|
||||
// ─── Typography ──────────────────────────────────────────
|
||||
|
||||
interface SubTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
subtitleColor?: string;
|
||||
fontWeight?: string;
|
||||
inverseColor?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const SubTitle = styled.h4<SubTitleProps>`
|
||||
color: ${({ inverseColor }) => (inverseColor ? inverseTextColor : textColor)};
|
||||
margin: 5px 0;
|
||||
${({ subtitleColor }) =>
|
||||
subtitleColor &&
|
||||
css`
|
||||
color: ${subtitleColor};
|
||||
`}
|
||||
font-weight: ${({ fontWeight }) => (fontWeight ? fontWeight : '500')};
|
||||
`;
|
||||
export const SubTitle = forwardRef<HTMLHeadingElement, SubTitleProps>(
|
||||
(
|
||||
{ subtitleColor, fontWeight, inverseColor, className, style, ...props },
|
||||
ref
|
||||
) => (
|
||||
<h4
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'my-[5px]',
|
||||
!subtitleColor &&
|
||||
(inverseColor
|
||||
? 'text-white dark:text-[#212735]'
|
||||
: 'text-[#212735] dark:text-white'),
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
fontWeight: fontWeight ?? '500',
|
||||
...(subtitleColor ? { color: subtitleColor } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const InverseSubtitle = styled(SubTitle)`
|
||||
color: ${inverseTextColor};
|
||||
`;
|
||||
export const InverseSubtitle = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h4
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'my-[5px] font-medium text-white dark:text-[#212735]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const Sub4Title = styled.h5`
|
||||
margin: 10px 0;
|
||||
font-weight: 500;
|
||||
`;
|
||||
export const Sub4Title = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5 ref={ref} className={cn('my-2.5 font-medium', className)} {...props} />
|
||||
));
|
||||
|
||||
export const NoWrapTitle = styled(Sub4Title)`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
export const NoWrapTitle = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('my-2.5 font-medium whitespace-nowrap', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const SingleLine = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const RightAlign = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ColumnLine = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
interface DarkProps {
|
||||
interface DarkSubTitleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
fontSize?: string;
|
||||
withMargin?: string;
|
||||
}
|
||||
|
||||
export const DarkSubTitle = styled.div<DarkProps>`
|
||||
font-size: ${({ fontSize }) => (fontSize ? fontSize : '14px')};
|
||||
color: ${unSelectedNavButton};
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
|
||||
`;
|
||||
export const DarkSubTitle = forwardRef<HTMLDivElement, DarkSubTitleProps>(
|
||||
({ fontSize, withMargin, className, style, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-gray-500', className)}
|
||||
style={{
|
||||
fontSize: fontSize ?? '14px',
|
||||
margin: withMargin ?? '0',
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
type SmallButtonProps = {
|
||||
export const OverflowText = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-right ml-2 wrap-break-words hyphens-auto md:ml-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
// ─── Buttons & Links ─────────────────────────────────────
|
||||
|
||||
interface SmallButtonProps extends ComponentProps<typeof Button> {
|
||||
selected?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const SmallButton = styled.button<SmallButtonProps>`
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
padding: 5px;
|
||||
margin: 5px;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
color: ${({ selected }) => (selected ? hoverTextColor : chartLinkColor)};
|
||||
background-color: ${({ selected }) =>
|
||||
selected ? colorButtonBorder : colorButtonBackground};
|
||||
export const SmallButton = forwardRef<HTMLButtonElement, SmallButtonProps>(
|
||||
({ selected, className, ...props }, ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={selected ? 'default' : 'secondary'}
|
||||
size="xs"
|
||||
className={cn('m-[5px]', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
color: ${hoverTextColor};
|
||||
background-color: ${colorButtonBorder};
|
||||
}
|
||||
`;
|
||||
export const SmallLink = forwardRef<
|
||||
HTMLAnchorElement,
|
||||
AnchorHTMLAttributes<HTMLAnchorElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<a
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'no-underline text-[#9254de] dark:text-[#adc6ff] hover:underline',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const OverflowText = styled.div`
|
||||
text-align: right;
|
||||
margin-left: 16px;
|
||||
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
hyphens: auto;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-left: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveLine = styled(SingleLine)<{ withWrap?: boolean }>`
|
||||
width: 100%;
|
||||
${({ withWrap }) =>
|
||||
withWrap &&
|
||||
css`
|
||||
flex-wrap: wrap;
|
||||
`}
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveCol = styled.div`
|
||||
flex-grow: 1;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ResponsiveSingle = styled(SingleLine)`
|
||||
flex-grow: 1;
|
||||
min-width: 200px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CopyIcon = styled.span`
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
padding: 0 4px;
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${themeColors.blue2};
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
export const CopyIcon = forwardRef<
|
||||
HTMLSpanElement,
|
||||
HTMLAttributes<HTMLSpanElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'cursor-pointer ml-1 px-1 rounded-sm hover:bg-[#6284e4] hover:text-white',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -1,60 +1,43 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { BitcoinFees } from '@/components/bitcoinInfo/BitcoinFees';
|
||||
import { BitcoinPrice } from '@/components/bitcoinInfo/BitcoinPrice';
|
||||
import { mediaWidths } from '../../styles/Themes';
|
||||
import { Section } from '../section/Section';
|
||||
import { Navigation } from '../../layouts/navigation/Navigation';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type GridProps = {
|
||||
noNavigation?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
const Container = styled.div<GridProps>`
|
||||
display: grid;
|
||||
grid-template-areas: 'nav content content';
|
||||
grid-template-columns: auto 1fr 200px;
|
||||
|
||||
${({ noNavigation }) =>
|
||||
!noNavigation &&
|
||||
css`
|
||||
gap: 16px;
|
||||
`}
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
const ContentStyle = styled.div`
|
||||
grid-area: content;
|
||||
`;
|
||||
|
||||
export const GridWrapper: FC<
|
||||
GridProps & { centerContent?: boolean; children?: ReactNode }
|
||||
> = ({ children, centerContent = true, noNavigation }) => (
|
||||
<Section padding={'16px 16px 32px'}>
|
||||
<Container noNavigation={noNavigation}>
|
||||
<div className="w-full bg-[#f5f6f9] dark:bg-[#181c30] md:p-[16px_16px_32px]">
|
||||
<div
|
||||
className={cn(
|
||||
'grid grid-cols-[auto_1fr_200px] [grid-template-areas:"nav_content_content"] md:grid',
|
||||
'flex flex-col md:grid md:grid-cols-[auto_1fr_200px]',
|
||||
!noNavigation && 'gap-4'
|
||||
)}
|
||||
>
|
||||
<BitcoinPrice />
|
||||
<BitcoinFees />
|
||||
{!noNavigation && <Navigation />}
|
||||
<ContentStyle>
|
||||
<div className="[grid-area:content]">
|
||||
{centerContent ? (
|
||||
<Section fixedWidth={true}>{children}</Section>
|
||||
<div className="max-w-[1000px] mx-auto px-4 lg:px-0">{children}</div>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ContentStyle>
|
||||
</Container>
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SimpleWrapper: FC<GridProps> = ({ children }) => (
|
||||
<Section padding={'16px'}>
|
||||
<div className="w-full bg-[#f5f6f9] dark:bg-[#181c30] md:p-4">
|
||||
<BitcoinPrice />
|
||||
<BitcoinFees />
|
||||
{children}
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,114 +0,0 @@
|
|||
import { FC, KeyboardEvent, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { unSelectedNavButton, mediaWidths } from '@/styles/Themes';
|
||||
import { SingleLine } from '../generic/Styled';
|
||||
import { Price } from '../price/Price';
|
||||
import { Input } from '.';
|
||||
|
||||
const NoWrapText = styled.div`
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const InputTitle = styled(NoWrapText)``;
|
||||
|
||||
const AmountText = styled(NoWrapText)`
|
||||
color: ${unSelectedNavButton};
|
||||
margin: 0 8px 0 16px;
|
||||
`;
|
||||
|
||||
const InputTitleRow = styled.div`
|
||||
display: flex;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputLine = styled(SingleLine)`
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
type InputWithDecoProps = {
|
||||
inputMaxWidth?: string;
|
||||
title: string;
|
||||
value?: string | number | null;
|
||||
noInput?: boolean;
|
||||
amount?: number | null;
|
||||
override?: string;
|
||||
customAmount?: string | JSX.Element;
|
||||
color?: string;
|
||||
placeholder?: string;
|
||||
inputType?: string;
|
||||
inputCallback?: (value: string) => void;
|
||||
blurCallback?: (value: string) => void;
|
||||
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEnter?: () => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const InputWithDeco: FC<InputWithDecoProps> = ({
|
||||
title,
|
||||
value,
|
||||
amount,
|
||||
override,
|
||||
customAmount,
|
||||
children,
|
||||
placeholder,
|
||||
color,
|
||||
noInput,
|
||||
inputMaxWidth,
|
||||
inputType = 'text',
|
||||
inputCallback,
|
||||
blurCallback,
|
||||
onKeyDown,
|
||||
onEnter,
|
||||
}) => {
|
||||
const showAmount = !!amount || customAmount;
|
||||
let correctValue = value ? value : '';
|
||||
|
||||
if (inputType === 'number' && typeof value === 'number') {
|
||||
correctValue = value && value > 0 ? value : '';
|
||||
}
|
||||
|
||||
const onKeyDownProp = onKeyDown ? { onKeyDown } : onEnter ? { onEnter } : {};
|
||||
const props = noInput ? {} : { value: correctValue };
|
||||
|
||||
return (
|
||||
<InputLine>
|
||||
<InputTitleRow>
|
||||
<InputTitle>{title}</InputTitle>
|
||||
{showAmount && (
|
||||
<AmountText>
|
||||
{customAmount ? (
|
||||
customAmount
|
||||
) : (
|
||||
<Price amount={amount} override={override} />
|
||||
)}
|
||||
</AmountText>
|
||||
)}
|
||||
</InputTitleRow>
|
||||
{!noInput && (
|
||||
<Input
|
||||
maxWidth={inputMaxWidth || '500px'}
|
||||
placeholder={placeholder}
|
||||
color={color}
|
||||
withMargin={'0 0 0 8px'}
|
||||
mobileMargin={'0'}
|
||||
type={inputType}
|
||||
onChange={e => inputCallback && inputCallback(e.target.value)}
|
||||
onBlur={e => blurCallback && blurCallback(e.target.value)}
|
||||
{...onKeyDownProp}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</InputLine>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
import { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import {
|
||||
textColor,
|
||||
colorButtonBorder,
|
||||
inputBackgroundColor,
|
||||
inputBorderColor,
|
||||
mediaWidths,
|
||||
} from '../../styles/Themes';
|
||||
|
||||
interface InputProps {
|
||||
color?: string;
|
||||
backgroundColor?: ThemeSet | string;
|
||||
withMargin?: string;
|
||||
mobileMargin?: string;
|
||||
fullWidth?: boolean;
|
||||
mobileFullWidth?: boolean;
|
||||
maxWidth?: string;
|
||||
}
|
||||
|
||||
export const StyledInput = styled.input<InputProps>`
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
height: 38px;
|
||||
margin: 8px 0;
|
||||
border: 1px solid ${inputBorderColor};
|
||||
background: none;
|
||||
border-radius: 5px;
|
||||
color: ${textColor};
|
||||
background-color: ${({ backgroundColor }) =>
|
||||
backgroundColor || inputBackgroundColor};
|
||||
${({ maxWidth }) =>
|
||||
maxWidth &&
|
||||
css`
|
||||
max-width: ${maxWidth};
|
||||
`}
|
||||
width: ${({ fullWidth }) => (fullWidth ? '100%' : 'auto')};
|
||||
margin: ${({ withMargin }) => (withMargin ? withMargin : '0')};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
${({ withMargin, mobileMargin }) =>
|
||||
mobileMargin
|
||||
? css`
|
||||
margin: ${mobileMargin};
|
||||
`
|
||||
: withMargin
|
||||
? css`
|
||||
margin: ${withMargin};
|
||||
`
|
||||
: ''};
|
||||
${({ fullWidth, mobileFullWidth }) =>
|
||||
mobileFullWidth
|
||||
? css`
|
||||
width: 100%;
|
||||
`
|
||||
: fullWidth
|
||||
? css`
|
||||
width: 100%;
|
||||
`
|
||||
: ''};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: 1px solid ${({ color }) => (color ? color : colorButtonBorder)};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: 1px solid ${({ color }) => (color ? color : colorButtonBorder)};
|
||||
}
|
||||
`;
|
||||
|
||||
interface InputCompProps {
|
||||
type?: string;
|
||||
value?: number | string;
|
||||
placeholder?: string;
|
||||
color?: string;
|
||||
backgroundColor?: ThemeSet | string;
|
||||
withMargin?: string;
|
||||
mobileMargin?: string;
|
||||
fullWidth?: boolean;
|
||||
mobileFullWidth?: boolean;
|
||||
maxWidth?: string;
|
||||
autoFocus?: boolean;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onBlur?: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEnter?: () => void;
|
||||
}
|
||||
|
||||
export const Input = ({
|
||||
type,
|
||||
value,
|
||||
placeholder,
|
||||
color,
|
||||
backgroundColor,
|
||||
withMargin,
|
||||
mobileMargin,
|
||||
mobileFullWidth,
|
||||
fullWidth = true,
|
||||
maxWidth,
|
||||
onChange,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
onEnter,
|
||||
autoFocus,
|
||||
}: InputCompProps) => {
|
||||
return (
|
||||
<StyledInput
|
||||
autoFocus={autoFocus}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
color={color}
|
||||
backgroundColor={backgroundColor}
|
||||
withMargin={withMargin}
|
||||
mobileMargin={mobileMargin}
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
fullWidth={fullWidth}
|
||||
mobileFullWidth={mobileFullWidth}
|
||||
maxWidth={maxWidth}
|
||||
onKeyDown={e => {
|
||||
if (onEnter && e.key === 'Enter') {
|
||||
onEnter();
|
||||
} else {
|
||||
if (onKeyDown) onKeyDown(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,77 +1,11 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { textColor, linkHighlight } from '../../styles/Themes';
|
||||
|
||||
interface StyledProps {
|
||||
fontColor?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
const StyledSpan = styled.span<StyledProps>`
|
||||
cursor: pointer;
|
||||
color: ${({ fontColor, inheritColor }) =>
|
||||
inheritColor ? 'inherit' : (fontColor ?? textColor)};
|
||||
text-decoration: none;
|
||||
${({ fullWidth }: StyledProps) =>
|
||||
fullWidth &&
|
||||
css`
|
||||
width: 100%;
|
||||
`};
|
||||
|
||||
:hover {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 0%,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 100%
|
||||
);
|
||||
background-position: 0 100%;
|
||||
background-size: 2px 2px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
`;
|
||||
|
||||
const NoStylingSpan = styled.span`
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a<StyledProps>`
|
||||
cursor: pointer;
|
||||
color: ${({ fontColor, inheritColor }) =>
|
||||
inheritColor ? 'inherit' : (fontColor ?? textColor)};
|
||||
text-decoration: none;
|
||||
${({ fullWidth }: StyledProps) =>
|
||||
fullWidth &&
|
||||
css`
|
||||
width: 100%;
|
||||
`};
|
||||
|
||||
:hover {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 0%,
|
||||
${({ underline }: StyledProps) => underline ?? linkHighlight} 100%
|
||||
);
|
||||
background-position: 0 100%;
|
||||
background-size: 2px 2px;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
`;
|
||||
|
||||
const NoStyling = styled.a`
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
`;
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LinkProps {
|
||||
href?: string;
|
||||
to?: string;
|
||||
color?: string | ThemeSet;
|
||||
underline?: string | ThemeSet;
|
||||
color?: string;
|
||||
inheritColor?: boolean;
|
||||
fullWidth?: boolean;
|
||||
noStyling?: boolean;
|
||||
|
|
@ -79,40 +13,59 @@ interface LinkProps {
|
|||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const getLinkClass = (opts: {
|
||||
inheritColor?: boolean;
|
||||
color?: string;
|
||||
fullWidth?: boolean;
|
||||
noStyling?: boolean;
|
||||
}) => {
|
||||
if (opts.noStyling) {
|
||||
return 'cursor-pointer no-underline';
|
||||
}
|
||||
|
||||
return cn(
|
||||
'cursor-pointer no-underline hover:underline hover:decoration-[#5163ba] hover:decoration-2 hover:underline-offset-2',
|
||||
opts.inheritColor
|
||||
? 'text-inherit'
|
||||
: !opts.color && 'text-[#212735] dark:text-white',
|
||||
opts.fullWidth && 'w-full'
|
||||
);
|
||||
};
|
||||
|
||||
export const Link: FC<LinkProps> = ({
|
||||
children,
|
||||
href,
|
||||
to,
|
||||
color,
|
||||
underline,
|
||||
inheritColor,
|
||||
fullWidth,
|
||||
noStyling,
|
||||
newTab,
|
||||
}) => {
|
||||
const props = { fontColor: color, underline, inheritColor, fullWidth };
|
||||
|
||||
if (!href && !to) return null;
|
||||
|
||||
const CorrectLink = noStyling ? NoStyling : StyledLink;
|
||||
const className = getLinkClass({ inheritColor, color, fullWidth, noStyling });
|
||||
const style = color && !inheritColor ? { color } : undefined;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<CorrectLink
|
||||
<a
|
||||
href={href}
|
||||
{...props}
|
||||
className={className}
|
||||
style={style}
|
||||
{...(newTab && { target: '_blank', rel: 'noreferrer noopener' })}
|
||||
>
|
||||
{children}
|
||||
</CorrectLink>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (to) {
|
||||
const CorrectSpan = noStyling ? NoStylingSpan : StyledSpan;
|
||||
return (
|
||||
<RouterLink to={to} style={{ textDecoration: 'none' }}>
|
||||
<CorrectSpan {...props}>{children}</CorrectSpan>
|
||||
<span className={className} style={style}>
|
||||
{children}
|
||||
</span>
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
import { Loader2 } from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { CardWithTitle, CardTitle, SubTitle, Card } from '../generic/Styled';
|
||||
import { themeColors } from '../../styles/Themes';
|
||||
|
||||
const Loading = styled.div<{ loadingHeight?: string }>`
|
||||
width: 100%;
|
||||
height: ${({ loadingHeight }) => (loadingHeight ? loadingHeight : 'auto')};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface LoadingCardProps {
|
||||
title?: string;
|
||||
|
|
@ -20,6 +10,21 @@ interface LoadingCardProps {
|
|||
inverseColor?: boolean;
|
||||
}
|
||||
|
||||
const Spinner = ({
|
||||
loadingHeight,
|
||||
color,
|
||||
}: {
|
||||
loadingHeight?: string;
|
||||
color: string;
|
||||
}) => (
|
||||
<div
|
||||
className="w-full flex justify-center items-center"
|
||||
style={{ height: loadingHeight || 'auto' }}
|
||||
>
|
||||
<Loader2 className="animate-spin" size={20} style={{ color }} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LoadingCard = ({
|
||||
title = '',
|
||||
color,
|
||||
|
|
@ -28,30 +33,16 @@ export const LoadingCard = ({
|
|||
loadingHeight,
|
||||
inverseColor,
|
||||
}: LoadingCardProps) => {
|
||||
const loadingColor = color ? color : themeColors.blue3;
|
||||
const loadingColor = color || '#5163ba';
|
||||
|
||||
if (noCard) {
|
||||
return (
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={20}
|
||||
style={{ color: loadingColor }}
|
||||
/>
|
||||
</Loading>
|
||||
);
|
||||
return <Spinner loadingHeight={loadingHeight} color={loadingColor} />;
|
||||
}
|
||||
|
||||
if (noTitle) {
|
||||
return (
|
||||
<Card>
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={20}
|
||||
style={{ color: loadingColor }}
|
||||
/>
|
||||
</Loading>
|
||||
<Spinner loadingHeight={loadingHeight} color={loadingColor} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -62,13 +53,7 @@ export const LoadingCard = ({
|
|||
<SubTitle inverseColor={inverseColor}>{title}</SubTitle>
|
||||
</CardTitle>
|
||||
<Card>
|
||||
<Loading loadingHeight={loadingHeight}>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={20}
|
||||
style={{ color: loadingColor }}
|
||||
/>
|
||||
</Loading>
|
||||
<Spinner loadingHeight={loadingHeight} color={loadingColor} />
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { progressBackground } from '../../styles/Themes';
|
||||
|
||||
const Progress = styled.div`
|
||||
width: 100%;
|
||||
background: ${progressBackground};
|
||||
`;
|
||||
|
||||
interface ProgressBar {
|
||||
percent: number;
|
||||
barColor?: string;
|
||||
}
|
||||
|
||||
const ProgressBar = styled.div<ProgressBar>`
|
||||
height: 10px;
|
||||
background-color: ${({ barColor }) => (barColor ? barColor : 'blue')};
|
||||
width: ${({ percent }: ProgressBar) => `${percent}%`};
|
||||
`;
|
||||
|
||||
const getColor = (percent: number) => {
|
||||
switch (true) {
|
||||
case percent < 20:
|
||||
return '#ff4d4f';
|
||||
case percent < 40:
|
||||
return '#ff7a45';
|
||||
case percent < 60:
|
||||
return '#ffa940';
|
||||
case percent < 80:
|
||||
return '#bae637';
|
||||
case percent <= 100:
|
||||
return '#73d13d';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const LoadingBar = ({ percent }: { percent: number }) => (
|
||||
<Progress>
|
||||
<ProgressBar percent={percent} barColor={getColor(percent)} />
|
||||
</Progress>
|
||||
);
|
||||
|
|
@ -1,108 +1,52 @@
|
|||
import { FC, ReactNode, useEffect } from 'react';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { LogOut, Loader2 } from 'lucide-react';
|
||||
import { useLogoutMutation } from '@/graphql/mutations/__generated__/logout.generated';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { HeaderNavButton } from '@/layouts/header/Header.styled';
|
||||
import styled from 'styled-components';
|
||||
import { themeColors } from '@/styles/Themes';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { config } from '../../config/thunderhubConfig';
|
||||
import { safeRedirect } from '../../utils/url';
|
||||
import { useChatDispatch } from '../../context/ChatContext';
|
||||
import { Button, buttonVariants } from '../ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
|
||||
const Logout = styled.button`
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
interface LogoutButtonProps extends VariantProps<typeof buttonVariants> {
|
||||
className?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const LogoutWrapperStyled = styled(Logout)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const LogoutWrapper: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
export const LogoutButton = ({
|
||||
variant = 'ghost',
|
||||
size = 'icon-sm',
|
||||
className,
|
||||
label,
|
||||
}: LogoutButtonProps) => {
|
||||
const client = useApolloClient();
|
||||
|
||||
const dispatchChat = useChatDispatch();
|
||||
|
||||
const [logout, { data, loading }] = useLogoutMutation({
|
||||
refetchQueries: ['GetServerAccounts'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.logout) {
|
||||
dispatchChat({ type: 'disconnected' });
|
||||
client.clearStore();
|
||||
|
||||
safeRedirect(
|
||||
config.logoutUrl || `${config.basePath}/login`,
|
||||
`${config.basePath}/login`
|
||||
);
|
||||
}
|
||||
}, [data, dispatchChat, client]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<LogoutWrapperStyled>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={14}
|
||||
style={{ color: themeColors.blue3 }}
|
||||
/>
|
||||
</LogoutWrapperStyled>
|
||||
);
|
||||
}
|
||||
}, [data, client]);
|
||||
|
||||
return (
|
||||
<LogoutWrapperStyled onClick={() => logout()}>
|
||||
{children}
|
||||
</LogoutWrapperStyled>
|
||||
);
|
||||
};
|
||||
|
||||
export const LogoutButton = () => {
|
||||
const client = useApolloClient();
|
||||
|
||||
const dispatchChat = useChatDispatch();
|
||||
|
||||
const [logout, { data, loading }] = useLogoutMutation({
|
||||
refetchQueries: ['GetServerAccounts'],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.logout) {
|
||||
dispatchChat({ type: 'disconnected' });
|
||||
client.clearStore();
|
||||
|
||||
safeRedirect(
|
||||
config.logoutUrl || `${config.basePath}/login`,
|
||||
`${config.basePath}/login`
|
||||
);
|
||||
}
|
||||
}, [data, dispatchChat, client]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Logout>
|
||||
<HeaderNavButton>
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={14}
|
||||
style={{ color: themeColors.blue3 }}
|
||||
/>
|
||||
</HeaderNavButton>
|
||||
</Logout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Logout onClick={() => logout()}>
|
||||
<HeaderNavButton>
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn(className)}
|
||||
onClick={() => !loading && logout()}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<LogOut size={18} />
|
||||
</HeaderNavButton>
|
||||
</Logout>
|
||||
)}
|
||||
{label && <span>{label}</span>}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import { useState } from 'react';
|
|||
import toast from 'react-hot-toast';
|
||||
import { useUpdateFeesMutation } from '@/graphql/mutations/__generated__/updateFees.generated';
|
||||
import { getErrorContent } from '@/utils/error';
|
||||
import { InputWithDeco } from '@/components/input/InputWithDeco';
|
||||
import { ColorButton } from '@/components/buttons/colorButton/ColorButton';
|
||||
import { Input } from '@/components/input';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Price } from '@/components/price/Price';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SingleLine, SubTitle, Sub4Title } from '../../generic/Styled';
|
||||
|
||||
type ChangeDetailsType = {
|
||||
|
|
@ -70,66 +70,88 @@ export const ChangeDetails = ({
|
|||
<SubTitle>{'Update Channel Policy'}</SubTitle>
|
||||
<Sub4Title>{`${name} [${id}]`}</Sub4Title>
|
||||
</SingleLine>
|
||||
<InputWithDeco
|
||||
title={'Base Fee'}
|
||||
customAmount={`${newBaseFee} sats`}
|
||||
noInput={true}
|
||||
>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Base Fee</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
{`${newBaseFee} sats`}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
maxWidth={'160px'}
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '160px' }}
|
||||
placeholder={'sats'}
|
||||
withMargin={'0 0 0 8px'}
|
||||
mobileMargin={'0'}
|
||||
type={'number'}
|
||||
onChange={e => setBaseFee(Number(e.target.value))}
|
||||
value={newBaseFee || undefined}
|
||||
value={newBaseFee || ''}
|
||||
/>
|
||||
</InputWithDeco>
|
||||
<InputWithDeco
|
||||
title={'Fee Rate'}
|
||||
customAmount={`${feeRatePercent}%`}
|
||||
noInput={true}
|
||||
>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Fee Rate</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
{`${feeRatePercent}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
maxWidth={'160px'}
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '160px' }}
|
||||
placeholder={'ppm'}
|
||||
withMargin={'0 0 0 8px'}
|
||||
mobileMargin={'0'}
|
||||
type={'number'}
|
||||
onChange={e => setFeeRate(Number(e.target.value))}
|
||||
value={newFeeRate || undefined}
|
||||
value={newFeeRate || ''}
|
||||
/>
|
||||
</InputWithDeco>
|
||||
<InputWithDeco
|
||||
title={'CLTV Delta'}
|
||||
value={newCLTV}
|
||||
placeholder={'cltv delta'}
|
||||
customAmount={newCLTV?.toString() || ''}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setCLTV(Number(value))}
|
||||
inputMaxWidth={'160px'}
|
||||
/>
|
||||
<InputWithDeco
|
||||
title={'Max HTLC'}
|
||||
value={newMax}
|
||||
placeholder={'sats'}
|
||||
amount={newMax}
|
||||
override={'sat'}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setMax(Number(value))}
|
||||
inputMaxWidth={'160px'}
|
||||
/>
|
||||
<InputWithDeco
|
||||
title={'Min HTLC'}
|
||||
value={newMin}
|
||||
placeholder={'sats'}
|
||||
amount={newMin}
|
||||
override={'sat'}
|
||||
inputType={'number'}
|
||||
inputCallback={value => setMin(Number(value))}
|
||||
inputMaxWidth={'160px'}
|
||||
/>
|
||||
<ColorButton
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>CLTV Delta</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
{newCLTV?.toString() || ''}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '160px' }}
|
||||
placeholder={'cltv delta'}
|
||||
type={'number'}
|
||||
value={newCLTV != null && newCLTV > 0 ? newCLTV : ''}
|
||||
onChange={e => setCLTV(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Max HTLC</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
<Price amount={newMax} override={'sat'} />
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '160px' }}
|
||||
placeholder={'sats'}
|
||||
type={'number'}
|
||||
value={newMax && newMax > 0 ? newMax : ''}
|
||||
onChange={e => setMax(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>Min HTLC</span>
|
||||
<span className="text-muted-foreground mx-2 ml-4">
|
||||
<Price amount={newMin} override={'sat'} />
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '160px' }}
|
||||
placeholder={'sats'}
|
||||
type={'number'}
|
||||
value={newMin && newMin > 0 ? newMin : ''}
|
||||
onChange={e => setMin(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
updateFees({
|
||||
variables: {
|
||||
|
|
@ -152,11 +174,11 @@ export const ChangeDetails = ({
|
|||
})
|
||||
}
|
||||
disabled={!withChanges}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
className="w-full"
|
||||
style={{ margin: '16px 0 0' }}
|
||||
>
|
||||
Update Channel Details
|
||||
</ColorButton>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import { useState } from 'react';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { AlertTriangle, ChevronRight, Loader2 } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useCloseChannelMutation } from '@/graphql/mutations/__generated__/closeChannel.generated';
|
||||
import { useBitcoinFees } from '@/hooks/UseBitcoinFees';
|
||||
import { useConfigState } from '@/context/ConfigContext';
|
||||
import { renderLine } from '@/components/generic/helpers';
|
||||
import { InputWithDeco } from '@/components/input/InputWithDeco';
|
||||
import { chartColors } from '@/styles/Themes';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Separation,
|
||||
SingleLine,
|
||||
|
|
@ -16,23 +14,8 @@ import {
|
|||
DarkSubTitle,
|
||||
} from '../../generic/Styled';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
import { ColorButton } from '../../buttons/colorButton/ColorButton';
|
||||
import {
|
||||
MultiButton,
|
||||
SingleButton,
|
||||
} from '../../buttons/multiButton/MultiButton';
|
||||
|
||||
const Warning = styled.div`
|
||||
font-size: 14px;
|
||||
color: ${chartColors.orange};
|
||||
`;
|
||||
|
||||
const WarningCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type CloseChannelProps = {
|
||||
channelId: string;
|
||||
|
|
@ -73,13 +56,17 @@ export const CloseChannel = ({
|
|||
text: string,
|
||||
selected: boolean
|
||||
) => (
|
||||
<SingleButton selected={selected} onClick={onClick}>
|
||||
<Button
|
||||
variant={selected ? 'default' : 'ghost'}
|
||||
onClick={() => onClick()}
|
||||
className={cn('grow', !selected && 'text-foreground')}
|
||||
>
|
||||
{text}
|
||||
</SingleButton>
|
||||
</Button>
|
||||
);
|
||||
|
||||
const renderWarning = () => (
|
||||
<WarningCard>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<AlertTriangle size={32} color={'red'} />
|
||||
<SubTitle>Are you sure you want to close the channel?</SubTitle>
|
||||
<Separation />
|
||||
|
|
@ -98,12 +85,11 @@ export const CloseChannel = ({
|
|||
<DarkSubTitle>This is a force close</DarkSubTitle>
|
||||
)}
|
||||
<Separation />
|
||||
<ColorButton
|
||||
fullWidth={true}
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
disabled={(loading || !amount) && !isForce}
|
||||
loading={loading}
|
||||
withMargin={'16px 4px 4px'}
|
||||
color={'red'}
|
||||
style={{ margin: '16px 4px 4px' }}
|
||||
onClick={() => {
|
||||
let details:
|
||||
| { target: number }
|
||||
|
|
@ -124,17 +110,22 @@ export const CloseChannel = ({
|
|||
});
|
||||
}}
|
||||
>
|
||||
{`Close Channel [ ${channelName}/${channelId} ]`}
|
||||
</ColorButton>
|
||||
<ColorButton
|
||||
fullWidth={true}
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{`Close Channel [ ${channelName}/${channelId} ]`}</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
withMargin={'4px'}
|
||||
style={{ margin: '4px' }}
|
||||
onClick={() => setIsConfirmed(false)}
|
||||
>
|
||||
Cancel
|
||||
</ColorButton>
|
||||
</WarningCard>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderContent = () => (
|
||||
|
|
@ -147,7 +138,7 @@ export const CloseChannel = ({
|
|||
<SingleLine>
|
||||
<Sub4Title>Force Close Channel:</Sub4Title>
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
<div className="flex justify-center items-center rounded-md p-1 bg-secondary flex-wrap">
|
||||
{renderButton(
|
||||
() => {
|
||||
setAmount(undefined);
|
||||
|
|
@ -157,14 +148,18 @@ export const CloseChannel = ({
|
|||
isForce
|
||||
)}
|
||||
{renderButton(() => setIsForce(false), 'No', !isForce)}
|
||||
</MultiButton>
|
||||
</div>
|
||||
{!isForce && (
|
||||
<>
|
||||
<SingleLine>
|
||||
<Sub4Title>Fee:</Sub4Title>
|
||||
{!dontShow && <Warning>{`Minimum: ${minimum} sats/vByte`}</Warning>}
|
||||
{!dontShow && (
|
||||
<span className="text-sm text-[#ffa940]">
|
||||
{`Minimum: ${minimum} sats/vByte`}
|
||||
</span>
|
||||
)}
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
<div className="flex justify-center items-center rounded-md p-1 bg-secondary flex-wrap">
|
||||
{fetchFees &&
|
||||
!dontShow &&
|
||||
renderButton(
|
||||
|
|
@ -191,7 +186,7 @@ export const CloseChannel = ({
|
|||
'Target',
|
||||
isType === 'target'
|
||||
)}
|
||||
</MultiButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isType === 'none' && !isForce && (
|
||||
|
|
@ -199,7 +194,7 @@ export const CloseChannel = ({
|
|||
<SingleLine>
|
||||
<Sub4Title>Fee Amount:</Sub4Title>
|
||||
</SingleLine>
|
||||
<MultiButton>
|
||||
<div className="flex justify-center items-center rounded-md p-1 bg-secondary flex-wrap">
|
||||
{renderButton(
|
||||
() => setAmount(fast),
|
||||
`Fastest (${fast} sats)`,
|
||||
|
|
@ -216,29 +211,35 @@ export const CloseChannel = ({
|
|||
`Hour (${hour} sats)`,
|
||||
amount === hour
|
||||
)}
|
||||
</MultiButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{isType !== 'none' && !isForce && (
|
||||
<InputWithDeco
|
||||
title={isType === 'target' ? 'Target Blocks:' : 'Fee (Sats/Byte)'}
|
||||
placeholder={isType === 'target' ? 'Blocks' : 'Sats/Byte'}
|
||||
value={amount}
|
||||
inputType={'number'}
|
||||
inputCallback={e => setAmount(Number(e))}
|
||||
/>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>
|
||||
{isType === 'target' ? 'Target Blocks:' : 'Fee (Sats/Byte)'}
|
||||
</span>
|
||||
</div>
|
||||
<Input
|
||||
className="ml-0 md:ml-2"
|
||||
style={{ maxWidth: '500px' }}
|
||||
placeholder={isType === 'target' ? 'Blocks' : 'Sats/Byte'}
|
||||
type={'number'}
|
||||
value={amount != null && amount > 0 ? amount : ''}
|
||||
onChange={e => setAmount(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ColorButton
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={!amount && !isForce}
|
||||
arrow={true}
|
||||
fullWidth={true}
|
||||
withMargin={'32px 0 0'}
|
||||
withBorder={true}
|
||||
color={'red'}
|
||||
className="w-full"
|
||||
style={{ margin: '32px 0 0' }}
|
||||
onClick={() => setIsConfirmed(true)}
|
||||
>
|
||||
Close Channel
|
||||
</ColorButton>
|
||||
Close Channel <ChevronRight size={18} />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { AlertTriangle } from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useRemovePeerMutation } from '@/graphql/mutations/__generated__/removePeer.generated';
|
||||
import { SubTitle } from '../../generic/Styled';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
import { ColorButton } from '../../buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface RemovePeerProps {
|
||||
setModalOpen: (status: boolean) => void;
|
||||
|
|
@ -12,13 +11,6 @@ interface RemovePeerProps {
|
|||
peerAlias: string;
|
||||
}
|
||||
|
||||
const WarningCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const RemovePeerModal = ({
|
||||
setModalOpen,
|
||||
publicKey,
|
||||
|
|
@ -27,6 +19,7 @@ export const RemovePeerModal = ({
|
|||
const [removePeer, { loading }] = useRemovePeerMutation({
|
||||
onCompleted: () => {
|
||||
toast.success('Peer Removed');
|
||||
setModalOpen(false);
|
||||
},
|
||||
onError: error => {
|
||||
toast.error(getErrorContent(error));
|
||||
|
|
@ -37,23 +30,30 @@ export const RemovePeerModal = ({
|
|||
const handleOnlyClose = () => setModalOpen(false);
|
||||
|
||||
return (
|
||||
<WarningCard>
|
||||
<div className="flex flex-col justify-center items-center">
|
||||
<AlertTriangle size={32} color={'red'} />
|
||||
<SubTitle>Are you sure you want to remove this peer?</SubTitle>
|
||||
<ColorButton
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
removePeer({ variables: { publicKey } });
|
||||
}}
|
||||
color={'red'}
|
||||
disabled={loading}
|
||||
loading={loading}
|
||||
withMargin={'4px'}
|
||||
style={{ margin: '4px' }}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{`Remove Peer [${peerAlias || publicKey?.substring(0, 6)}]`}</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
style={{ margin: '4px' }}
|
||||
onClick={handleOnlyClose}
|
||||
>
|
||||
{`Remove Peer [${peerAlias || publicKey?.substring(0, 6)}]`}
|
||||
</ColorButton>
|
||||
<ColorButton withMargin={'4px'} onClick={handleOnlyClose}>
|
||||
Cancel
|
||||
</ColorButton>
|
||||
</WarningCard>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { chartColors, mediaWidths } from '@/styles/Themes';
|
||||
|
||||
export const BetaNotification = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: ${chartColors.orange};
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
margin-bottom: 16px;
|
||||
padding: 4px 0;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
GraphicComponent,
|
||||
GridComponent,
|
||||
|
|
@ -11,7 +11,7 @@ import * as echarts from 'echarts/core';
|
|||
import { SankeyChart } from 'echarts/charts';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import ReactEChartsCore from 'echarts-for-react/lib/core';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||
|
||||
echarts.use([
|
||||
SankeyChart,
|
||||
|
|
@ -46,10 +46,10 @@ export interface SankeyData {
|
|||
}
|
||||
|
||||
export const Sankey = ({ data, width, height }: SankeyProps) => {
|
||||
const themeContext = useContext(ThemeContext);
|
||||
const themeMode = useThemeMode();
|
||||
|
||||
const option = useMemo(() => {
|
||||
const fontColor = themeContext?.mode === 'light' ? 'black' : 'white';
|
||||
const fontColor = themeMode === 'light' ? 'black' : 'white';
|
||||
return {
|
||||
resize: true,
|
||||
tooltip: {
|
||||
|
|
@ -86,7 +86,7 @@ export const Sankey = ({ data, width, height }: SankeyProps) => {
|
|||
},
|
||||
],
|
||||
};
|
||||
}, [data, themeContext, height]);
|
||||
}, [data, themeMode, height]);
|
||||
|
||||
return (
|
||||
<ReactEChartsCore
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import { FC, Fragment, ReactNode } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import { backgroundColor, mediaWidths } from '../../styles/Themes';
|
||||
|
||||
interface FullWidthProps {
|
||||
padding?: string;
|
||||
withColor?: boolean;
|
||||
sectionColor?: string | ThemeSet;
|
||||
textColor?: string | ThemeSet;
|
||||
}
|
||||
|
||||
const FullWidth = styled.div<FullWidthProps>`
|
||||
width: 100%;
|
||||
${({ padding }) =>
|
||||
padding &&
|
||||
css`
|
||||
padding: ${padding};
|
||||
`}
|
||||
${({ textColor }) =>
|
||||
textColor &&
|
||||
css`
|
||||
color: ${textColor};
|
||||
`}
|
||||
background-color: ${({ sectionColor }) =>
|
||||
sectionColor ? sectionColor : backgroundColor};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding: 16px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const FixedWidth = styled.div`
|
||||
max-width: 1000px;
|
||||
margin: 0 auto 0;
|
||||
|
||||
@media (max-width: 1035px) {
|
||||
padding: 0 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
type SectionProps = {
|
||||
fixedWidth?: boolean;
|
||||
color?: string | ThemeSet;
|
||||
textColor?: string | ThemeSet;
|
||||
padding?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const Section: FC<SectionProps> = ({
|
||||
fixedWidth = false,
|
||||
children,
|
||||
color,
|
||||
textColor,
|
||||
padding,
|
||||
}) => {
|
||||
const Fixed = fixedWidth ? FixedWidth : Fragment;
|
||||
|
||||
return (
|
||||
<FullWidth padding={padding} sectionColor={color} textColor={textColor}>
|
||||
<Fixed>{children}</Fixed>
|
||||
</FullWidth>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { mediaWidths, themeColors } from '@/styles/Themes';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { SingleLine } from '../generic/Styled';
|
||||
import { Select, SelectWithValue, ValueProp } from '.';
|
||||
import { FC, ReactNode } from 'react';
|
||||
|
||||
const NoWrapText = styled.div`
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const InputTitle = styled(NoWrapText)``;
|
||||
|
||||
const InputTitleRow = styled.div`
|
||||
display: flex;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-wrap: wrap;
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const InputLine = styled(SingleLine)`
|
||||
width: 100%;
|
||||
margin: 8px 0;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`;
|
||||
|
||||
type InputWithDecoProps = {
|
||||
title: string;
|
||||
options: ValueProp[];
|
||||
noInput?: boolean;
|
||||
loading?: boolean;
|
||||
maxWidth?: string;
|
||||
callback: (value: ValueProp[]) => void;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export const SelectWithDeco: FC<InputWithDecoProps> = ({
|
||||
children,
|
||||
title,
|
||||
noInput,
|
||||
options,
|
||||
loading,
|
||||
maxWidth,
|
||||
callback,
|
||||
}) => {
|
||||
const renderContent = () => {
|
||||
switch (true) {
|
||||
case loading:
|
||||
return (
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={20}
|
||||
style={{ color: themeColors.blue3 }}
|
||||
/>
|
||||
);
|
||||
case !noInput:
|
||||
return (
|
||||
<Select
|
||||
maxWidth={maxWidth || '500px'}
|
||||
options={options}
|
||||
callback={callback}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<InputLine>
|
||||
<InputTitleRow>
|
||||
<InputTitle>{title}</InputTitle>
|
||||
</InputTitleRow>
|
||||
{renderContent()}
|
||||
{children}
|
||||
</InputLine>
|
||||
);
|
||||
};
|
||||
|
||||
type InputWithDecoAndValueProps = {
|
||||
title: string;
|
||||
value: ValueProp | undefined;
|
||||
options: ValueProp[];
|
||||
noInput?: boolean;
|
||||
loading?: boolean;
|
||||
callback: (value: ValueProp[]) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SelectWithDecoAndValue: React.FC<InputWithDecoAndValueProps> = ({
|
||||
children,
|
||||
title,
|
||||
noInput,
|
||||
options,
|
||||
loading,
|
||||
callback,
|
||||
value,
|
||||
}) => {
|
||||
const renderContent = () => {
|
||||
switch (true) {
|
||||
case loading:
|
||||
return (
|
||||
<Loader2
|
||||
className="animate-spin"
|
||||
size={20}
|
||||
style={{ color: themeColors.blue3 }}
|
||||
/>
|
||||
);
|
||||
case !noInput:
|
||||
return (
|
||||
<SelectWithValue
|
||||
maxWidth={'500px'}
|
||||
options={options}
|
||||
callback={callback}
|
||||
value={value}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<InputLine>
|
||||
<InputTitleRow>
|
||||
<InputTitle>{title}</InputTitle>
|
||||
</InputTitleRow>
|
||||
{renderContent()}
|
||||
{children}
|
||||
</InputLine>
|
||||
);
|
||||
};
|
||||
|
|
@ -36,7 +36,6 @@ type SelectWithValueProps = {
|
|||
options: ValueProp[];
|
||||
value: ValueProp | undefined;
|
||||
maxWidth?: string;
|
||||
minWidth?: string;
|
||||
isClearable?: boolean;
|
||||
callback: (value: ValueProp[]) => void;
|
||||
};
|
||||
|
|
@ -44,13 +43,12 @@ type SelectWithValueProps = {
|
|||
export const SelectWithValue = ({
|
||||
options,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
callback,
|
||||
value,
|
||||
isClearable = true,
|
||||
}: SelectWithValueProps) => {
|
||||
return (
|
||||
<div style={{ maxWidth, minWidth, width: maxWidth ? undefined : 'auto' }}>
|
||||
<div style={{ maxWidth, width: maxWidth ? undefined : 'auto' }}>
|
||||
<NativeSelect
|
||||
value={value ? String(value.value) : ''}
|
||||
onChange={e => {
|
||||
|
|
@ -84,9 +82,8 @@ export const SmallSelectWithValue = ({
|
|||
isClearable = true,
|
||||
}: SelectWithValueProps) => {
|
||||
return (
|
||||
<div style={{ maxWidth, width: '100%' }}>
|
||||
<div style={{ maxWidth }}>
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
value={value ? String(value.value) : ''}
|
||||
onChange={e => {
|
||||
const selectedValue = e.target.value;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { shorten } from '@/components/generic/helpers';
|
||||
import { useGetChannelsWithPeersQuery } from '@/graphql/queries/__generated__/getChannels.generated';
|
||||
import { SelectWithDeco } from '../SelectWithDeco';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Select, ValueProp } from '..';
|
||||
import { Channel } from '../../../graphql/types';
|
||||
import { ValueProp } from '..';
|
||||
|
||||
type ChannelSelectProps = {
|
||||
title: string;
|
||||
|
|
@ -30,7 +30,7 @@ export const ChannelSelect = ({
|
|||
channel?.partner_node_info?.node?.alias
|
||||
? ` - ${channel.partner_node_info.node.alias}`
|
||||
: ''
|
||||
} -
|
||||
} -
|
||||
${shorten(channel.partner_public_key)}`;
|
||||
|
||||
return {
|
||||
|
|
@ -55,12 +55,19 @@ export const ChannelSelect = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<SelectWithDeco
|
||||
loading={loading}
|
||||
title={title}
|
||||
options={options}
|
||||
callback={handleChange}
|
||||
maxWidth={maxWidth}
|
||||
/>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin text-[#5163ba]" size={20} />
|
||||
) : (
|
||||
<Select
|
||||
maxWidth={maxWidth || '500px'}
|
||||
options={options}
|
||||
callback={handleChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useGetPeersQuery } from '@/graphql/queries/__generated__/getPeers.generated';
|
||||
import { shorten } from '@/components/generic/helpers';
|
||||
import { Peer } from '@/graphql/types';
|
||||
import { SelectWithDeco } from '../SelectWithDeco';
|
||||
import { ValueProp } from '..';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { Select, ValueProp } from '..';
|
||||
|
||||
type PeerSelectProps = {
|
||||
title: string;
|
||||
|
|
@ -52,11 +52,15 @@ export const PeerSelect = ({ title, callback }: PeerSelectProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<SelectWithDeco
|
||||
loading={loading}
|
||||
title={title}
|
||||
options={options}
|
||||
callback={handleChange}
|
||||
/>
|
||||
<div className="flex items-center w-full my-2 flex-col md:flex-row justify-between">
|
||||
<div className="flex text-sm whitespace-nowrap flex-wrap md:my-0 my-2">
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin text-[#5163ba]" size={20} />
|
||||
) : (
|
||||
<Select maxWidth={'500px'} options={options} callback={handleChange} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { mediaWidths } from '@/styles/Themes';
|
||||
|
||||
const StyledSpacer = styled.div`
|
||||
height: 32px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Spacer = () => <StyledSpacer />;
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { Table } from '@tanstack/react-table';
|
||||
import { FC, useMemo } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { mediaWidths } from '../../styles/Themes';
|
||||
import { groupBy } from 'lodash';
|
||||
import { DarkSubTitle, SubCard } from '../generic/Styled';
|
||||
|
||||
|
|
@ -10,44 +8,10 @@ interface ColumnConfigurationsProps {
|
|||
toggleConfiguration: (hide: boolean, id: string) => void;
|
||||
}
|
||||
|
||||
const S = {
|
||||
row: styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
optionRow: styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
option: styled.label`
|
||||
margin: 4px 8px;
|
||||
`,
|
||||
options: styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: row;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const ColumnConfigurations: FC<ColumnConfigurationsProps> = ({
|
||||
table,
|
||||
toggleConfiguration,
|
||||
}: ColumnConfigurationsProps) => {
|
||||
// The columns that are hideable in configurations need to be grouped by their parents id in order for display purposes, see enableHiding to toggle viewability of each column
|
||||
const groupedHideableColumns = useMemo(() => {
|
||||
const allLeafColumns = table
|
||||
.getAllLeafColumns()
|
||||
|
|
@ -57,7 +21,7 @@ export const ColumnConfigurations: FC<ColumnConfigurationsProps> = ({
|
|||
}, [table]);
|
||||
|
||||
return (
|
||||
<S.optionRow>
|
||||
<div className="flex justify-start items-stretch flex-wrap md:flex md:flex-row">
|
||||
{Object.keys(groupedHideableColumns).map(
|
||||
(group: string, index: number) => {
|
||||
return (
|
||||
|
|
@ -65,31 +29,27 @@ export const ColumnConfigurations: FC<ColumnConfigurationsProps> = ({
|
|||
<DarkSubTitle fontSize="16px">
|
||||
{group === 'undefined' ? 'General' : group}
|
||||
</DarkSubTitle>
|
||||
<S.options>
|
||||
<div className="flex flex-row md:flex-col justify-start items-start flex-wrap">
|
||||
{groupedHideableColumns[group].map((column: any) => {
|
||||
return (
|
||||
<S.option key={column.id} className="px-1">
|
||||
<label>
|
||||
<input
|
||||
{...{
|
||||
type: 'checkbox',
|
||||
checked: column.getIsVisible(),
|
||||
onChange: column.getToggleVisibilityHandler(),
|
||||
}}
|
||||
onClick={(e: any) =>
|
||||
toggleConfiguration(!e.target.checked, column.id)
|
||||
}
|
||||
/>{' '}
|
||||
{column.columnDef.header}
|
||||
</label>
|
||||
</S.option>
|
||||
<label key={column.id} className="m-1 mx-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={column.getIsVisible()}
|
||||
onChange={column.getToggleVisibilityHandler()}
|
||||
onClick={(e: any) =>
|
||||
toggleConfiguration(!e.target.checked, column.id)
|
||||
}
|
||||
/>{' '}
|
||||
{column.columnDef.header}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</S.options>
|
||||
</div>
|
||||
</SubCard>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</S.optionRow>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { Input } from '../input';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
// A debounced input react component
|
||||
export function DebouncedInput({
|
||||
|
|
@ -31,7 +31,7 @@ export function DebouncedInput({
|
|||
|
||||
return (
|
||||
<Input
|
||||
maxWidth={'300px'}
|
||||
style={{ maxWidth: '300px' }}
|
||||
value={value || ''}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder={`Search ${count} ${placeholder || ''}`}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
|
|
@ -11,10 +10,10 @@ import {
|
|||
VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { Settings, X } from 'lucide-react';
|
||||
import { separationColor } from '../../styles/Themes';
|
||||
import { ColorButton } from '../buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ColumnConfigurations } from './ColumnConfigurations';
|
||||
import { DebouncedInput } from './DebouncedInput';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface TableProps {
|
||||
columns: ColumnDef<any, any>[];
|
||||
|
|
@ -30,60 +29,6 @@ interface TableProps {
|
|||
toggleConfiguration?: (hide: boolean, id: string) => void;
|
||||
}
|
||||
|
||||
type StyledTableProps = {
|
||||
withBorder?: boolean;
|
||||
alignCenter?: boolean;
|
||||
fontSize?: string;
|
||||
};
|
||||
|
||||
const S = {
|
||||
row: styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
`,
|
||||
wrapper: styled.div<StyledTableProps>`
|
||||
overflow-x: auto;
|
||||
table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
tr {
|
||||
:last-child {
|
||||
td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
font-size: ${({ fontSize }) => fontSize || '14px'};
|
||||
text-align: left;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
${({ withBorder }: StyledTableProps) =>
|
||||
withBorder &&
|
||||
css`
|
||||
border-bottom: 1px solid ${separationColor};
|
||||
`}
|
||||
${({ alignCenter }: StyledTableProps) =>
|
||||
alignCenter &&
|
||||
css`
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
`}
|
||||
:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export default function Table({
|
||||
columns,
|
||||
data,
|
||||
|
|
@ -137,7 +82,7 @@ export default function Table({
|
|||
|
||||
return (
|
||||
<>
|
||||
<S.row>
|
||||
<div className="flex flex-row justify-between mb-6">
|
||||
{withGlobalSort ? (
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ''}
|
||||
|
|
@ -148,12 +93,12 @@ export default function Table({
|
|||
) : null}
|
||||
{toggleConfiguration ? (
|
||||
<>
|
||||
<ColorButton onClick={() => setIsOpen(p => !p)}>
|
||||
<Button variant="outline" onClick={() => setIsOpen(p => !p)}>
|
||||
{isOpen ? <X size={18} /> : <Settings size={18} />}
|
||||
</ColorButton>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</S.row>
|
||||
</div>
|
||||
|
||||
{isOpen && toggleConfiguration ? (
|
||||
<ColumnConfigurations
|
||||
|
|
@ -162,10 +107,23 @@ export default function Table({
|
|||
/>
|
||||
) : null}
|
||||
|
||||
<S.wrapper
|
||||
withBorder={withBorder}
|
||||
fontSize={fontSize}
|
||||
alignCenter={alignCenter}
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-x-auto',
|
||||
'[&_table]:w-full [&_table]:border-spacing-0',
|
||||
'[&_table_tr:last-child_td]:border-b-0',
|
||||
'[&_table_.cursor]:cursor-pointer',
|
||||
'[&_table_th]:text-left [&_table_th]:m-0 [&_table_th]:p-2',
|
||||
'[&_table_td]:text-left [&_table_td]:m-0 [&_table_td]:p-2',
|
||||
'[&_table_th:last-child]:border-r-0',
|
||||
'[&_table_td:last-child]:border-r-0',
|
||||
withBorder &&
|
||||
'[&_table_th]:border-b [&_table_th]:border-[#f0f2f8] [&_table_th]:dark:border-[#212735] [&_table_td]:border-b [&_table_td]:border-[#f0f2f8] [&_table_td]:dark:border-[#212735]',
|
||||
alignCenter && '[&_table_th]:text-center [&_table_td]:text-center'
|
||||
)}
|
||||
style={{
|
||||
fontSize: fontSize || '14px',
|
||||
}}
|
||||
>
|
||||
<table>
|
||||
<thead>
|
||||
|
|
@ -224,7 +182,7 @@ export default function Table({
|
|||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</S.wrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +1,155 @@
|
|||
import styled from 'styled-components';
|
||||
// import { Card, CardProps } from 'components/generic/Styled';
|
||||
import { fontColors, mediaWidths, textColor } from '../../styles/Themes';
|
||||
import { Card, CardProps } from '../generic/CardGeneric';
|
||||
import { forwardRef, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export const Center = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
`;
|
||||
export const Center = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex justify-center items-center text-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const Title = styled.h1<{ textColor?: string }>`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: ${({ textColor }) => (textColor ? textColor : fontColors.grey3)};
|
||||
font-size: 40px;
|
||||
interface TitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
||||
textColor?: string;
|
||||
}
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
font-size: 24px;
|
||||
}
|
||||
`;
|
||||
export const Title = forwardRef<HTMLHeadingElement, TitleProps>(
|
||||
({ textColor, className, style, ...props }, ref) => (
|
||||
<h1
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'w-full text-center text-2xl md:text-[40px]',
|
||||
!textColor && 'text-[#e1e6ed]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...(textColor ? { color: textColor } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const SectionTitle = styled.h2<{ textColor?: string }>`
|
||||
color: ${({ textColor }) => (textColor ? textColor : fontColors.blue)};
|
||||
font-size: 24px;
|
||||
`;
|
||||
export const SectionTitle = forwardRef<HTMLHeadingElement, TitleProps>(
|
||||
({ textColor, className, style, ...props }, ref) => (
|
||||
<h2
|
||||
ref={ref}
|
||||
className={cn('text-2xl', !textColor && 'text-[#ccd0e7]', className)}
|
||||
style={{
|
||||
...(textColor ? { color: textColor } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const Subtitle = styled.h2<{ textColor?: string }>`
|
||||
color: ${({ textColor }) => (textColor ? textColor : fontColors.blue)};
|
||||
font-size: 16px;
|
||||
max-width: 600px;
|
||||
`;
|
||||
export const Subtitle = forwardRef<HTMLHeadingElement, TitleProps>(
|
||||
({ textColor, className, style, ...props }, ref) => (
|
||||
<h2
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-base max-w-[600px]',
|
||||
!textColor && 'text-[#ccd0e7]',
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
...(textColor ? { color: textColor } : {}),
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const Question = styled.h3`
|
||||
color: ${fontColors.grey8};
|
||||
`;
|
||||
export const Question = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3 ref={ref} className={cn('text-[#4a5669]', className)} {...props} />
|
||||
));
|
||||
|
||||
export const Text = styled.p`
|
||||
color: ${fontColors.grey6};
|
||||
text-align: justify;
|
||||
`;
|
||||
export const Text = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-[#667587] text-justify', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const SmallText = styled(Text)`
|
||||
color: ${textColor};
|
||||
text-align: start;
|
||||
`;
|
||||
export const SmallText = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-[#212735] dark:text-white text-start', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const BulletPoint = styled(Text)`
|
||||
margin-left: 32px;
|
||||
`;
|
||||
export const BulletPoint = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-[#667587] text-justify ml-8', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const DetailCard = styled(Card)<CardProps>`
|
||||
margin-bottom: 0;
|
||||
margin: 8px 16px;
|
||||
z-index: 1;
|
||||
flex: 1 0 30%;
|
||||
export const DetailCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-white dark:bg-[#1a1f35] shadow-[0_8px_16px_-8px_rgba(0,0,0,0.1)] rounded border border-[#f0f2f8] dark:border-[#20263d] w-full',
|
||||
'm-[8px_16px] z-[1] flex-[1_0_100%] md:flex-[1_0_30%]',
|
||||
className
|
||||
)}
|
||||
style={{ padding: '16px', marginBottom: 0 }}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
`;
|
||||
export const DetailLine = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mx-0 flex justify-center items-center flex-wrap md:mx-[-16px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const DetailLine = styled.div`
|
||||
margin: 0 -16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
export const IconTitle = forwardRef<
|
||||
HTMLDivElement,
|
||||
HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex text-[#212735] dark:text-white', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
export const IconTitle = styled.div`
|
||||
display: flex;
|
||||
color: ${textColor};
|
||||
`;
|
||||
|
||||
export const IconMargin = styled.span`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
export const IconMargin = forwardRef<
|
||||
HTMLSpanElement,
|
||||
HTMLAttributes<HTMLSpanElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span ref={ref} className={cn('mr-1', className)} {...props} />
|
||||
));
|
||||
|
|
|
|||
66
src/client/src/components/ui/accordion.tsx
Normal file
66
src/client/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { Accordion as AccordionPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn('border-b last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
'flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn('pt-0 pb-4', className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
66
src/client/src/components/ui/alert.tsx
Normal file
66
src/client/src/components/ui/alert.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-card text-card-foreground',
|
||||
destructive:
|
||||
'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
'col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
'col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
48
src/client/src/components/ui/badge.tsx
Normal file
48
src/client/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Slot } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90',
|
||||
outline:
|
||||
'border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
ghost: '[a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 [a&]:hover:underline',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<'span'> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot.Root : 'span';
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
data-variant={variant}
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
import { ComponentProps } from 'react';
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { Slot } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
|
||||
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40',
|
||||
outline:
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
|
||||
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost:
|
||||
|
|
@ -23,7 +23,7 @@ const buttonVariants = cva(
|
|||
size: {
|
||||
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
|
||||
sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5',
|
||||
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
|
||||
icon: 'size-9',
|
||||
'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
||||
|
|
@ -44,7 +44,7 @@ function Button({
|
|||
size = 'default',
|
||||
asChild = false,
|
||||
...props
|
||||
}: ComponentProps<'button'> &
|
||||
}: React.ComponentProps<'button'> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
|
|
|
|||
92
src/client/src/components/ui/card.tsx
Normal file
92
src/client/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn('px-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
30
src/client/src/components/ui/checkbox.tsx
Normal file
30
src/client/src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import * as React from 'react';
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { Checkbox as CheckboxPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
'peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
255
src/client/src/components/ui/dropdown-menu.tsx
Normal file
255
src/client/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
import * as React from 'react';
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: 'default' | 'destructive';
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn('-mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
'ml-auto text-xs tracking-widest text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
'z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
170
src/client/src/components/ui/input-group.tsx
Normal file
170
src/client/src/components/ui/input-group.tsx
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
'group/input-group relative flex w-full items-center rounded-md border border-input shadow-xs transition-[color,box-shadow] outline-none dark:bg-input/30',
|
||||
'h-9 min-w-0 has-[>textarea]:h-auto',
|
||||
|
||||
// Variants based on alignment.
|
||||
'has-[>[data-align=inline-start]]:[&>input]:pl-2',
|
||||
'has-[>[data-align=inline-end]]:[&>input]:pr-2',
|
||||
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
|
||||
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
|
||||
|
||||
// Focus state.
|
||||
'has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-[3px] has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50',
|
||||
|
||||
// Error state.
|
||||
'has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-destructive/20 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40',
|
||||
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
'inline-start':
|
||||
'order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]',
|
||||
'inline-end':
|
||||
'order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]',
|
||||
'block-start':
|
||||
'order-first w-full justify-start px-3 pt-3 group-has-[>input]/input-group:pt-2.5 [.border-b]:pb-3',
|
||||
'block-end':
|
||||
'order-last w-full justify-start px-3 pb-3 group-has-[>input]/input-group:pb-2.5 [.border-t]:pt-3',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: 'inline-start',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = 'inline-start',
|
||||
...props
|
||||
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={e => {
|
||||
if ((e.target as HTMLElement).closest('button')) {
|
||||
return;
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
'flex items-center gap-2 text-sm shadow-none',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-2 has-[>svg]:px-2 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: 'h-8 gap-1.5 rounded-md px-2.5 has-[>svg]:px-2.5',
|
||||
'icon-xs':
|
||||
'size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0',
|
||||
'icon-sm': 'size-8 p-0 has-[>svg]:p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'xs',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = 'button',
|
||||
variant = 'ghost',
|
||||
size = 'xs',
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, 'size'> &
|
||||
VariantProps<typeof inputGroupButtonVariants>) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
'flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
'flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
};
|
||||
21
src/client/src/components/ui/input.tsx
Normal file
21
src/client/src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30',
|
||||
'focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
||||
'aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
89
src/client/src/components/ui/popover.tsx
Normal file
89
src/client/src/components/ui/popover.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Popover as PopoverPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
className={cn('flex flex-col gap-1 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverTitle({ className, ...props }: React.ComponentProps<'h2'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="popover-title"
|
||||
className={cn('font-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'p'>) {
|
||||
return (
|
||||
<p
|
||||
data-slot="popover-description"
|
||||
className={cn('text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
PopoverAnchor,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverDescription,
|
||||
};
|
||||
31
src/client/src/components/ui/progress.tsx
Normal file
31
src/client/src/components/ui/progress.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { Progress as ProgressPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
'relative h-2 w-full overflow-hidden rounded-full bg-primary/20',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
190
src/client/src/components/ui/select.tsx
Normal file
190
src/client/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
|
||||
import { Select as SelectPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />;
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />;
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: 'sm' | 'default';
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = 'item-aligned',
|
||||
align = 'center',
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
'relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1'
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn('px-2 py-1.5 text-xs text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
data-slot="select-item-indicator"
|
||||
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn('pointer-events-none -mx-1 my-1 h-px bg-border', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
'flex cursor-default items-center justify-center py-1',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
26
src/client/src/components/ui/separator.tsx
Normal file
26
src/client/src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as React from 'react';
|
||||
import { Separator as SeparatorPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
143
src/client/src/components/ui/sheet.tsx
Normal file
143
src/client/src/components/ui/sheet.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { Dialog as SheetPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = 'right',
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: 'top' | 'right' | 'bottom' | 'left';
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
'fixed z-50 flex flex-col gap-4 bg-background shadow-lg transition ease-in-out data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:animate-in data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
side === 'top' &&
|
||||
'inset-x-0 top-0 h-auto border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
side === 'bottom' &&
|
||||
'inset-x-0 bottom-0 h-auto border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn('flex flex-col gap-1.5 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn('mt-auto flex flex-col gap-2 p-4', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn('font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
};
|
||||
114
src/client/src/components/ui/table.tsx
Normal file
114
src/client/src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn('[&_tr]:border-b', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
'border-t bg-muted/50 font-medium [&>tr]:last:border-b-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
'border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
'h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn('mt-4 text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
89
src/client/src/components/ui/tabs.tsx
Normal file
89
src/client/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import { Tabs as TabsPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
orientation = 'horizontal',
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
data-orientation={orientation}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
'group/tabs flex gap-2 data-[orientation=horizontal]:flex-col',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tabsListVariants = cva(
|
||||
'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-muted',
|
||||
line: 'gap-1 bg-transparent',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
||||
VariantProps<typeof tabsListVariants>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
className={cn(tabsListVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
|
||||
'data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground',
|
||||
'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn('flex-1 outline-none', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
|
||||
18
src/client/src/components/ui/textarea.tsx
Normal file
18
src/client/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
55
src/client/src/components/ui/tooltip.tsx
Normal file
55
src/client/src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from 'react';
|
||||
import { Tooltip as TooltipPrimitive } from 'radix-ui';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
|
@ -1,21 +1,7 @@
|
|||
import { useGetLatestVersionQuery } from '@/graphql/queries/__generated__/getLatestVersion.generated';
|
||||
import { config } from '../../config/thunderhubConfig';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from '../link/Link';
|
||||
|
||||
const VersionBox = styled.div`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
opacity: 0.3;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
color: white;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Version = () => {
|
||||
const { npmVersion, noVersionCheck } = config;
|
||||
|
||||
|
|
@ -49,7 +35,9 @@ export const Version = () => {
|
|||
href={'https://docs.thunderhub.io/installation#updating'}
|
||||
newTab={true}
|
||||
>
|
||||
<VersionBox>{`Version ${githubVersion} is available. You are on version ${npmVersion}`}</VersionBox>
|
||||
<div className="w-full text-center text-sm cursor-pointer text-muted-foreground">
|
||||
{`Version ${githubVersion} is available. You are on version ${npmVersion}`}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { mediaWidths } from '../../styles/Themes';
|
||||
|
||||
const HideMobile = styled.div`
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const HideDesktop = styled.div`
|
||||
display: none;
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: unset;
|
||||
}
|
||||
`;
|
||||
|
||||
interface ViewSwitchProps {
|
||||
hideMobile?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const ViewSwitch: FC<ViewSwitchProps> = ({ hideMobile, children }) => {
|
||||
return hideMobile ? (
|
||||
<HideMobile>{children}</HideMobile>
|
||||
) : (
|
||||
<HideDesktop>{children}</HideDesktop>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
import { FC, ReactNode, createContext, useContext, useReducer } from 'react';
|
||||
import { Message } from '@/graphql/types';
|
||||
|
||||
export interface SentChatProps extends Message {
|
||||
isSent?: boolean;
|
||||
feePaid?: number;
|
||||
}
|
||||
|
||||
type State = {
|
||||
initialized: boolean;
|
||||
chats: Message[];
|
||||
sentChats: SentChatProps[];
|
||||
lastChat: string;
|
||||
sender: string;
|
||||
};
|
||||
|
||||
type ActionType =
|
||||
| {
|
||||
type: 'initialized';
|
||||
chats?: Message[];
|
||||
lastChat?: string;
|
||||
sender?: string;
|
||||
sentChats?: SentChatProps[];
|
||||
}
|
||||
| {
|
||||
type: 'additional';
|
||||
chats: Message[];
|
||||
lastChat: string;
|
||||
}
|
||||
| {
|
||||
type: 'changeActive';
|
||||
sender: string;
|
||||
userId: string;
|
||||
}
|
||||
| {
|
||||
type: 'newChat';
|
||||
sender: string;
|
||||
userId: string;
|
||||
newChat: SentChatProps;
|
||||
}
|
||||
| {
|
||||
type: 'disconnected';
|
||||
};
|
||||
|
||||
type Dispatch = (action: ActionType) => void;
|
||||
|
||||
const StateContext = createContext<State | undefined>(undefined);
|
||||
const DispatchContext = createContext<Dispatch | undefined>(undefined);
|
||||
|
||||
const initialState: State = {
|
||||
initialized: false,
|
||||
chats: [],
|
||||
lastChat: '',
|
||||
sender: '',
|
||||
sentChats: [],
|
||||
};
|
||||
|
||||
const stateReducer = (state: State, action: ActionType): State => {
|
||||
switch (action.type) {
|
||||
case 'initialized':
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
...action,
|
||||
};
|
||||
case 'additional':
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
chats: [...state.chats, ...action.chats],
|
||||
lastChat: action.lastChat,
|
||||
};
|
||||
case 'changeActive':
|
||||
return {
|
||||
...state,
|
||||
sender: action.sender,
|
||||
};
|
||||
case 'newChat':
|
||||
localStorage.setItem(
|
||||
`${action.userId}-sentChats`,
|
||||
JSON.stringify([...state.sentChats, action.newChat])
|
||||
);
|
||||
return {
|
||||
...state,
|
||||
sentChats: [...state.sentChats, action.newChat],
|
||||
...(action.sender && { sender: action.sender }),
|
||||
};
|
||||
case 'disconnected':
|
||||
return initialState;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const ChatProvider: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
const [state, dispatch] = useReducer(stateReducer, initialState);
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<StateContext.Provider value={state}>{children}</StateContext.Provider>
|
||||
</DispatchContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const useChatState = () => {
|
||||
const context = useContext(StateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useChatState must be used within a ChatProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const useChatDispatch = () => {
|
||||
const context = useContext(DispatchContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useChatDispatch must be used within a ChatProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { ChatProvider, useChatState, useChatDispatch };
|
||||
|
|
@ -1,15 +1,12 @@
|
|||
import { FC, ReactNode } from 'react';
|
||||
import { PriceProvider } from './PriceContext';
|
||||
import { ChatProvider } from './ChatContext';
|
||||
import { DashProvider } from './DashContext';
|
||||
import { NotificationProvider } from './NotificationContext';
|
||||
|
||||
export const ContextProvider: FC<{ children?: ReactNode }> = ({ children }) => (
|
||||
<NotificationProvider>
|
||||
<DashProvider>
|
||||
<PriceProvider>
|
||||
<ChatProvider>{children}</ChatProvider>
|
||||
</PriceProvider>
|
||||
<PriceProvider>{children}</PriceProvider>
|
||||
</DashProvider>
|
||||
</NotificationProvider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
import * as Types from '../../types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type SendMessageMutationVariables = Types.Exact<{
|
||||
publicKey: Types.Scalars['String']['input'];
|
||||
message: Types.Scalars['String']['input'];
|
||||
messageType?: Types.InputMaybe<Types.Scalars['String']['input']>;
|
||||
tokens?: Types.InputMaybe<Types.Scalars['Float']['input']>;
|
||||
maxFee?: Types.InputMaybe<Types.Scalars['Float']['input']>;
|
||||
}>;
|
||||
|
||||
export type SendMessageMutation = {
|
||||
__typename?: 'Mutation';
|
||||
sendMessage: number;
|
||||
};
|
||||
|
||||
export const SendMessageDocument = gql`
|
||||
mutation SendMessage(
|
||||
$publicKey: String!
|
||||
$message: String!
|
||||
$messageType: String
|
||||
$tokens: Float
|
||||
$maxFee: Float
|
||||
) {
|
||||
sendMessage(
|
||||
publicKey: $publicKey
|
||||
message: $message
|
||||
messageType: $messageType
|
||||
tokens: $tokens
|
||||
maxFee: $maxFee
|
||||
)
|
||||
}
|
||||
`;
|
||||
export type SendMessageMutationFn = Apollo.MutationFunction<
|
||||
SendMessageMutation,
|
||||
SendMessageMutationVariables
|
||||
>;
|
||||
|
||||
/**
|
||||
* __useSendMessageMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useSendMessageMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useSendMessageMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [sendMessageMutation, { data, loading, error }] = useSendMessageMutation({
|
||||
* variables: {
|
||||
* publicKey: // value for 'publicKey'
|
||||
* message: // value for 'message'
|
||||
* messageType: // value for 'messageType'
|
||||
* tokens: // value for 'tokens'
|
||||
* maxFee: // value for 'maxFee'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSendMessageMutation(
|
||||
baseOptions?: Apollo.MutationHookOptions<
|
||||
SendMessageMutation,
|
||||
SendMessageMutationVariables
|
||||
>
|
||||
) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<SendMessageMutation, SendMessageMutationVariables>(
|
||||
SendMessageDocument,
|
||||
options
|
||||
);
|
||||
}
|
||||
export type SendMessageMutationHookResult = ReturnType<
|
||||
typeof useSendMessageMutation
|
||||
>;
|
||||
export type SendMessageMutationResult =
|
||||
Apollo.MutationResult<SendMessageMutation>;
|
||||
export type SendMessageMutationOptions = Apollo.BaseMutationOptions<
|
||||
SendMessageMutation,
|
||||
SendMessageMutationVariables
|
||||
>;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SEND_MESSAGE = gql`
|
||||
mutation SendMessage(
|
||||
$publicKey: String!
|
||||
$message: String!
|
||||
$messageType: String
|
||||
$tokens: Float
|
||||
$maxFee: Float
|
||||
) {
|
||||
sendMessage(
|
||||
publicKey: $publicKey
|
||||
message: $message
|
||||
messageType: $messageType
|
||||
tokens: $tokens
|
||||
maxFee: $maxFee
|
||||
)
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
import * as Types from '../../types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type GetMessagesQueryVariables = Types.Exact<{
|
||||
initialize?: Types.InputMaybe<Types.Scalars['Boolean']['input']>;
|
||||
}>;
|
||||
|
||||
export type GetMessagesQuery = {
|
||||
__typename?: 'Query';
|
||||
getMessages: {
|
||||
__typename?: 'GetMessages';
|
||||
token?: string | null;
|
||||
messages: Array<{
|
||||
__typename?: 'Message';
|
||||
date: string;
|
||||
contentType?: string | null;
|
||||
alias?: string | null;
|
||||
message?: string | null;
|
||||
id: string;
|
||||
sender?: string | null;
|
||||
verified: boolean;
|
||||
tokens?: number | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
export const GetMessagesDocument = gql`
|
||||
query GetMessages($initialize: Boolean) {
|
||||
getMessages(initialize: $initialize) {
|
||||
token
|
||||
messages {
|
||||
date
|
||||
contentType
|
||||
alias
|
||||
message
|
||||
id
|
||||
sender
|
||||
verified
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetMessagesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetMessagesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetMessagesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useGetMessagesQuery({
|
||||
* variables: {
|
||||
* initialize: // value for 'initialize'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetMessagesQuery(
|
||||
baseOptions?: Apollo.QueryHookOptions<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>
|
||||
) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useQuery<GetMessagesQuery, GetMessagesQueryVariables>(
|
||||
GetMessagesDocument,
|
||||
options
|
||||
);
|
||||
}
|
||||
export function useGetMessagesLazyQuery(
|
||||
baseOptions?: Apollo.LazyQueryHookOptions<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>
|
||||
) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useLazyQuery<GetMessagesQuery, GetMessagesQueryVariables>(
|
||||
GetMessagesDocument,
|
||||
options
|
||||
);
|
||||
}
|
||||
// @ts-ignore
|
||||
export function useGetMessagesSuspenseQuery(
|
||||
baseOptions?: Apollo.SuspenseQueryHookOptions<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>
|
||||
): Apollo.UseSuspenseQueryResult<GetMessagesQuery, GetMessagesQueryVariables>;
|
||||
export function useGetMessagesSuspenseQuery(
|
||||
baseOptions?:
|
||||
| Apollo.SkipToken
|
||||
| Apollo.SuspenseQueryHookOptions<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>
|
||||
): Apollo.UseSuspenseQueryResult<
|
||||
GetMessagesQuery | undefined,
|
||||
GetMessagesQueryVariables
|
||||
>;
|
||||
export function useGetMessagesSuspenseQuery(
|
||||
baseOptions?:
|
||||
| Apollo.SkipToken
|
||||
| Apollo.SuspenseQueryHookOptions<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>
|
||||
) {
|
||||
const options =
|
||||
baseOptions === Apollo.skipToken
|
||||
? baseOptions
|
||||
: { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useSuspenseQuery<GetMessagesQuery, GetMessagesQueryVariables>(
|
||||
GetMessagesDocument,
|
||||
options
|
||||
);
|
||||
}
|
||||
export type GetMessagesQueryHookResult = ReturnType<typeof useGetMessagesQuery>;
|
||||
export type GetMessagesLazyQueryHookResult = ReturnType<
|
||||
typeof useGetMessagesLazyQuery
|
||||
>;
|
||||
export type GetMessagesSuspenseQueryHookResult = ReturnType<
|
||||
typeof useGetMessagesSuspenseQuery
|
||||
>;
|
||||
export type GetMessagesQueryResult = Apollo.QueryResult<
|
||||
GetMessagesQuery,
|
||||
GetMessagesQueryVariables
|
||||
>;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_MESSAGES = gql`
|
||||
query GetMessages($initialize: Boolean) {
|
||||
getMessages(initialize: $initialize) {
|
||||
token
|
||||
messages {
|
||||
date
|
||||
contentType
|
||||
alias
|
||||
message
|
||||
id
|
||||
sender
|
||||
verified
|
||||
tokens
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
6
src/client/src/hooks/useThemeMode.ts
Normal file
6
src/client/src/hooks/useThemeMode.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { useConfigState } from '../context/ConfigContext';
|
||||
|
||||
export const useThemeMode = () => {
|
||||
const { theme } = useConfigState();
|
||||
return theme as 'dark' | 'light';
|
||||
};
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const PageWrapper = styled.div`
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
`;
|
||||
|
||||
export const HeaderBodyWrapper = styled.div`
|
||||
padding-bottom: 120px;
|
||||
`;
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import styled from 'styled-components';
|
||||
import { headerTextColor, fontColors, mediaWidths } from '../../styles/Themes';
|
||||
|
||||
export const FooterWrapper = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
`;
|
||||
|
||||
export const FooterStyle = styled.div`
|
||||
padding: 16px 0;
|
||||
min-height: 120px;
|
||||
color: ${headerTextColor};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding-bottom: 32px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SideFooter = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const RightFooter = styled(SideFooter)`
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin: 16px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Title = styled.div`
|
||||
font-weight: 800;
|
||||
color: ${headerTextColor};
|
||||
`;
|
||||
|
||||
export const SideText = styled.div`
|
||||
font-size: 14px;
|
||||
color: ${fontColors.grey7};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Line = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Version = styled.div`
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
export const FooterRow = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
export const FooterCenterText = styled(SideText)`
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
|
@ -1,62 +1,56 @@
|
|||
import { config } from '../../config/thunderhubConfig';
|
||||
import { Section } from '../../components/section/Section';
|
||||
import { Link } from '../../components/link/Link';
|
||||
import { Emoji } from '../../components/emoji/Emoji';
|
||||
import { headerColor, fontColors } from '../../styles/Themes';
|
||||
import {
|
||||
FooterWrapper,
|
||||
FooterStyle,
|
||||
SideFooter,
|
||||
Line,
|
||||
Title,
|
||||
Version,
|
||||
SideText,
|
||||
RightFooter,
|
||||
FooterRow,
|
||||
FooterCenterText,
|
||||
} from './Footer.styled';
|
||||
import { fontColors } from '../../styles/Themes';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const Footer = () => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<FooterWrapper>
|
||||
<Section
|
||||
padding="0 16px"
|
||||
fixedWidth={pathname === '/login'}
|
||||
color={headerColor}
|
||||
>
|
||||
<FooterStyle>
|
||||
<FooterRow>
|
||||
<SideFooter>
|
||||
<Line>
|
||||
<Title>ThunderHub</Title>
|
||||
<Version>{config.npmVersion}</Version>
|
||||
</Line>
|
||||
<SideText>Open-source Lightning Node Manager.</SideText>
|
||||
</SideFooter>
|
||||
<RightFooter>
|
||||
<Link
|
||||
href={'https://github.com/apotdevin/thunderhub'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
<Link
|
||||
href={'https://twitter.com/thunderhubio'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
</RightFooter>
|
||||
</FooterRow>
|
||||
<FooterCenterText>
|
||||
Made in Munich with <Emoji symbol={'🧡 '} label={'heart'} /> and{' '}
|
||||
<Emoji symbol={'⚡'} label={'lightning'} />.
|
||||
</FooterCenterText>
|
||||
</FooterStyle>
|
||||
</Section>
|
||||
</FooterWrapper>
|
||||
<div className="absolute bottom-0 w-full h-[120px]">
|
||||
<div className="w-full bg-[#151727] px-4">
|
||||
<div
|
||||
className={
|
||||
pathname === '/login'
|
||||
? 'max-w-[1000px] mx-auto px-4 lg:px-0'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="py-4 pb-8 md:pb-4 min-h-[120px] text-white flex flex-col justify-center items-center">
|
||||
<div className="w-full flex flex-col justify-center items-center md:flex-row md:justify-between">
|
||||
<div className="flex flex-col justify-center items-center md:justify-start md:items-start">
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="font-extrabold text-white">ThunderHub</div>
|
||||
<div className="text-xs ml-2">{config.npmVersion}</div>
|
||||
</div>
|
||||
<div className="text-sm text-[#b0b3c7]">
|
||||
Open-source Lightning Node Manager.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col my-4 justify-center items-center md:my-0 md:justify-start md:items-end">
|
||||
<Link
|
||||
href={'https://github.com/apotdevin/thunderhub'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Github
|
||||
</Link>
|
||||
<Link
|
||||
href={'https://twitter.com/thunderhubio'}
|
||||
color={fontColors.blue}
|
||||
>
|
||||
Twitter
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-[#b0b3c7] w-full text-center mt-4">
|
||||
Made in Munich with <Emoji symbol={'🧡 '} label={'heart'} /> and{' '}
|
||||
<Emoji symbol={'⚡'} label={'lightning'} />.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,94 +0,0 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
headerTextColor,
|
||||
themeColors,
|
||||
mediaWidths,
|
||||
unSelectedNavButton,
|
||||
homeCompatibleColor,
|
||||
} from '../../styles/Themes';
|
||||
import { SingleLine } from '../../components/generic/Styled';
|
||||
|
||||
export const HeaderStyle = styled.div`
|
||||
padding: 16px 0;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding: 16px 16px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const IconPadding = styled.div`
|
||||
padding-right: 6px;
|
||||
margin-bottom: -4px;
|
||||
`;
|
||||
|
||||
export const HeaderTitle = styled.div<{ withPadding: boolean }>`
|
||||
color: ${headerTextColor};
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
${({ withPadding }) =>
|
||||
withPadding &&
|
||||
css`
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
export const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
`;
|
||||
|
||||
export const LinkWrapper = styled.div<{ last?: boolean }>`
|
||||
color: ${headerTextColor};
|
||||
margin: ${({ last }) => (last ? '0 16px 0 4px' : '0 4px')};
|
||||
|
||||
:hover {
|
||||
color: ${themeColors.blue2};
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderLine = styled(SingleLine)<{ loggedIn: boolean }>`
|
||||
@media (${mediaWidths.mobile}) {
|
||||
${({ loggedIn }) =>
|
||||
!loggedIn &&
|
||||
css`
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export const HeaderButtons = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface NavProps {
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const HeaderNavButton = styled.div<NavProps>`
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background: ${({ selected }) => selected && homeCompatibleColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin: 0 4px;
|
||||
color: ${({ selected }) =>
|
||||
selected ? headerTextColor : unSelectedNavButton};
|
||||
|
||||
&:hover {
|
||||
color: ${headerTextColor};
|
||||
background: ${homeCompatibleColor};
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,40 +1,20 @@
|
|||
import { FC, useEffect, useState } from 'react';
|
||||
import {
|
||||
Cpu,
|
||||
Menu,
|
||||
X,
|
||||
MessageCircle,
|
||||
Settings,
|
||||
Heart,
|
||||
LucideProps,
|
||||
} from 'lucide-react';
|
||||
import { Cpu, Menu, X, Settings, Heart, LucideProps } from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { LogoutButton } from '../../components/logoutButton';
|
||||
import { headerColor, headerTextColor } from '../../styles/Themes';
|
||||
import {
|
||||
useDonate,
|
||||
DonateModal,
|
||||
} from '../../views/home/quickActions/donate/DonateContent';
|
||||
import { SingleLine } from '../../components/generic/Styled';
|
||||
import { BurgerMenu } from '../../components/burgerMenu/BurgerMenu';
|
||||
import { Section } from '../../components/section/Section';
|
||||
import { Link } from '../../components/link/Link';
|
||||
import { ViewSwitch } from '../../components/viewSwitch/ViewSwitch';
|
||||
import {
|
||||
IconWrapper,
|
||||
HeaderStyle,
|
||||
HeaderLine,
|
||||
HeaderTitle,
|
||||
IconPadding,
|
||||
HeaderButtons,
|
||||
HeaderNavButton,
|
||||
} from './Header.styled';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export type Icon = FC<LucideProps>;
|
||||
|
||||
const SSO = '/sso';
|
||||
const MAIN = '/login';
|
||||
const CHAT = '/chat';
|
||||
const SETTINGS = '/settings';
|
||||
|
||||
export const Header = () => {
|
||||
|
|
@ -57,58 +37,72 @@ export const Header = () => {
|
|||
|
||||
const renderNavButton = (link: string, NavIcon: Icon) => (
|
||||
<Link to={link} noStyling={true}>
|
||||
<HeaderNavButton selected={pathname === link}>
|
||||
<Button variant={'ghost'} size={'icon'}>
|
||||
<NavIcon size={18} />
|
||||
</HeaderNavButton>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const renderLoggedIn = () => (
|
||||
<>
|
||||
<ViewSwitch>
|
||||
<IconWrapper onClick={() => setOpen(prev => !prev)}>
|
||||
{open ? <X size={24} /> : <Menu size={24} />}
|
||||
</IconWrapper>
|
||||
</ViewSwitch>
|
||||
<ViewSwitch hideMobile={true}>
|
||||
<HeaderButtons>
|
||||
<HeaderNavButton onClick={openDonate} style={{ cursor: 'pointer' }}>
|
||||
<Heart size={18} />
|
||||
</HeaderNavButton>
|
||||
{renderNavButton(CHAT, MessageCircle)}
|
||||
{renderNavButton(SETTINGS, Settings)}
|
||||
<LogoutButton />
|
||||
</HeaderButtons>
|
||||
</ViewSwitch>
|
||||
<div
|
||||
className="flex md:hidden justify-center items-center w-6 h-6"
|
||||
onClick={() => setOpen(prev => !prev)}
|
||||
>
|
||||
{open ? <X size={24} /> : <Menu size={24} />}
|
||||
</div>
|
||||
<div className="hidden md:flex items-center">
|
||||
<Button onClick={openDonate} variant={'ghost'} size={'icon'}>
|
||||
<Heart size={18} />
|
||||
</Button>
|
||||
{renderNavButton(SETTINGS, Settings)}
|
||||
<LogoutButton />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section
|
||||
padding="0 16px"
|
||||
fixedWidth={pathname === MAIN}
|
||||
color={pathname === MAIN ? 'transparent' : headerColor}
|
||||
textColor={headerTextColor}
|
||||
<div
|
||||
className={cn(
|
||||
'w-full py-4 px-4 text-white',
|
||||
pathname === MAIN ? 'bg-transparent' : 'bg-[#151727]'
|
||||
)}
|
||||
>
|
||||
<HeaderStyle>
|
||||
<HeaderLine loggedIn={!isRoot}>
|
||||
<Link to={!isRoot ? '/' : '/login'} underline={'transparent'}>
|
||||
<HeaderTitle withPadding={isRoot}>
|
||||
<IconPadding>
|
||||
<div
|
||||
className={cn(
|
||||
pathname === MAIN && 'max-w-[1000px] mx-auto px-4 lg:px-0'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-between items-center',
|
||||
!isRoot ? '' : 'w-full flex-col md:w-auto md:flex-row'
|
||||
)}
|
||||
>
|
||||
<Link to={!isRoot ? '/' : '/login'} noStyling>
|
||||
<div
|
||||
className={cn(
|
||||
'text-white font-extrabold flex items-center justify-center',
|
||||
isRoot && 'mb-4 md:mb-0'
|
||||
)}
|
||||
>
|
||||
<div className="pr-1.5 -mb-1">
|
||||
<Cpu color={'white'} size={18} />
|
||||
</IconPadding>
|
||||
</div>
|
||||
ThunderHub
|
||||
</HeaderTitle>
|
||||
</div>
|
||||
</Link>
|
||||
<SingleLine>{!isRoot && renderLoggedIn()}</SingleLine>
|
||||
</HeaderLine>
|
||||
</HeaderStyle>
|
||||
</Section>
|
||||
<div className="flex justify-between items-center">
|
||||
{!isRoot && renderLoggedIn()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{open && (
|
||||
<ViewSwitch>
|
||||
<div className="block md:hidden">
|
||||
<BurgerMenu open={open} setOpen={setOpen} />
|
||||
</ViewSwitch>
|
||||
</div>
|
||||
)}
|
||||
<DonateModal
|
||||
payRequest={donatePayRequest}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { FC } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
Home,
|
||||
Cpu,
|
||||
|
|
@ -9,7 +8,6 @@ import {
|
|||
GitPullRequest,
|
||||
Link as LinkIcon,
|
||||
Users,
|
||||
MessageCircle,
|
||||
BarChart2,
|
||||
Shuffle,
|
||||
Grid,
|
||||
|
|
@ -17,14 +15,7 @@ import {
|
|||
LucideProps,
|
||||
} from 'lucide-react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import {
|
||||
unSelectedNavButton,
|
||||
navBackgroundColor,
|
||||
navTextColor,
|
||||
subCardColor,
|
||||
mediaWidths,
|
||||
burgerRowColor,
|
||||
} from '../../styles/Themes';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { useConfigState } from '../../context/ConfigContext';
|
||||
import { Link } from '../../components/link/Link';
|
||||
import { SideSettings } from './sideSettings/SideSettings';
|
||||
|
|
@ -32,88 +23,6 @@ import { NodeInfo } from './nodeInfo/NodeInfo';
|
|||
|
||||
type Icon = FC<LucideProps>;
|
||||
|
||||
const NavigationStyle = styled.div<{ isOpen: boolean }>`
|
||||
grid-area: nav;
|
||||
width: ${({ isOpen }) => (isOpen ? '200px' : '60px')};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const StickyCard = styled.div`
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 16px;
|
||||
`;
|
||||
|
||||
const LinkView = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
`;
|
||||
|
||||
const ButtonSection = styled.div<{ isOpen: boolean }>`
|
||||
width: 100%;
|
||||
${({ isOpen }) =>
|
||||
!isOpen &&
|
||||
css`
|
||||
margin: 8px 0;
|
||||
`}
|
||||
`;
|
||||
|
||||
const NavSeparation = styled.div`
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
interface NavProps {
|
||||
selected: boolean;
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
const NavButton = styled.div<NavProps>`
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
background: ${({ selected }) => selected && navBackgroundColor};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${({ isOpen }) => !isOpen && 'justify-content: center'};
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
margin: 4px 0;
|
||||
color: ${({ selected }) => (selected ? navTextColor : unSelectedNavButton)};
|
||||
|
||||
&:hover {
|
||||
color: ${navTextColor};
|
||||
background: ${navBackgroundColor};
|
||||
}
|
||||
`;
|
||||
|
||||
const BurgerRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
overflow: scroll;
|
||||
background: ${burgerRowColor};
|
||||
margin: 0 -16px;
|
||||
padding: 16px;
|
||||
`;
|
||||
|
||||
const BurgerNav = styled.a<NavProps>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px 16px 8px;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
background: ${({ selected }) => selected && subCardColor};
|
||||
${({ isOpen }) => !isOpen && 'justify-content: center'};
|
||||
color: ${({ selected }) => (selected ? navTextColor : unSelectedNavButton)};
|
||||
`;
|
||||
|
||||
const HOME = '/';
|
||||
const DASHBOARD = '/dashboard';
|
||||
const PEERS = '/peers';
|
||||
|
|
@ -123,7 +32,6 @@ const FORWARDS = '/forwards';
|
|||
const CHAIN_TRANS = '/chain';
|
||||
const TOOLS = '/tools';
|
||||
const STATS = '/stats';
|
||||
const CHAT = '/chat';
|
||||
const SETTINGS = '/settings';
|
||||
const SWAP = '/swap';
|
||||
const AMBOSS = '/amboss';
|
||||
|
|
@ -146,27 +54,41 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
open = true
|
||||
) => (
|
||||
<Link to={link}>
|
||||
<NavButton isOpen={sidebar} selected={pathname === link}>
|
||||
<div
|
||||
className={cn(
|
||||
'p-1 rounded flex items-center w-full no-underline my-1',
|
||||
pathname === link
|
||||
? 'bg-white dark:bg-[#151727] text-[#212735] dark:text-white'
|
||||
: 'text-gray-500',
|
||||
!sidebar && 'justify-center',
|
||||
'hover:text-[#212735] hover:dark:text-white hover:bg-white hover:dark:bg-[#151727]'
|
||||
)}
|
||||
>
|
||||
<NavIcon size={18} />
|
||||
{open && <NavSeparation>{title}</NavSeparation>}
|
||||
</NavButton>
|
||||
{open && <div className="ml-2 text-sm">{title}</div>}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const renderBurgerNav = (title: string, link: string, NavIcon: Icon) => (
|
||||
<Link to={link}>
|
||||
<BurgerNav
|
||||
selected={pathname === link}
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center px-4 pt-4 pb-2 rounded no-underline',
|
||||
pathname === link
|
||||
? 'bg-white dark:bg-[#151727] text-[#212735] dark:text-white'
|
||||
: 'text-gray-500'
|
||||
)}
|
||||
onClick={() => setOpen && setOpen(false)}
|
||||
>
|
||||
<NavIcon />
|
||||
{title}
|
||||
</BurgerNav>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const renderLinks = () => (
|
||||
<ButtonSection isOpen={sidebar}>
|
||||
<div className={cn('w-full', !sidebar && 'my-2')}>
|
||||
{renderNavButton('Home', HOME, Home, sidebar)}
|
||||
{renderNavButton('Dashboard', DASHBOARD, Grid, sidebar)}
|
||||
{renderNavButton('Peers', PEERS, Users, sidebar)}
|
||||
|
|
@ -178,11 +100,11 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
{renderNavButton('Tools', TOOLS, Shield, sidebar)}
|
||||
{renderNavButton('Swap', SWAP, Shuffle, sidebar)}
|
||||
{renderNavButton('Stats', STATS, BarChart2, sidebar)}
|
||||
</ButtonSection>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderBurger = () => (
|
||||
<BurgerRow>
|
||||
<div className="flex justify-start items-center overflow-scroll bg-[#f0f2f8] dark:bg-[#20263d] -mx-4 px-4 py-4">
|
||||
{renderBurgerNav('Home', HOME, Home)}
|
||||
{renderBurgerNav('Dashboard', DASHBOARD, Grid)}
|
||||
{renderBurgerNav('Peers', PEERS, Users)}
|
||||
|
|
@ -194,9 +116,8 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
{renderBurgerNav('Tools', TOOLS, Shield)}
|
||||
{renderBurgerNav('Swap', SWAP, Shuffle)}
|
||||
{renderBurgerNav('Stats', STATS, BarChart2)}
|
||||
{renderBurgerNav('Chat', CHAT, MessageCircle)}
|
||||
{renderBurgerNav('Settings', SETTINGS, Settings)}
|
||||
</BurgerRow>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isBurger) {
|
||||
|
|
@ -204,14 +125,17 @@ export const Navigation = ({ isBurger, setOpen }: NavigationProps) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<NavigationStyle isOpen={sidebar}>
|
||||
<StickyCard>
|
||||
<LinkView>
|
||||
<div
|
||||
className="[grid-area:nav] hidden md:block"
|
||||
style={{ width: sidebar ? '200px' : '60px' }}
|
||||
>
|
||||
<div className="sticky top-4">
|
||||
<div className="flex flex-col items-start py-2">
|
||||
{!isRoot && <NodeInfo isOpen={sidebar} />}
|
||||
{renderLinks()}
|
||||
<SideSettings />
|
||||
</LinkView>
|
||||
</StickyCard>
|
||||
</NavigationStyle>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import { Zap, Anchor, Circle } from 'lucide-react';
|
||||
import { Tooltip as ReactTooltip } from 'react-tooltip';
|
||||
import styled from 'styled-components';
|
||||
import { getPrice, Price } from '../../../components/price/Price';
|
||||
import { addEllipsis, renderLine } from '../../../components/generic/helpers';
|
||||
import { useNodeInfo } from '../../../hooks/UseNodeInfo';
|
||||
import { useNodeBalances } from '../../../hooks/UseNodeBalances';
|
||||
import Big from 'big.js';
|
||||
import { unSelectedNavButton } from '../../../styles/Themes';
|
||||
import {
|
||||
Separation,
|
||||
SingleLine,
|
||||
|
|
@ -16,69 +14,6 @@ import {
|
|||
import { useConfigState } from '../../../context/ConfigContext';
|
||||
import { usePriceState } from '../../../context/PriceContext';
|
||||
|
||||
const Closed = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Margin = styled.div`
|
||||
margin: 8px 0 2px;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Info = styled.div<{ bottomColor: string }>`
|
||||
font-size: 14px;
|
||||
color: #bfbfbf;
|
||||
border-bottom: 2px solid ${({ bottomColor }) => bottomColor};
|
||||
`;
|
||||
|
||||
const Balance = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 2px 0;
|
||||
padding: 0 5px;
|
||||
cursor: default;
|
||||
`;
|
||||
|
||||
const Alias = styled.div<{ bottomColor: string }>`
|
||||
max-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
border-bottom: 2px solid ${({ bottomColor }) => bottomColor};
|
||||
`;
|
||||
|
||||
const ProgressBar = styled.div<{ percentage: number; color: string }>`
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: #3a3a3a;
|
||||
border-radius: 3px;
|
||||
margin-top: 6px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #555;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: ${({ percentage }) => Math.min(Math.max(percentage, 0), 100)}%;
|
||||
height: 100%;
|
||||
background-color: ${({ color }) => color};
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
`;
|
||||
|
||||
interface NodeInfoProps {
|
||||
isOpen?: boolean;
|
||||
isBurger?: boolean;
|
||||
|
|
@ -165,7 +100,7 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
|
|||
if (!isOpen) {
|
||||
return (
|
||||
<>
|
||||
<Closed>
|
||||
<div className="flex justify-center items-center flex-col w-full">
|
||||
<div data-tip data-for="full_balance_tip">
|
||||
<Circle size={18} strokeWidth={'0'} fill={syncColor} />
|
||||
{(channelPending > 0 || chainPending > 0) && (
|
||||
|
|
@ -173,13 +108,13 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
|
|||
<Circle size={18} fill={'#652EC7'} strokeWidth={'0'} />
|
||||
</div>
|
||||
)}
|
||||
<Margin>
|
||||
<div className="mt-2 mb-0.5">
|
||||
<Zap
|
||||
size={18}
|
||||
fill={channelPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
color={channelPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
/>
|
||||
</Margin>
|
||||
</div>
|
||||
<Anchor
|
||||
size={18}
|
||||
color={chainPending === 0 ? '#FFD300' : '#652EC7'}
|
||||
|
|
@ -191,8 +126,8 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
|
|||
<SingleLine>{closedChannelCount}</SingleLine>
|
||||
<SingleLine>{peersCount}</SingleLine>
|
||||
</div>
|
||||
</Closed>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
</div>
|
||||
<Separation lineColor={'grey'} />
|
||||
<ReactTooltip id={'full_balance_tip'} place={'right'}>
|
||||
{renderLine('Channel Balance', formatCCB)}
|
||||
{renderLine('Pending Channel Balance', formatPCB)}
|
||||
|
|
@ -211,35 +146,61 @@ export const NodeInfo = ({ isOpen, isBurger }: NodeInfoProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title>
|
||||
<Alias bottomColor={color} data-tip={`Version: ${version}`}>
|
||||
<div className="text-lg font-bold flex justify-center items-center">
|
||||
<div
|
||||
className="max-w-[200px] text-ellipsis whitespace-nowrap overflow-hidden"
|
||||
style={{ borderBottom: `2px solid ${color}` }}
|
||||
data-tip={`Version: ${version}`}
|
||||
>
|
||||
{alias}
|
||||
</Alias>
|
||||
</Title>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<Balance data-tip data-for="balance_tip">
|
||||
</div>
|
||||
</div>
|
||||
<Separation lineColor={'grey'} />
|
||||
<div
|
||||
className="flex justify-center items-center my-0.5 px-[5px] cursor-default"
|
||||
data-tip
|
||||
data-for="balance_tip"
|
||||
>
|
||||
<Zap size={18} color={channelPending === 0 ? '#FFD300' : '#652EC7'} />
|
||||
<Price amount={totalLightning} />
|
||||
</Balance>
|
||||
<Balance data-tip data-for="chain_balance_tip">
|
||||
</div>
|
||||
<div
|
||||
className="flex justify-center items-center my-0.5 px-[5px] cursor-default"
|
||||
data-tip
|
||||
data-for="chain_balance_tip"
|
||||
>
|
||||
<Anchor size={18} color={chainPending === 0 ? '#FFD300' : '#652EC7'} />
|
||||
<Price amount={totalChain} />
|
||||
</Balance>
|
||||
<Balance
|
||||
</div>
|
||||
<div
|
||||
className="flex justify-center items-center my-0.5 px-[5px] cursor-default"
|
||||
data-tip
|
||||
data-for="node_tip"
|
||||
>{`${activeChannelCount} / ${pendingChannelCount} / ${closedChannelCount} / ${peersCount}`}</Balance>
|
||||
<Balance>
|
||||
>{`${activeChannelCount} / ${pendingChannelCount} / ${closedChannelCount} / ${peersCount}`}</div>
|
||||
<div className="flex justify-center items-center my-0.5 px-[5px] cursor-default">
|
||||
<div style={{ width: '100%' }}>
|
||||
<Info bottomColor={syncedToChain ? syncColor : 'transparent'}>
|
||||
<div
|
||||
className="text-sm text-[#bfbfbf]"
|
||||
style={{
|
||||
borderBottom: `2px solid ${syncedToChain ? syncColor : 'transparent'}`,
|
||||
}}
|
||||
>
|
||||
{syncText}
|
||||
</Info>
|
||||
</div>
|
||||
{!!syncPercentage && (
|
||||
<ProgressBar percentage={syncPercentage} color={syncColor} />
|
||||
<div className="w-full h-1.5 bg-[#3a3a3a] rounded-[3px] mt-1.5 overflow-hidden border border-[#555]">
|
||||
<div
|
||||
className="h-full rounded-[2px] transition-[width] duration-300 ease-in-out"
|
||||
style={{
|
||||
width: `${Math.min(Math.max(syncPercentage, 0), 100)}%`,
|
||||
backgroundColor: syncColor,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Balance>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
</div>
|
||||
<Separation lineColor={'grey'} />
|
||||
<ReactTooltip place={'right'} />
|
||||
<ReactTooltip id={'balance_tip'} place={'right'}>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -6,60 +6,17 @@ import {
|
|||
ChevronRight,
|
||||
LucideProps,
|
||||
} from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { cn } from '../../../lib/utils';
|
||||
import { SatoshiSymbol } from '../../../components/satoshi/Satoshi';
|
||||
import { Separation, SingleLine } from '../../../components/generic/Styled';
|
||||
import {
|
||||
useConfigState,
|
||||
useConfigDispatch,
|
||||
} from '../../../context/ConfigContext';
|
||||
import {
|
||||
progressBackground,
|
||||
iconButtonHover,
|
||||
inverseTextColor,
|
||||
unSelectedNavButton,
|
||||
} from '../../../styles/Themes';
|
||||
import { usePriceState } from '../../../context/PriceContext';
|
||||
|
||||
type Icon = FC<LucideProps>;
|
||||
|
||||
const SelectedIcon = styled.div<{ selected: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 100%;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (min-width: 579px) {
|
||||
&:hover {
|
||||
background-color: ${iconButtonHover};
|
||||
color: ${inverseTextColor};
|
||||
}
|
||||
}
|
||||
background-color: ${({ selected }) => (selected ? progressBackground : '')};
|
||||
`;
|
||||
|
||||
const Symbol = styled.div`
|
||||
margin-top: 2px;
|
||||
font-weight: 700;
|
||||
`;
|
||||
|
||||
const IconRow = styled.div<{ center?: boolean }>`
|
||||
margin: 5px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
${({ center }) => center && 'width: 100%'}
|
||||
`;
|
||||
|
||||
const BurgerPadding = styled(SingleLine)`
|
||||
margin: 16px 0;
|
||||
`;
|
||||
|
||||
const currencyArray = ['sat', 'btc', 'fiat'];
|
||||
const currencyNoFiatArray = ['sat', 'btc'];
|
||||
|
||||
|
|
@ -113,18 +70,24 @@ export const SideSettings = ({ isBurger }: SideSettingsProps) => {
|
|||
if (text === 'S') {
|
||||
return <SatoshiSymbol />;
|
||||
}
|
||||
return <Symbol>{text}</Symbol>;
|
||||
return <div className="mt-0.5 font-bold">{text}</div>;
|
||||
}
|
||||
if (type === 'theme' && SideIcon) {
|
||||
return <SideIcon size={18} />;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const selected =
|
||||
(type === 'currency' ? currency === value : theme === value) || on;
|
||||
|
||||
return (
|
||||
<SelectedIcon
|
||||
selected={
|
||||
(type === 'currency' ? currency === value : theme === value) || on
|
||||
}
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-center items-center outline-none w-[30px] h-[30px] rounded-full mx-[5px] cursor-pointer',
|
||||
'sm:hover:bg-[#5163ba] sm:hover:text-white sm:dark:hover:text-[#212735]',
|
||||
selected && 'bg-[#e1e6ed] dark:bg-[#212735]'
|
||||
)}
|
||||
onClick={() => {
|
||||
localStorage.setItem(type, value);
|
||||
if (type === 'currency') {
|
||||
|
|
@ -138,7 +101,7 @@ export const SideSettings = ({ isBurger }: SideSettingsProps) => {
|
|||
}}
|
||||
>
|
||||
{renderText()}
|
||||
</SelectedIcon>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -146,11 +109,11 @@ export const SideSettings = ({ isBurger }: SideSettingsProps) => {
|
|||
if (!sidebar) {
|
||||
return (
|
||||
<>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<IconRow center={true}>
|
||||
<Separation lineColor={'grey'} />
|
||||
<div className="my-[5px] flex justify-center items-center w-full">
|
||||
{renderIcon('currency', currency, correctMap[currency], true)}
|
||||
</IconRow>
|
||||
<IconRow center={true}>
|
||||
</div>
|
||||
<div className="my-[5px] flex justify-center items-center w-full">
|
||||
{renderIcon(
|
||||
'theme',
|
||||
getNextValue(themeArray, theme),
|
||||
|
|
@ -158,56 +121,65 @@ export const SideSettings = ({ isBurger }: SideSettingsProps) => {
|
|||
true,
|
||||
getNextValue(themeArray, theme) === 'light' ? Sun : Moon
|
||||
)}
|
||||
</IconRow>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Separation lineColor={unSelectedNavButton} />
|
||||
<IconRow>
|
||||
<Separation lineColor={'grey'} />
|
||||
<div className="my-[5px] flex justify-center items-center">
|
||||
{renderIcon('currency', 'sat', 'S')}
|
||||
{renderIcon('currency', 'btc', '₿')}
|
||||
{!dontShow && renderIcon('currency', 'fiat', 'F')}
|
||||
</IconRow>
|
||||
<IconRow>
|
||||
</div>
|
||||
<div className="my-[5px] flex justify-center items-center">
|
||||
{renderIcon('theme', 'light', '', false, Sun)}
|
||||
{renderIcon('theme', 'dark', '', false, Moon)}
|
||||
</IconRow>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (isBurger) {
|
||||
return (
|
||||
<BurgerPadding>
|
||||
<IconRow>
|
||||
<SingleLine className="my-4">
|
||||
<div className="my-[5px] flex justify-center items-center">
|
||||
{renderIcon('currency', 'sat', 'S')}
|
||||
{renderIcon('currency', 'btc', '₿')}
|
||||
{!dontShow && renderIcon('currency', 'fiat', 'F')}
|
||||
</IconRow>
|
||||
<IconRow>
|
||||
</div>
|
||||
<div className="my-[5px] flex justify-center items-center">
|
||||
{renderIcon('theme', 'light', '', false, Sun)}
|
||||
{renderIcon('theme', 'dark', '', false, Moon)}
|
||||
</IconRow>
|
||||
</BurgerPadding>
|
||||
</div>
|
||||
</SingleLine>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderContent()}
|
||||
<IconRow center={!sidebar}>
|
||||
<SelectedIcon
|
||||
selected={true}
|
||||
<div
|
||||
className={cn(
|
||||
'my-[5px] flex justify-center items-center',
|
||||
!sidebar && 'w-full'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-center items-center outline-none w-[30px] h-[30px] rounded-full mx-[5px] cursor-pointer',
|
||||
'sm:hover:bg-[#5163ba] sm:hover:text-white sm:dark:hover:text-[#212735]',
|
||||
'bg-[#e1e6ed] dark:bg-[#212735]'
|
||||
)}
|
||||
onClick={() => {
|
||||
localStorage.setItem('sidebar', (!sidebar).toString());
|
||||
dispatch({ type: 'change', sidebar: !sidebar });
|
||||
}}
|
||||
>
|
||||
{sidebar ? <ChevronLeft size={18} /> : <ChevronRight size={18} />}
|
||||
</SelectedIcon>
|
||||
</IconRow>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { Card } from '../components/generic/CardGeneric';
|
||||
import {
|
||||
CardWithTitle,
|
||||
|
|
@ -14,14 +13,6 @@ import { CloseChannel } from '../components/modal/closeChannel/CloseChannel';
|
|||
import { useGetChannelInfoQuery } from '../graphql/queries/__generated__/getChannel.generated';
|
||||
import { ChannelDetails } from '../views/channels/channels/ChannelDetails';
|
||||
|
||||
const S = {
|
||||
row: styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
`,
|
||||
};
|
||||
|
||||
const Channel = () => {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
|
||||
|
|
@ -35,11 +26,11 @@ const Channel = () => {
|
|||
if (loading) {
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<S.row>
|
||||
<div className="flex justify-start items-center">
|
||||
<Link to={'/channels'}>Channels</Link>
|
||||
<ChevronRight size={18} />
|
||||
<SubTitle>{slug}</SubTitle>
|
||||
</S.row>
|
||||
</div>
|
||||
<LoadingCard noTitle />
|
||||
</CardWithTitle>
|
||||
);
|
||||
|
|
@ -48,11 +39,11 @@ const Channel = () => {
|
|||
if (!data?.getChannel || error) {
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<S.row>
|
||||
<div className="flex justify-start items-center">
|
||||
<Link to={'/channels'}>Channels</Link>
|
||||
<ChevronRight size={18} />
|
||||
<SubTitle>{slug}</SubTitle>
|
||||
</S.row>
|
||||
</div>
|
||||
<Card>
|
||||
<DarkSubTitle>
|
||||
Error getting channel information. Try refreshing the page.
|
||||
|
|
@ -64,11 +55,11 @@ const Channel = () => {
|
|||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<S.row>
|
||||
<div className="flex justify-start items-center">
|
||||
<Link to={'/channels'}>Channels</Link>
|
||||
<ChevronRight size={18} />
|
||||
<SubTitle>{slug}</SubTitle>
|
||||
</S.row>
|
||||
</div>
|
||||
<Card>
|
||||
<ChannelDetails
|
||||
id={id}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Settings } from 'lucide-react';
|
||||
import { ChannelManage } from '../views/channels/channels/ChannelManage';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
|
|
@ -12,30 +11,17 @@ import {
|
|||
SmallButton,
|
||||
Card,
|
||||
} from '../components/generic/Styled';
|
||||
import { mediaWidths } from '../styles/Themes';
|
||||
import { ChannelTable } from '../views/channels/channels/ChannelTable';
|
||||
|
||||
export const IconCursor = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
`;
|
||||
|
||||
const ChannelsCardTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const ButtonRow = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`;
|
||||
export const IconCursor = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={`flex items-center cursor-pointer ml-2 ${className ?? ''}`}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const ChannelView = () => {
|
||||
const [isOpen, isOpenSet] = useState<boolean>(false);
|
||||
|
|
@ -100,9 +86,9 @@ const ChannelView = () => {
|
|||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<ChannelsCardTitle>
|
||||
<div className="flex justify-between flex-col items-center md:flex-row">
|
||||
<SubTitle>{getTitle()}</SubTitle>
|
||||
<ButtonRow>
|
||||
<div className="flex flex-wrap">
|
||||
<SmallButton onClick={() => setView(1)}>
|
||||
{`Open (${amounts.active})`}
|
||||
</SmallButton>
|
||||
|
|
@ -117,8 +103,8 @@ const ChannelView = () => {
|
|||
<Settings size={16} onClick={() => isOpenSet(p => !p)} />
|
||||
</IconCursor>
|
||||
)}
|
||||
</ButtonRow>
|
||||
</ChannelsCardTitle>
|
||||
</div>
|
||||
</div>
|
||||
{view === 1 && isOpen && <ChannelManage />}
|
||||
{getView()}
|
||||
</CardWithTitle>
|
||||
|
|
|
|||
|
|
@ -1,136 +0,0 @@
|
|||
import { useReducer } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Users } from 'lucide-react';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
import { ChatInit } from '../components/chat/ChatInit';
|
||||
import { ChatFetcher } from '../components/chat/ChatFetcher';
|
||||
import { useChatState } from '../context/ChatContext';
|
||||
import { separateBySender, getSenders } from '../utils/chat';
|
||||
import {
|
||||
CardWithTitle,
|
||||
SubTitle,
|
||||
SingleLine,
|
||||
} from '../components/generic/Styled';
|
||||
import { Contacts } from '../views/chat/Contacts';
|
||||
import { ChatBox } from '../views/chat/ChatBox';
|
||||
import { ChatStart } from '../views/chat/ChatStart';
|
||||
import { LoadingCard } from '../components/loading/LoadingCard';
|
||||
import { ChatCard } from '../views/chat/Chat.styled';
|
||||
import { ViewSwitch } from '../components/viewSwitch/ViewSwitch';
|
||||
import { ColorButton } from '../components/buttons/colorButton/ColorButton';
|
||||
|
||||
const ChatLayout = styled.div<{ withHeight: boolean }>`
|
||||
display: flex;
|
||||
${({ withHeight = true }) => withHeight && 'height: 600px'}
|
||||
`;
|
||||
|
||||
type State = {
|
||||
user: string;
|
||||
showContacts: boolean;
|
||||
};
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: 'setUserAndHide' | 'setUser';
|
||||
user: string;
|
||||
}
|
||||
| { type: 'toggleShow' };
|
||||
|
||||
const initialState: State = { user: '', showContacts: false };
|
||||
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'setUser':
|
||||
return { ...state, user: action.user };
|
||||
case 'setUserAndHide':
|
||||
return { user: action.user, showContacts: false };
|
||||
case 'toggleShow':
|
||||
return { ...state, showContacts: !state.showContacts };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const ChatView = () => {
|
||||
const { chats, sender, sentChats, initialized } = useChatState();
|
||||
const bySender = separateBySender([...chats, ...sentChats]);
|
||||
const senders = getSenders(bySender) || [];
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const { user, showContacts } = state;
|
||||
|
||||
const setUser = (user: string) => dispatch({ type: 'setUserAndHide', user });
|
||||
const setName = (user: string) => dispatch({ type: 'setUser', user });
|
||||
|
||||
if (!initialized) {
|
||||
return <LoadingCard title={'Chats'} />;
|
||||
}
|
||||
|
||||
const renderChats = () => {
|
||||
if (showContacts) {
|
||||
return (
|
||||
<Contacts
|
||||
contacts={senders}
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
setName={setName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatLayout withHeight={user !== 'New Chat'}>
|
||||
<Contacts
|
||||
contacts={senders}
|
||||
user={user}
|
||||
setUser={setUser}
|
||||
setName={setName}
|
||||
hide={true}
|
||||
/>
|
||||
{user === 'New Chat' ? (
|
||||
<ChatStart noTitle={true} callback={() => setUser('')} />
|
||||
) : (
|
||||
<ChatBox messages={bySender[sender]} alias={user} />
|
||||
)}
|
||||
</ChatLayout>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
{!showContacts && user !== 'New Chat' && (
|
||||
<>
|
||||
<ViewSwitch hideMobile={true}>
|
||||
<SingleLine>
|
||||
<SubTitle>Chat</SubTitle>
|
||||
</SingleLine>
|
||||
</ViewSwitch>
|
||||
<ViewSwitch>
|
||||
<SingleLine>
|
||||
<ColorButton onClick={() => dispatch({ type: 'toggleShow' })}>
|
||||
<Users size={18} />
|
||||
</ColorButton>
|
||||
<SubTitle>{user}</SubTitle>
|
||||
</SingleLine>
|
||||
</ViewSwitch>
|
||||
</>
|
||||
)}
|
||||
<ChatCard mobileCardPadding={'0'}>
|
||||
{chats.length <= 0 && sentChats.length <= 0 ? (
|
||||
<ChatStart callback={() => setUser('')} />
|
||||
) : (
|
||||
renderChats()
|
||||
)}
|
||||
</ChatCard>
|
||||
</CardWithTitle>
|
||||
);
|
||||
};
|
||||
|
||||
const ChatPage = () => (
|
||||
<GridWrapper>
|
||||
<ChatInit />
|
||||
<ChatFetcher />
|
||||
<ChatView />
|
||||
</GridWrapper>
|
||||
);
|
||||
|
||||
export default ChatPage;
|
||||
|
|
@ -5,7 +5,6 @@ import { useMemo, useState } from 'react';
|
|||
import { ForwardTable } from '../views/forwards/ForwardTable';
|
||||
import { options, typeOptions } from '../views/home/reports/forwardReport';
|
||||
import { ForwardsGraph } from '../views/home/reports/forwardReport/ForwardsGraph';
|
||||
import styled from 'styled-components';
|
||||
import { SelectWithValue } from '../components/select';
|
||||
import { ForwardResume } from '../views/home/reports/forwardReport/ForwardResume';
|
||||
import {
|
||||
|
|
@ -21,20 +20,6 @@ import { useGetForwardsListQuery } from '../graphql/queries/__generated__/getFor
|
|||
import toast from 'react-hot-toast';
|
||||
import { getErrorContent } from '../utils/error';
|
||||
|
||||
const S = {
|
||||
header: styled.div`
|
||||
margin: 0 0 8px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
`,
|
||||
options: styled.div`
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
`,
|
||||
};
|
||||
|
||||
const viewOptions = [
|
||||
{ label: 'Graph', value: 'graph' },
|
||||
{ label: 'List', value: 'list' },
|
||||
|
|
@ -70,9 +55,10 @@ const ForwardsView = () => {
|
|||
<>
|
||||
<CardWithTitle>
|
||||
<CardTitle>
|
||||
<S.header>
|
||||
<div className="mb-2 w-full flex flex-col md:flex-row gap-2">
|
||||
<SubTitle>Forwards</SubTitle>
|
||||
<S.options>
|
||||
|
||||
<div className="flex grow gap-2 justify-end">
|
||||
<SelectWithValue
|
||||
callback={e => setView((e[0] || viewOptions[0]) as any)}
|
||||
options={viewOptions}
|
||||
|
|
@ -95,7 +81,10 @@ const ForwardsView = () => {
|
|||
isClearable={false}
|
||||
maxWidth={'110px'}
|
||||
/>
|
||||
) : (
|
||||
) : null}
|
||||
</div>
|
||||
<div className="">
|
||||
{view.value != 'byChannel' ? null : (
|
||||
<SelectWithValue
|
||||
callback={e => setChannel((e[0] || emptyChannel) as any)}
|
||||
options={
|
||||
|
|
@ -105,12 +94,10 @@ const ForwardsView = () => {
|
|||
}
|
||||
value={channel}
|
||||
isClearable={false}
|
||||
maxWidth={'340px'}
|
||||
minWidth={'250px'}
|
||||
/>
|
||||
)}
|
||||
</S.options>
|
||||
</S.header>
|
||||
</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
{view.value === 'list' && (
|
||||
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { Spacer } from '../components/spacer/Spacer';
|
||||
import { ThunderStorm } from '../views/homepage/HomePage.styled';
|
||||
import { appendBasePath } from '../utils/basePath';
|
||||
import { TopSection } from '../views/homepage/Top';
|
||||
import { Accounts } from '../views/homepage/Accounts';
|
||||
|
||||
const LoginPage = () => (
|
||||
<>
|
||||
<ThunderStorm alt={''} src={appendBasePath('/static/thunderstorm.webp')} />
|
||||
<img
|
||||
alt={''}
|
||||
src={appendBasePath('/static/thunderstorm.webp')}
|
||||
className="h-80 w-full top-0 object-cover absolute z-[-1] bg-[#151727]"
|
||||
/>
|
||||
<TopSection />
|
||||
<Accounts />
|
||||
<Spacer />
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useGetPeersQuery } from '../graphql/queries/__generated__/getPeers.generated';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
import {
|
||||
|
|
@ -11,10 +12,17 @@ import { LoadingCard } from '../components/loading/LoadingCard';
|
|||
import { AddPeer } from '../views/peers/AddPeer';
|
||||
import { copyLink, getNodeLink } from '../components/generic/helpers';
|
||||
import { Price } from '../components/price/Price';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { RemovePeerModal } from '../components/modal/removePeer/RemovePeer';
|
||||
import Modal from '../components/modal/ReactModal';
|
||||
import Table from '../components/table';
|
||||
|
||||
const PeersView = () => {
|
||||
const { loading, data } = useGetPeersQuery();
|
||||
const [removePeer, setRemovePeer] = useState<{
|
||||
publicKey: string;
|
||||
alias: string;
|
||||
} | null>(null);
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
const channelData = data?.getPeers || [];
|
||||
|
|
@ -102,6 +110,25 @@ const PeersView = () => {
|
|||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: '',
|
||||
id: 'actions',
|
||||
cell: ({ row }: any) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-red-500"
|
||||
onClick={() =>
|
||||
setRemovePeer({
|
||||
publicKey: row.original.public_key,
|
||||
alias: row.original.alias,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
|
@ -123,19 +150,30 @@ const PeersView = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<SubTitle>Peers</SubTitle>
|
||||
<Card mobileNoBackground={true}>
|
||||
<Table
|
||||
withBorder={true}
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
withSorting={true}
|
||||
withGlobalSort={true}
|
||||
filterPlaceholder="peers"
|
||||
/>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
<>
|
||||
<CardWithTitle>
|
||||
<SubTitle>Peers</SubTitle>
|
||||
<Card mobileNoBackground={true}>
|
||||
<Table
|
||||
withBorder={true}
|
||||
columns={columns}
|
||||
data={tableData}
|
||||
withSorting={true}
|
||||
withGlobalSort={true}
|
||||
filterPlaceholder="peers"
|
||||
/>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
<Modal isOpen={!!removePeer} closeCallback={() => setRemovePeer(null)}>
|
||||
{removePeer && (
|
||||
<RemovePeerModal
|
||||
setModalOpen={() => setRemovePeer(null)}
|
||||
publicKey={removePeer.publicKey}
|
||||
peerAlias={removePeer.alias}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import styled from 'styled-components';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
import { DashboardSettings } from '../views/settings/Dashboard';
|
||||
import { SingleLine } from '../components/generic/Styled';
|
||||
|
|
@ -10,15 +9,22 @@ import { Security } from '../views/settings/Security';
|
|||
import { NetworkInfo } from '../views/home/networkInfo/NetworkInfo';
|
||||
import { NotificationSettings } from '../views/settings/Notifications';
|
||||
import { AmbossSettings } from '../views/settings/Amboss';
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const ButtonRow = styled.div`
|
||||
width: auto;
|
||||
display: flex;
|
||||
`;
|
||||
export const ButtonRow = ({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex w-auto', className)} {...props} />
|
||||
);
|
||||
|
||||
export const SettingsLine = styled(SingleLine)`
|
||||
margin: 8px 0;
|
||||
`;
|
||||
export const SettingsLine = ({
|
||||
className,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement>) => (
|
||||
<SingleLine className={cn('my-2', className)} {...props} />
|
||||
);
|
||||
|
||||
const SettingsView = () => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,9 @@
|
|||
import styled from 'styled-components';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
import { VolumeStats } from '../views/stats/FlowStats';
|
||||
import { TimeStats } from '../views/stats/TimeStats';
|
||||
import { FeeStats } from '../views/stats/FeeStats';
|
||||
import { StatResume } from '../views/stats/StatResume';
|
||||
import { StatsProvider } from '../views/stats/context';
|
||||
import { SingleLine } from '../components/generic/Styled';
|
||||
|
||||
export const ButtonRow = styled.div`
|
||||
width: auto;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const SettingsLine = styled(SingleLine)`
|
||||
margin: 8px 0;
|
||||
`;
|
||||
|
||||
const StatsView = () => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@ import { useState, useMemo, useCallback } from 'react';
|
|||
import toast from 'react-hot-toast';
|
||||
import { InvoiceCard } from '../views/transactions/InvoiceCard';
|
||||
import { GridWrapper } from '../components/gridWrapper/GridWrapper';
|
||||
import { Settings } from 'lucide-react';
|
||||
import styled from 'styled-components';
|
||||
import { Settings, Loader2 } from 'lucide-react';
|
||||
import { useLocalStorage } from '../hooks/UseLocalStorage';
|
||||
import { useNodeInfo } from '../hooks/UseNodeInfo';
|
||||
import {
|
||||
|
|
@ -19,7 +18,7 @@ import {
|
|||
} from '../components/generic/Styled';
|
||||
import { getErrorContent } from '../utils/error';
|
||||
import { PaymentsCard } from '../views/transactions/PaymentsCards';
|
||||
import { ColorButton } from '../components/buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { FlowBox } from '../views/home/reports/flow';
|
||||
import {
|
||||
GetInvoicesQuery,
|
||||
|
|
@ -31,17 +30,6 @@ import {
|
|||
} from '../graphql/queries/__generated__/getPayments.generated';
|
||||
import { SmallSelectWithValue } from '../components/select';
|
||||
|
||||
const S = {
|
||||
row: styled.div`
|
||||
width: 100%;
|
||||
display: grid;
|
||||
column-gap: 16px;
|
||||
grid-template-columns: 1fr 110px 50px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
`,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'Invoices', value: 'invoices' },
|
||||
{ label: 'Payments', value: 'payments' },
|
||||
|
|
@ -225,7 +213,10 @@ const TransactionsView = () => {
|
|||
<>
|
||||
<FlowBox />
|
||||
<CardWithTitle>
|
||||
<S.row>
|
||||
<div
|
||||
className="grid w-full items-center gap-4 mb-2"
|
||||
style={{ gridTemplateColumns: '1fr 110px 50px' }}
|
||||
>
|
||||
<SubTitle>
|
||||
Transactions
|
||||
<DarkSubTitle fontSize={'12px'}>{beforeDate}</DarkSubTitle>
|
||||
|
|
@ -236,14 +227,15 @@ const TransactionsView = () => {
|
|||
value={show}
|
||||
isClearable={false}
|
||||
/>
|
||||
<ColorButton
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(p => !p);
|
||||
}}
|
||||
>
|
||||
<Settings size={18} />
|
||||
</ColorButton>
|
||||
</S.row>
|
||||
</Button>
|
||||
</div>
|
||||
{open && (
|
||||
<Card>
|
||||
<TransactionSettings />
|
||||
|
|
@ -252,15 +244,19 @@ const TransactionsView = () => {
|
|||
<Card bottom={'8px'} mobileCardPadding={'0'} mobileNoBackground={true}>
|
||||
{show.value === 'invoices' ? renderInvoices() : renderPayments()}
|
||||
{isDisabled ? null : (
|
||||
<ColorButton
|
||||
loading={loadingOrRefetching}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
style={{ margin: '16px 0 0' }}
|
||||
disabled={loadingOrRefetching}
|
||||
fullWidth={true}
|
||||
withMargin={'16px 0 0'}
|
||||
onClick={() => handleClick()}
|
||||
>
|
||||
Fetch More
|
||||
</ColorButton>
|
||||
{loadingOrRefetching ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>Fetch More</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
import { createGlobalStyle } from 'styled-components';
|
||||
import { backgroundColor, textColor } from './Themes';
|
||||
|
||||
const fontFamily = "'Noto Sans', sans-serif";
|
||||
|
||||
export const GlobalStyles = createGlobalStyle`
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: ${fontFamily};
|
||||
}
|
||||
*, *::after, *::before {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: ${backgroundColor};
|
||||
color: ${textColor};
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: ${fontFamily};
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
`;
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
import theme from 'styled-theming';
|
||||
|
||||
export const themeColors = {
|
||||
white: '#fff',
|
||||
grey: '#f5f6f9',
|
||||
|
|
@ -41,249 +39,11 @@ export const fontColors = {
|
|||
black: '#212735',
|
||||
};
|
||||
|
||||
export const mediaDimensions = {
|
||||
mobile: 700,
|
||||
};
|
||||
export const mediaWidths = {
|
||||
mobile: `max-width: ${mediaDimensions.mobile}px`,
|
||||
};
|
||||
|
||||
// ---------------------------------------
|
||||
// APP COLORS
|
||||
// ---------------------------------------
|
||||
export const backgroundColor = theme('mode', {
|
||||
light: themeColors.grey,
|
||||
dark: themeColors.blue5,
|
||||
});
|
||||
|
||||
export const textColor = theme('mode', {
|
||||
light: fontColors.black,
|
||||
dark: fontColors.white,
|
||||
});
|
||||
|
||||
export const textColorMap: { [key: string]: string } = {
|
||||
light: fontColors.black,
|
||||
dark: fontColors.white,
|
||||
};
|
||||
|
||||
export const inverseTextColor = theme('mode', {
|
||||
light: fontColors.white,
|
||||
dark: fontColors.black,
|
||||
});
|
||||
|
||||
export const burgerColor = theme('mode', {
|
||||
light: themeColors.white,
|
||||
dark: themeColors.blue6,
|
||||
});
|
||||
|
||||
export const burgerRowColor = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue4,
|
||||
});
|
||||
|
||||
export const linkHighlight = theme('mode', {
|
||||
light: fontColors.blue3,
|
||||
dark: fontColors.blue3,
|
||||
});
|
||||
|
||||
export const separationColor = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.black,
|
||||
});
|
||||
|
||||
export const unSelectedNavButton = theme('mode', {
|
||||
light: 'grey',
|
||||
dark: 'grey',
|
||||
});
|
||||
|
||||
export const buttonBorderColor = theme('mode', {
|
||||
light: '#d9d9d9',
|
||||
dark: '#2e3245',
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// HOMEPAGE COLORS
|
||||
// ---------------------------------------
|
||||
export const headerColor = theme('mode', {
|
||||
light: themeColors.blue7,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const headerTextColor = theme('mode', {
|
||||
light: fontColors.white,
|
||||
dark: fontColors.white,
|
||||
});
|
||||
|
||||
export const homeCompatibleColor = theme('mode', {
|
||||
light: themeColors.blue5,
|
||||
dark: themeColors.blue5,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// CARD COLORS
|
||||
// ---------------------------------------
|
||||
export const cardColor = theme('mode', {
|
||||
light: themeColors.white,
|
||||
dark: themeColors.blue6,
|
||||
});
|
||||
|
||||
export const subCardColor = theme('mode', {
|
||||
light: themeColors.white,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const cardBorderColor = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue4,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// CHAT COLORS
|
||||
// ---------------------------------------
|
||||
export const chatSubCardColor = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const chatBubbleColor = theme('mode', {
|
||||
light: themeColors.blue2,
|
||||
dark: themeColors.blue2,
|
||||
});
|
||||
|
||||
export const chatSentBubbleColor = theme('mode', {
|
||||
light: themeColors.blue3,
|
||||
dark: themeColors.blue3,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// BUTTON COLORS
|
||||
// ---------------------------------------
|
||||
export const colorButtonBackground = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const colorButtonBorder = theme('mode', {
|
||||
light: themeColors.blue3,
|
||||
dark: themeColors.blue3,
|
||||
});
|
||||
|
||||
export const colorButtonBorderTwo = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const disabledButtonBackground = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const disabledButtonBorder = theme('mode', {
|
||||
light: themeColors.grey,
|
||||
dark: themeColors.blue6,
|
||||
});
|
||||
|
||||
export const disabledTextColor = theme('mode', {
|
||||
light: fontColors.grey7,
|
||||
dark: fontColors.grey8,
|
||||
});
|
||||
|
||||
export const hoverTextColor = theme('mode', {
|
||||
light: fontColors.white,
|
||||
dark: fontColors.white,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// MULTI BUTTON COLORS
|
||||
// ---------------------------------------
|
||||
export const multiButtonColor = theme('mode', {
|
||||
light: themeColors.grey2,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const multiSelectColor = theme('mode', {
|
||||
light: fontColors.black,
|
||||
dark: fontColors.white,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// NAVIGATION COLORS
|
||||
// ---------------------------------------
|
||||
export const navBackgroundColor = theme('mode', {
|
||||
light: themeColors.white,
|
||||
dark: themeColors.blue7,
|
||||
});
|
||||
|
||||
export const navTextColor = theme('mode', {
|
||||
light: fontColors.black,
|
||||
dark: fontColors.white,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// INPUT COLORS
|
||||
// ---------------------------------------
|
||||
export const inputBackgroundColor = theme('mode', {
|
||||
light: themeColors.grey,
|
||||
dark: themeColors.blue5,
|
||||
});
|
||||
|
||||
export const inputBorderColor = theme('mode', {
|
||||
light: themeColors.grey3,
|
||||
dark: themeColors.grey8,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// SLIDER COLORS
|
||||
// ---------------------------------------
|
||||
export const sliderBackgroundColor = theme('mode', {
|
||||
light: themeColors.grey3,
|
||||
dark: themeColors.grey8,
|
||||
});
|
||||
|
||||
export const sliderThumbColor = theme('mode', {
|
||||
light: themeColors.grey8,
|
||||
dark: 'white',
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// ICON COLORS
|
||||
// ---------------------------------------
|
||||
export const iconButtonHover = theme('mode', {
|
||||
light: themeColors.blue3,
|
||||
dark: themeColors.grey,
|
||||
});
|
||||
|
||||
export const smallLinkColor = theme('mode', {
|
||||
light: '#9254de',
|
||||
dark: '#adc6ff',
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// PROGRESS BAR COLORS
|
||||
// ---------------------------------------
|
||||
export const progressBackground = theme('mode', {
|
||||
light: themeColors.grey3,
|
||||
dark: themeColors.black,
|
||||
});
|
||||
|
||||
// ---------------------------------------
|
||||
// SELECT COLORS
|
||||
// ---------------------------------------
|
||||
export const selectColors = {
|
||||
smallBackground: theme('mode', {
|
||||
light: 'rgba(255, 255, 255, 0.7)',
|
||||
dark: 'rgba(0, 0, 0, 0.2)',
|
||||
}),
|
||||
};
|
||||
|
||||
// ---------------------------------------
|
||||
// CHART COLORS
|
||||
// ---------------------------------------
|
||||
export const chartLinkColor = theme('mode', {
|
||||
light: '#595959',
|
||||
dark: '#8c8c8c',
|
||||
});
|
||||
|
||||
export const chartAxisColor: { [key: string]: string } = {
|
||||
light: '#1b1c22',
|
||||
dark: 'white',
|
||||
|
|
@ -299,13 +59,11 @@ export const chartBarColor: { [key: string]: string } = {
|
|||
dark: chartColors.purple,
|
||||
};
|
||||
|
||||
// ---------------------------------------
|
||||
// Flow Report Bar Colors
|
||||
// ---------------------------------------
|
||||
export const flowBarColor: { [key: string]: string } = {
|
||||
light: chartColors.orange2,
|
||||
dark: chartColors.orange2,
|
||||
};
|
||||
|
||||
export const flowBarColor2: { [key: string]: string } = {
|
||||
light: chartColors.darkyellow,
|
||||
dark: chartColors.darkyellow,
|
||||
|
|
|
|||
|
|
@ -119,8 +119,13 @@
|
|||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { sortBy, groupBy } from 'lodash';
|
||||
import { Message } from '../graphql/types';
|
||||
|
||||
export const separateBySender = (chats: Message[]) => {
|
||||
return groupBy(chats, 'sender');
|
||||
};
|
||||
|
||||
export const getSenders = (
|
||||
bySender: ReturnType<typeof separateBySender>
|
||||
): Message[] => {
|
||||
const senders: Message[] = [];
|
||||
for (const key in bySender) {
|
||||
if (Object.prototype.hasOwnProperty.call(bySender, key)) {
|
||||
const messages = bySender[key];
|
||||
const sorted: Message[] = sortBy(messages, 'date').reverse();
|
||||
|
||||
if (sorted.length > 0) {
|
||||
const chat = sorted[0];
|
||||
if (chat?.sender) {
|
||||
senders.push(chat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return senders;
|
||||
};
|
||||
|
||||
export const getSubMessage = (
|
||||
contentType: string | null,
|
||||
message: string | null,
|
||||
tokens: number | null,
|
||||
isSent: boolean
|
||||
): string => {
|
||||
if (!contentType) return '';
|
||||
if (!message && !tokens) return '';
|
||||
switch (contentType) {
|
||||
case 'payment':
|
||||
if (isSent) {
|
||||
return `Sent ${tokens} sats`;
|
||||
}
|
||||
return `Received ${tokens} sats`;
|
||||
case 'paymentrequest':
|
||||
if (isSent) {
|
||||
return `You requested ${tokens} sats`;
|
||||
}
|
||||
return `Requested ${tokens} sats from you`;
|
||||
default:
|
||||
if (message) return message;
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { ReactNode } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
|
||||
const getMessage = (error: string) => {
|
||||
|
|
@ -26,17 +25,6 @@ const getMessage = (error: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const ErrorLine = styled.div`
|
||||
padding: 4px 0;
|
||||
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
hyphens: auto;
|
||||
`;
|
||||
|
||||
export const getErrorContent = (error: ApolloError): JSX.Element => {
|
||||
const errors = error.graphQLErrors.map(x => x.message);
|
||||
|
||||
|
|
@ -47,7 +35,11 @@ export const getErrorContent = (error: ApolloError): JSX.Element => {
|
|||
return (
|
||||
<div>
|
||||
{errors.map((errorMsg, i) => {
|
||||
return <ErrorLine key={i}>{getMessage(errorMsg)}</ErrorLine>;
|
||||
return (
|
||||
<div key={i} className="py-1 break-words hyphens-auto">
|
||||
{getMessage(errorMsg)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import { SatoshiSymbol } from '../components/satoshi/Satoshi';
|
||||
import { unSelectedNavButton } from '../styles/Themes';
|
||||
import styled from 'styled-components';
|
||||
import { Bitcoin } from 'lucide-react';
|
||||
|
||||
const DarkUnit = styled.span`
|
||||
font-size: 12px;
|
||||
color: ${unSelectedNavButton};
|
||||
`;
|
||||
|
||||
const fmt1 = (n: number) =>
|
||||
n.toLocaleString('en-US', { maximumFractionDigits: 1 });
|
||||
|
||||
|
|
@ -83,9 +76,7 @@ export const getValue = ({
|
|||
return (
|
||||
<>
|
||||
{breakAmount}
|
||||
<DarkUnit as={'span'} className="ml-1">
|
||||
sats
|
||||
</DarkUnit>
|
||||
<span className="ml-1 text-xs text-gray-500">sats</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -108,7 +99,7 @@ export const getValue = ({
|
|||
) : (
|
||||
<>
|
||||
{fiatFormatted}
|
||||
<DarkUnit className="ml-1">{symbol}</DarkUnit>
|
||||
<span className="ml-1 text-xs text-gray-500">{symbol}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { ColorButton } from '../../components/buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardWithTitle,
|
||||
|
|
@ -26,15 +27,14 @@ const PushBackup = () => {
|
|||
return (
|
||||
<SingleLine>
|
||||
<Text>Push Backup to Amboss</Text>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
withMargin={'4px 0'}
|
||||
<Button
|
||||
variant="outline"
|
||||
style={{ margin: '4px 0' }}
|
||||
disabled={loading}
|
||||
onClick={() => backup()}
|
||||
loading={loading}
|
||||
>
|
||||
Push
|
||||
</ColorButton>
|
||||
{loading ? <Loader2 className="animate-spin" size={16} /> : <>Push</>}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
);
|
||||
};
|
||||
|
|
@ -106,17 +106,20 @@ export const Backups = () => {
|
|||
? 'By disabling automatic backups to Amboss, ThunderHub will no longer push encrypted backups.'
|
||||
: 'By enabling automatic backups to Amboss, ThunderHub will automatically push an encrypted version of your static channel backups (SCB) whenever there is a change that needs backing up.'}
|
||||
</Text>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
loading={loading || toggleLoading}
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={loading || toggleLoading}
|
||||
withMargin="0 0 0 16px"
|
||||
style={{ margin: '0 0 0 16px' }}
|
||||
onClick={() =>
|
||||
toggle({ variables: { field: ConfigFields.Backups } })
|
||||
}
|
||||
>
|
||||
{isEnabled ? 'Disable' : 'Enable'}
|
||||
</ColorButton>
|
||||
{loading || toggleLoading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{isEnabled ? 'Disable' : 'Enable'}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
<AmbossBackupsView />
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import toast from 'react-hot-toast';
|
||||
import { ColorButton } from '../../components/buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardWithTitle,
|
||||
|
|
@ -29,23 +30,28 @@ export const Balances = () => {
|
|||
private_channels_push_enabled = false,
|
||||
} = data?.getConfigState || {};
|
||||
|
||||
const isLoading = loading || toggleLoading;
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
<SubTitle>Balances</SubTitle>
|
||||
<Card>
|
||||
<SingleLine>
|
||||
<SubTitle>Push Onchain</SubTitle>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
loading={loading || toggleLoading}
|
||||
disabled={loading || toggleLoading}
|
||||
withMargin="0 0 0 16px"
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
style={{ margin: '0 0 0 16px' }}
|
||||
onClick={() =>
|
||||
toggle({ variables: { field: ConfigFields.OnchainPush } })
|
||||
}
|
||||
>
|
||||
{onchain_push_enabled ? 'Disable' : 'Enable'}
|
||||
</ColorButton>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{onchain_push_enabled ? 'Disable' : 'Enable'}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
<Text>
|
||||
Push your onchain balance to Amboss to get historical reports.
|
||||
|
|
@ -53,17 +59,20 @@ export const Balances = () => {
|
|||
<Separation />
|
||||
<SingleLine>
|
||||
<SubTitle>Push Public Channels</SubTitle>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
loading={loading || toggleLoading}
|
||||
disabled={loading || toggleLoading}
|
||||
withMargin="0 0 0 16px"
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
style={{ margin: '0 0 0 16px' }}
|
||||
onClick={() =>
|
||||
toggle({ variables: { field: ConfigFields.ChannelsPush } })
|
||||
}
|
||||
>
|
||||
{channels_push_enabled ? 'Disable' : 'Enable'}
|
||||
</ColorButton>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{channels_push_enabled ? 'Disable' : 'Enable'}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
<Text>
|
||||
Push your public channel balances to get historical reports.
|
||||
|
|
@ -71,17 +80,20 @@ export const Balances = () => {
|
|||
<Separation />
|
||||
<SingleLine>
|
||||
<SubTitle>Push Private Channels</SubTitle>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
loading={loading || toggleLoading}
|
||||
disabled={loading || toggleLoading}
|
||||
withMargin="0 0 0 16px"
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
style={{ margin: '0 0 0 16px' }}
|
||||
onClick={() =>
|
||||
toggle({ variables: { field: ConfigFields.PrivateChannelsPush } })
|
||||
}
|
||||
>
|
||||
{private_channels_push_enabled ? 'Disable' : 'Enable'}
|
||||
</ColorButton>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{private_channels_push_enabled ? 'Disable' : 'Enable'}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
<Text>
|
||||
Push your private channel balances to get historical reports.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import { ChatInput } from '../chat/ChatInput';
|
||||
import {
|
||||
Card,
|
||||
CardWithTitle,
|
||||
SubTitle,
|
||||
Separation,
|
||||
} from '../../components/generic/Styled';
|
||||
import { Card, CardWithTitle, SubTitle } from '../../components/generic/Styled';
|
||||
import { Text } from '../../components/typography/Styled';
|
||||
import { Link } from '../../components/link/Link';
|
||||
|
||||
|
|
@ -24,13 +18,6 @@ export const Billboard = () => {
|
|||
a message and it will appear on their home page! Messages are sorted
|
||||
by amount of sats sent and how recent it was.
|
||||
</Text>
|
||||
<Separation />
|
||||
<ChatInput
|
||||
alias={'Amboss.Space'}
|
||||
sender={
|
||||
'03006fcf3312dae8d068ea297f58e2bd00ec1ffe214b793eda46966b6294a53ce6'
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import toast from 'react-hot-toast';
|
||||
import { ColorButton } from '../../components/buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardWithTitle,
|
||||
|
|
@ -23,6 +24,7 @@ export const Healthchecks = () => {
|
|||
});
|
||||
|
||||
const isEnabled = data?.getConfigState.healthcheck_ping_state || false;
|
||||
const isLoading = loading || toggleLoading;
|
||||
|
||||
return (
|
||||
<CardWithTitle>
|
||||
|
|
@ -34,17 +36,20 @@ export const Healthchecks = () => {
|
|||
? 'By disabling automatic healthcheck pings to Amboss, ThunderHub will no longer ping Amboss.'
|
||||
: 'By enabling automatic healthcheck pings to Amboss, ThunderHub will consistently ping Amboss to show the liveliness of your node.'}
|
||||
</Text>
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
loading={loading || toggleLoading}
|
||||
disabled={loading || toggleLoading}
|
||||
withMargin="0 0 0 16px"
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={isLoading}
|
||||
style={{ margin: '0 0 0 16px' }}
|
||||
onClick={() =>
|
||||
toggle({ variables: { field: ConfigFields.Healthchecks } })
|
||||
}
|
||||
>
|
||||
{isEnabled ? 'Disable' : 'Enable'}
|
||||
</ColorButton>
|
||||
{isLoading ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<>{isEnabled ? 'Disable' : 'Enable'}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
</Card>
|
||||
</CardWithTitle>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import toast from 'react-hot-toast';
|
|||
import { useAmbossUser } from '../../hooks/UseAmbossUser';
|
||||
import { useLoginAmbossMutation } from '../../graphql/mutations/__generated__/loginAmboss.generated';
|
||||
import { useGetAmbossLoginTokenLazyQuery } from '../../graphql/queries/__generated__/getAmbossLoginToken.generated';
|
||||
import { ColorButton } from '../../components/buttons/colorButton/ColorButton';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export const AmbossLoginButton = () => {
|
||||
const { user } = useAmbossUser();
|
||||
|
|
@ -32,8 +32,7 @@ export const AmbossLoginButton = () => {
|
|||
|
||||
if (!user) {
|
||||
return (
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
|
|
@ -41,13 +40,12 @@ export const AmbossLoginButton = () => {
|
|||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Loading...' : 'Login'}
|
||||
</ColorButton>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorButton
|
||||
color="#ff0080"
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (tokenLoading) return;
|
||||
getToken();
|
||||
|
|
@ -55,6 +53,6 @@ export const AmbossLoginButton = () => {
|
|||
disabled={tokenLoading}
|
||||
>
|
||||
{tokenLoading ? 'Loading...' : 'Go To'}
|
||||
</ColorButton>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import {
|
|||
SingleLine,
|
||||
} from '../../../components/generic/Styled';
|
||||
import { DetailsChange } from '../../../components/details/detailsChange';
|
||||
import { X } from 'lucide-react';
|
||||
import { ColorButton } from '../../../components/buttons/colorButton/ColorButton';
|
||||
import { X, ChevronRight } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { OpenChannel } from '../../home/liquidity/OpenChannel';
|
||||
|
||||
export const ChannelManage = () => {
|
||||
|
|
@ -16,29 +16,37 @@ export const ChannelManage = () => {
|
|||
const renderOpenButton = () => (
|
||||
<SingleLine>
|
||||
<Sub4Title>Open Channel</Sub4Title>
|
||||
<ColorButton
|
||||
arrow={openWindow !== 'open'}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
setOpenWindow(prev => (prev === 'none' ? 'open' : 'none'))
|
||||
}
|
||||
>
|
||||
{openWindow === 'open' ? <X size={16} /> : 'Open'}
|
||||
</ColorButton>
|
||||
{openWindow === 'open' ? (
|
||||
<X size={16} />
|
||||
) : (
|
||||
<>Open {openWindow !== 'open' && <ChevronRight size={18} />}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
);
|
||||
|
||||
const renderDetailsButton = () => (
|
||||
<SingleLine>
|
||||
<Sub4Title>Change Channel Details</Sub4Title>
|
||||
<ColorButton
|
||||
withMargin={'8px 0 0'}
|
||||
arrow={openWindow !== 'details'}
|
||||
<Button
|
||||
variant="outline"
|
||||
style={{ margin: '8px 0 0' }}
|
||||
onClick={() =>
|
||||
setOpenWindow(prev => (prev === 'none' ? 'details' : 'none'))
|
||||
}
|
||||
>
|
||||
{openWindow === 'details' ? <X size={16} /> : 'Change'}
|
||||
</ColorButton>
|
||||
{openWindow === 'details' ? (
|
||||
<X size={16} />
|
||||
) : (
|
||||
<>Change {openWindow !== 'details' && <ChevronRight size={18} />}</>
|
||||
)}
|
||||
</Button>
|
||||
</SingleLine>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
X,
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import styled from 'styled-components';
|
||||
import { BalanceBars } from '../../../components/balance';
|
||||
import {
|
||||
getChannelLink,
|
||||
|
|
@ -31,27 +30,6 @@ import { ChannelDetails } from './ChannelDetails';
|
|||
import { defaultHiddenColumns } from './helpers';
|
||||
import { VisibilityState } from '@tanstack/react-table';
|
||||
|
||||
const S = {
|
||||
link: styled.span`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`,
|
||||
button: styled.button`
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
|
||||
:hover {
|
||||
color: ${chartColors.orange};
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const getBar = (top: number, bottom: number) => {
|
||||
const percent = (top / bottom) * 100;
|
||||
return Math.min(percent, 100);
|
||||
|
|
@ -159,7 +137,8 @@ export const ChannelTable = () => {
|
|||
|
||||
const actions = {
|
||||
editAction: (
|
||||
<S.button
|
||||
<button
|
||||
className="bg-none text-inherit border-none p-0 font-inherit cursor-pointer outline-inherit hover:text-[#FFA940]"
|
||||
onClick={() =>
|
||||
setChannel({
|
||||
channel: c.id,
|
||||
|
|
@ -169,10 +148,11 @@ export const ChannelTable = () => {
|
|||
}
|
||||
>
|
||||
<Edit size={14} />
|
||||
</S.button>
|
||||
</button>
|
||||
),
|
||||
closeAction: (
|
||||
<S.button
|
||||
<button
|
||||
className="bg-none text-inherit border-none p-0 font-inherit cursor-pointer outline-inherit hover:text-[#FFA940]"
|
||||
onClick={() =>
|
||||
setChannel({
|
||||
channel: c.id,
|
||||
|
|
@ -182,7 +162,7 @@ export const ChannelTable = () => {
|
|||
}
|
||||
>
|
||||
<X size={14} />
|
||||
</S.button>
|
||||
</button>
|
||||
),
|
||||
};
|
||||
|
||||
|
|
@ -270,10 +250,10 @@ export const ChannelTable = () => {
|
|||
),
|
||||
viewAction: (
|
||||
<Link to={`/channels/${c.id}`}>
|
||||
<S.link>
|
||||
<span className="flex items-center justify-center">
|
||||
View
|
||||
<ChevronRight size={12} />
|
||||
</S.link>
|
||||
</span>
|
||||
</Link>
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,238 +0,0 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import { DarkSubTitle, SubCard, Card } from '../../components/generic/Styled';
|
||||
import {
|
||||
cardBorderColor,
|
||||
subCardColor,
|
||||
mediaWidths,
|
||||
textColor,
|
||||
chatSubCardColor,
|
||||
colorButtonBorder,
|
||||
chatBubbleColor,
|
||||
chatSentBubbleColor,
|
||||
chartColors,
|
||||
backgroundColor,
|
||||
unSelectedNavButton,
|
||||
} from '../../styles/Themes';
|
||||
|
||||
export const ChatColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
border: 1px solid ${cardBorderColor};
|
||||
margin: 0 0 16px;
|
||||
padding-bottom: 8px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background-color: ${subCardColor};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
border: none;
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatColumnWithInput = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const ChatStyledLine = styled.div<{ rightAlign: boolean }>`
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
align-items: flex-start;
|
||||
${({ rightAlign }) =>
|
||||
rightAlign &&
|
||||
css`
|
||||
align-items: flex-end;
|
||||
`};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatDaySeparator = styled.div<{ isLast?: boolean }>`
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: ${({ isLast }) => (isLast ? '32px 0 8px' : '8px 0')};
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin: 8px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatStyledDark = styled(DarkSubTitle)`
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
interface ChatStyledMessageProps {
|
||||
bubbleColor: string | ThemeSet;
|
||||
}
|
||||
|
||||
export const ChatStyledMessage = styled.div<ChatStyledMessageProps>`
|
||||
margin: 0;
|
||||
position: relative;
|
||||
background-color: ${({ bubbleColor }) => bubbleColor || chatBubbleColor};
|
||||
color: white;
|
||||
max-width: 60%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
max-width: 80%;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatContactColumn = styled.div<{ hide?: boolean }>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
width: 30%;
|
||||
margin-right: 16px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
width: 100%;
|
||||
background-color: ${backgroundColor};
|
||||
${({ hide }) => hide && 'display: none;'}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatStyledStart = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatTitle = styled.div`
|
||||
width: 100%;
|
||||
font-weight: bolder;
|
||||
text-align: center;
|
||||
color: ${textColor};
|
||||
font-size: 24px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
font-size: 16px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatSubCard = styled(SubCard)<{ open?: boolean }>`
|
||||
background: ${chatSubCardColor};
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
box-shadow: unset;
|
||||
${({ open }) =>
|
||||
!open &&
|
||||
css`
|
||||
background-color: ${colorButtonBorder};
|
||||
color: white;
|
||||
`}
|
||||
}
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
margin: 16px 0 -4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatStyledSubTitle = styled.h4`
|
||||
font-weight: 500;
|
||||
`;
|
||||
|
||||
export const ChatBoxAlias = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
margin-top: 8px;
|
||||
|
||||
@media (${mediaWidths.mobile}) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatBoxTopAlias = styled.div`
|
||||
background: ${subCardColor};
|
||||
padding: 4px 16px;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
export const ChatFeePaid = styled.div`
|
||||
margin-right: 8px;
|
||||
font-size: 12px;
|
||||
color: ${chartColors.orange2};
|
||||
`;
|
||||
|
||||
export const ChatFeeDateColumn = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 0 8px;
|
||||
`;
|
||||
|
||||
export const ChatCard = styled(Card)`
|
||||
@media (${mediaWidths.mobile}) {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatBubbleMessage = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-moz-hyphens: auto;
|
||||
hyphens: auto;
|
||||
`;
|
||||
|
||||
export const StatusChatDot = styled.div`
|
||||
position: absolute;
|
||||
top: -3px;
|
||||
right: 3px;
|
||||
`;
|
||||
|
||||
export const ChatSendButton = styled.div`
|
||||
margin: 0 0 0 16px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
background: ${chatSentBubbleColor};
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: ${chatSentBubbleColor};
|
||||
background: white;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ChatContactDate = styled.div`
|
||||
font-size: 12px;
|
||||
color: ${unSelectedNavButton};
|
||||
`;
|
||||
|
||||
export const ChatSubText = styled(ChatContactDate)`
|
||||
overflow: hidden;
|
||||
max-height: 34px;
|
||||
`;
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
import { Fragment } from 'react';
|
||||
import { sortBy } from 'lodash';
|
||||
import { Message } from '../../graphql/types';
|
||||
import { SentChatProps } from '../../context/ChatContext';
|
||||
import {
|
||||
getMessageDate,
|
||||
getIsDifferentDay,
|
||||
getDayChange,
|
||||
} from '../../components/generic/helpers';
|
||||
import { useConfigState } from '../../context/ConfigContext';
|
||||
import { ChatInput } from './ChatInput';
|
||||
import {
|
||||
ChatStyledLine,
|
||||
ChatStyledDark,
|
||||
ChatColumnWithInput,
|
||||
ChatColumn,
|
||||
ChatDaySeparator,
|
||||
ChatBoxAlias,
|
||||
ChatFeePaid,
|
||||
ChatFeeDateColumn,
|
||||
ChatBoxTopAlias,
|
||||
} from './Chat.styled';
|
||||
import { ChatBubble } from './ChatBubble';
|
||||
|
||||
export const MessageCard = ({
|
||||
message,
|
||||
key,
|
||||
}: {
|
||||
message: SentChatProps;
|
||||
key?: string;
|
||||
}) => {
|
||||
const { hideFee, hideNonVerified } = useConfigState();
|
||||
if (!message.message && message.contentType === 'text') {
|
||||
return null;
|
||||
}
|
||||
const { date, isSent, feePaid, verified } = message;
|
||||
|
||||
if (hideNonVerified && !verified && !isSent) return null;
|
||||
|
||||
return (
|
||||
<ChatStyledLine key={key} rightAlign={isSent || false}>
|
||||
<ChatBubble message={message} />
|
||||
<ChatFeeDateColumn>
|
||||
{!hideFee && isSent && feePaid && feePaid > 0 ? (
|
||||
<ChatFeePaid>{`${feePaid} sats`}</ChatFeePaid>
|
||||
) : null}
|
||||
<ChatStyledDark withMargin={'8px'}>
|
||||
{getMessageDate(date)}
|
||||
</ChatStyledDark>
|
||||
</ChatFeeDateColumn>
|
||||
</ChatStyledLine>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatBoxProps {
|
||||
messages: Message[];
|
||||
alias: string;
|
||||
}
|
||||
|
||||
export const ChatBox = ({ messages, alias }: ChatBoxProps) => {
|
||||
if (!messages) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sorted = sortBy(messages, 'date').reverse();
|
||||
|
||||
return (
|
||||
<ChatColumnWithInput>
|
||||
<ChatColumn>
|
||||
{sorted.map((message, index: number) => {
|
||||
const nextDate =
|
||||
index < sorted.length - 1 ? sorted[index + 1].date : message.date;
|
||||
const isDifferent = getIsDifferentDay(message.date, nextDate);
|
||||
return (
|
||||
<Fragment key={`${message.sender}/${message.date}`}>
|
||||
<MessageCard message={message} />
|
||||
{isDifferent && (
|
||||
<ChatDaySeparator>
|
||||
{getDayChange(message.date)}
|
||||
</ChatDaySeparator>
|
||||
)}
|
||||
{index === sorted.length - 1 && (
|
||||
<ChatDaySeparator isLast={true}>
|
||||
{getDayChange(message.date)}
|
||||
</ChatDaySeparator>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</ChatColumn>
|
||||
<ChatInput withMargin={'0'} alias={alias} />
|
||||
<ChatBoxAlias>
|
||||
<ChatBoxTopAlias>{alias}</ChatBoxTopAlias>
|
||||
</ChatBoxAlias>
|
||||
</ChatColumnWithInput>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
import { useEffect } from 'react';
|
||||
import { ThemeSet } from 'styled-theming';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Circle } from 'lucide-react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useSendMessageMutation } from '../../graphql/mutations/__generated__/sendMessage.generated';
|
||||
import { useMutationResultWithReset } from '../../hooks/UseMutationWithReset';
|
||||
import { useAccount } from '../../hooks/UseAccount';
|
||||
import {
|
||||
chatBubbleColor,
|
||||
chatSentBubbleColor,
|
||||
chartColors,
|
||||
} from '../../styles/Themes';
|
||||
import { getErrorContent } from '../../utils/error';
|
||||
import {
|
||||
useChatState,
|
||||
useChatDispatch,
|
||||
SentChatProps,
|
||||
} from '../../context/ChatContext';
|
||||
import { useConfigState } from '../../context/ConfigContext';
|
||||
import { usePriceState } from '../../context/PriceContext';
|
||||
import { getPrice } from '../../components/price/Price';
|
||||
import {
|
||||
ChatStyledMessage,
|
||||
ChatBubbleMessage,
|
||||
StatusChatDot,
|
||||
ChatSendButton,
|
||||
} from './Chat.styled';
|
||||
|
||||
interface SendButtonProps {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
const SendButton = ({ amount }: SendButtonProps) => {
|
||||
const { maxFee } = useConfigState();
|
||||
const { sender } = useChatState();
|
||||
const dispatch = useChatDispatch();
|
||||
|
||||
const account = useAccount();
|
||||
|
||||
const [sendMessage, { loading, data: _data }] = useSendMessageMutation({
|
||||
onError: error => toast.error(getErrorContent(error)),
|
||||
});
|
||||
const [data, resetMutationResult] = useMutationResultWithReset(_data);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && data && data?.sendMessage) {
|
||||
dispatch({
|
||||
type: 'newChat',
|
||||
newChat: {
|
||||
id: '',
|
||||
verified: true,
|
||||
date: new Date().toISOString(),
|
||||
message: 'payment',
|
||||
sender,
|
||||
isSent: true,
|
||||
feePaid: data.sendMessage - 1,
|
||||
contentType: 'payment',
|
||||
tokens: amount,
|
||||
},
|
||||
userId: account?.id || '',
|
||||
sender,
|
||||
});
|
||||
resetMutationResult();
|
||||
}
|
||||
}, [loading, data, amount, dispatch, sender, account, resetMutationResult]);
|
||||
|
||||
return (
|
||||
<ChatSendButton
|
||||
onClick={() =>
|
||||
sendMessage({
|
||||
variables: {
|
||||
message: 'payment',
|
||||
messageType: 'payment',
|
||||
publicKey: sender,
|
||||
tokens: amount,
|
||||
maxFee,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<Loader2 className="animate-spin" size={8} color={'white'} />
|
||||
) : (
|
||||
'Pay'
|
||||
)}
|
||||
</ChatSendButton>
|
||||
);
|
||||
};
|
||||
|
||||
interface ChatBubbleProps {
|
||||
message: SentChatProps;
|
||||
}
|
||||
|
||||
export const ChatBubble = ({ message }: ChatBubbleProps) => {
|
||||
const { currency, displayValues } = useConfigState();
|
||||
const priceContext = usePriceState();
|
||||
const format = getPrice(currency, displayValues, priceContext);
|
||||
|
||||
const {
|
||||
contentType,
|
||||
message: chatMessage = '',
|
||||
isSent,
|
||||
verified,
|
||||
tokens = 0,
|
||||
} = message;
|
||||
|
||||
let color: ThemeSet | string = chatBubbleColor;
|
||||
let textMessage: JSX.Element | string = chatMessage || '';
|
||||
let dotColor = '';
|
||||
let showButton = false;
|
||||
let amount = 0;
|
||||
|
||||
if (isSent) {
|
||||
color = chatSentBubbleColor;
|
||||
if (contentType === 'payment') {
|
||||
dotColor = chartColors.red;
|
||||
if (chatMessage === 'payment') {
|
||||
textMessage = (
|
||||
<>
|
||||
{'You sent '}
|
||||
{format({ amount: tokens })}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
textMessage = (
|
||||
<>
|
||||
{chatMessage}
|
||||
{'('}
|
||||
{format({ amount: tokens })}
|
||||
{')'}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (contentType === 'paymentrequest') {
|
||||
if (chatMessage === 'paymentrequest') {
|
||||
textMessage = (
|
||||
<>
|
||||
{'You requested '}
|
||||
{format({ amount: tokens })}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
textMessage = (
|
||||
<>
|
||||
{chatMessage}
|
||||
{'('}
|
||||
{format({ amount: tokens })}
|
||||
{')'}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (contentType === 'payment') {
|
||||
dotColor = chartColors.green;
|
||||
if (chatMessage === 'payment' || !chatMessage) {
|
||||
textMessage = (
|
||||
<>
|
||||
{'You received '}
|
||||
{format({ amount: tokens })}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
textMessage = (
|
||||
<>
|
||||
{chatMessage}
|
||||
{'('}
|
||||
{format({ amount: tokens })}
|
||||
{')'}
|
||||
</>
|
||||
);
|
||||
}
|
||||
} else if (contentType === 'paymentrequest') {
|
||||
showButton = true;
|
||||
const messageSplit = chatMessage?.split(',') || [''];
|
||||
amount = verified
|
||||
? Number(messageSplit[0])
|
||||
: Math.abs(Number(messageSplit[0]) / 1000); // This is only for Juggernaut compatibility.
|
||||
const finalMessage = [...messageSplit];
|
||||
finalMessage.shift();
|
||||
if (messageSplit[1] === 'paymentrequest' || !messageSplit[1]) {
|
||||
textMessage = (
|
||||
<>
|
||||
{format({ amount: tokens })}
|
||||
{' requested from you'}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
textMessage = (
|
||||
<>
|
||||
{finalMessage.join(' ')}
|
||||
{'('}
|
||||
{format({ amount: tokens })}
|
||||
{')'}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType === 'paymentrequest') {
|
||||
dotColor = 'white';
|
||||
}
|
||||
|
||||
if (!verified && !isSent) {
|
||||
color = 'black';
|
||||
}
|
||||
|
||||
return (
|
||||
<ChatStyledMessage bubbleColor={color}>
|
||||
<ChatBubbleMessage>
|
||||
{textMessage}
|
||||
{showButton && <SendButton amount={amount} />}
|
||||
</ChatBubbleMessage>
|
||||
{dotColor !== '' && (
|
||||
<StatusChatDot>
|
||||
<Circle size={10} color={dotColor} fill={dotColor} />
|
||||
</StatusChatDot>
|
||||
)}
|
||||
</ChatStyledMessage>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue