chore: migrate to tailwind (#655)

This commit is contained in:
Anthony Potdevin 2026-03-12 00:11:29 -06:00 committed by GitHub
parent 9e2efb02c7
commit 89e3e4cfbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
199 changed files with 6631 additions and 8272 deletions

1492
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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!

View file

@ -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>

View file

@ -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>
);
}

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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

View file

@ -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();

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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>
);
};

View file

@ -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>
</>
);

View file

@ -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}
/>
)
);

View file

@ -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}
/>
));

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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);
}
}}
/>
);
};

View file

@ -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>
);
}

View file

@ -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>
);

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -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>
</>
);

View file

@ -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>
);
};

View file

@ -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;
}
`;

View file

@ -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

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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 />;

View file

@ -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>
);
};

View file

@ -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 || ''}`}

View file

@ -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>
</>
);
}

View file

@ -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} />
));

View 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 };

View 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 };

View 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 };

View file

@ -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;
}) {

View 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,
};

View 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 };

View 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,
};

View 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,
};

View 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 };

View 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,
};

View 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 };

View 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,
};

View 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 };

View 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,
};

View 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,
};

View 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 };

View 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 };

View 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 };

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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 };

View file

@ -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>
);

View file

@ -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
>;

View file

@ -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
)
}
`;

View file

@ -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
>;

View file

@ -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
}
}
}
`;

View file

@ -0,0 +1,6 @@
import { useConfigState } from '../context/ConfigContext';
export const useThemeMode = () => {
const { theme } = useConfigState();
return theme as 'dark' | 'light';
};

View file

@ -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;
`;

View file

@ -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;
`;

View file

@ -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>
);
};

View file

@ -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};
}
`;

View file

@ -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}

View file

@ -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>
);
};

View file

@ -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>

View file

@ -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>
</>
);
};

View file

@ -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}

View file

@ -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>

View file

@ -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;

View file

@ -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}>

View file

@ -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 />
</>
);

View file

@ -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>
</>
);
};

View file

@ -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 = () => (
<>

View file

@ -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 = () => (
<>

View file

@ -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>

View file

@ -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;
}
`;

View file

@ -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,

View file

@ -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;
}
}

View file

@ -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 '';
}
};

View file

@ -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>
);

View file

@ -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>
</>
);
};

View file

@ -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>

View file

@ -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.

View file

@ -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>
);

View file

@ -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>

View file

@ -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>
);
};

View file

@ -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>
);

View file

@ -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>
),
};

View file

@ -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;
`;

View file

@ -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>
);
};

View file

@ -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