Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"devDependencies": {
"@types/node": "^22.5.4",
"@types/react": "^18.0.28",
"@types/react": "latest",
"@types/react-dom": "^18.0.11",
"@types/vite-plugin-react-svg": "^0.2.2",
"@vitejs/plugin-react": "^3.1.0",
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,15 @@ select {
#root {
height: 100vh;
width: 100%;
display: grid;
}

.App {
padding: 12px 24px 0;
background-color: rgba(14, 17, 35, 0.02);
min-height: 100%;
display: grid;
align-content: start;
}

@media (max-width: 767px) {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ListItem/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { memo } from 'react';
import './listItem.css';
type ListItemProps = {
Icon: React.ReactNode;
Expand All @@ -23,4 +23,4 @@ const ListItem = ({ Icon, label, spacing }: ListItemProps) => {
);
};

export default ListItem;
export default memo(ListItem);
3 changes: 2 additions & 1 deletion frontend/src/components/PerformanceCard/src/VolumeLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import './performanceInfo.css';
import { ReactComponent as UpArrow } from '@/assets/up-arrow.svg';
import { ReactComponent as DownArrow } from '@/assets/down-arrow.svg';
import ListItem from '@/components/ListItem';
import { showShortenedAmount } from '@/helpers/currency';

type TrendLabelProps = {
volume: number;
Expand All @@ -11,6 +12,6 @@ type TrendLabelProps = {

const VolumeLabel = ({ volume, change }: TrendLabelProps) => {
const arrow = change > 1 ? <UpArrow /> : <DownArrow />;
return <ListItem Icon={arrow} label={volume.toString()} />;
return <ListItem Icon={arrow} label={showShortenedAmount(volume)} />;
};
export default memo(VolumeLabel);
9 changes: 6 additions & 3 deletions frontend/src/components/PriceChart/PriceChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,31 @@ type PriceChartProps = {

const PriceChart = ({ symbolId }: PriceChartProps) => {
const dispatch = useAppDispatch();

useEffect(() => {
if (symbolId) {
dispatch(fetchPriceHistory(symbolId));
const promise = dispatch(fetchPriceHistory(symbolId));
return () => promise?.abort();
}
}, [dispatch, symbolId]);

const apiState = useAppSelector(selectors.apiState);
const data = useAppSelector(selectors.selectPriceHistory);
const symbolInfo = useAppSelector(selectors.selectSymbolInfo);

if (apiState.loading && symbolId !== null)
if (apiState.loading && symbolId !== null) {
return (
<div className="priceChart">
<Loading />
</div>
);
}
if (apiState.error) return <div className="priceChart">Failed to get price history!</div>;
if (!symbolId) return <div className="priceChart">Select stock</div>;
return (
<div className="priceChart">
<div>{symbolInfo}</div>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="98%" height="100%">
<LineChart data={data.map((e) => ({ ...e, time: new Date(e.time).toLocaleTimeString() }))}>
<Line type="monotone" dataKey="price" stroke="#8884d8" dot={false} />
<XAxis dataKey="time" />
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/PriceChart/priceChart.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
.priceChart {
display: flex;
flex-direction: column;
width: 400px;
height: 300px;
display: flex;
}

@media (max-width: 1023px) {
Expand Down
80 changes: 68 additions & 12 deletions frontend/src/components/SymbolCard/SymbolCard.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,84 @@
import './symbolCard.css';
import { ReactComponent as CompanyIcon } from '@/assets/company.svg';
import { useAppSelector } from '@/hooks/redux';
import ListItem from '@/components/ListItem';
import { memo, useEffect, useRef } from 'react';
import { selectShowCardInfo } from '@/store/dashboardOptionsSlice';
import useAddBoxShadow from '@/hooks/useAddBoxShadow';
import useAddEffect from '@/hooks/useAddEffect';
import SymbolCardBody from '@/components/SymbolCard/src/SymbolCardBody';
import SymbolCardHeader from '@/components/SymbolCard/src/SymbolCardHeader';

type SymbolCardProps = {
id: string;
onClick: (symbolId: string) => void;
price: number;
activeSymbol: string | null;
};

const SymbolCard = ({ id, onClick, price }: SymbolCardProps) => {
const { trend, companyName } = useAppSelector((state) => state.stocks.entities[id]);
const SymbolCard = memo(({ id, onClick, price, activeSymbol }: SymbolCardProps) => {
const ref = useRef<HTMLDivElement>(null);
const priceRef = useRef(price);
const { shadow, addShadow, setShadow } = useAddBoxShadow();
const { effect, addEffect, setEffect } = useAddEffect();
const { trend, companyName, industry, marketCap } = useAppSelector(
(state) => state.stocks.entities[id]
);
const showCardInfo = useAppSelector(selectShowCardInfo);

const handleOnClick = () => {
onClick(id);
};

useEffect(() => {
addShadow(price, priceRef.current);
}, [price]);

useEffect(() => {
addEffect(price, priceRef.current);
priceRef.current = price;
}, [activeSymbol, price]);

const getClassName = () => {
let className = 'symbolCard';

if (activeSymbol) {
if (activeSymbol === id) {
className = className + ` symbolCard--active`;
} else className = className + ` symbolCard--nonactive`;
}

if (effect) className = className + ` symbolCard--${effect}`;
if (shadow) className = className + ` symbolCard--${shadow}`;

return className;
};

const onAnimationEnd = () => {
if (ref.current) {
setShadow('');
setEffect('');
}
};

return (
<div onClick={handleOnClick} className="symbolCard">
<div>
{id} - {trend}
</div>
<div>Price:</div>
<div>{price || '--'} </div>
<ListItem Icon={<CompanyIcon />} label={companyName} />
<div
ref={ref}
data-price={price}
onClick={handleOnClick}
className={getClassName()}
onAnimationEnd={onAnimationEnd}
>
<SymbolCardHeader id={id} trend={trend} />
<SymbolCardBody
companyName={companyName}
price={price}
industry={industry}
marketCap={marketCap}
showCardInfo={showCardInfo}
/>
</div>
);
};
});

export default SymbolCard;

SymbolCard.displayName = 'SymbolCard';
21 changes: 21 additions & 0 deletions frontend/src/components/SymbolCard/src/SymbolCardBody.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import SymbolCardList from '@/components/SymbolCard/src/SymbolCardList';
import SymbolCardPrice from '@/components/SymbolCard/src/SymbolCardPrice';
import './symbolCardBody.css';
import { Stock } from '@/store/stocksSlice';
import { memo } from 'react';

export type SymbolCardBodyProps = {
price: number;
showCardInfo: boolean;
} & Pick<Stock, 'companyName' | 'industry' | 'marketCap'>;

const SymbolCardBody = ({ price, showCardInfo, ...rest }: SymbolCardBodyProps) => {
return (
<div className="symbolCard__body">
<SymbolCardPrice price={price} />
{showCardInfo && <SymbolCardList {...rest} />}
</div>
);
};

export default memo(SymbolCardBody);
25 changes: 25 additions & 0 deletions frontend/src/components/SymbolCard/src/SymbolCardHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import './symbolCardHeader.css';
import arrowDown from '@/assets/down.png';
import arrowUp from '@/assets/up.png';
import { memo } from 'react';

type SymbolCardHeaderProps = {
id: string;
trend: 'UP' | 'DOWN' | null;
};

const SymbolCardHeader = memo(({ id, trend }: SymbolCardHeaderProps) => {
return (
<header className="symbolCard__header">
{trend && (
<img
src={trend === 'UP' ? arrowUp : trend === 'DOWN' ? arrowDown : ''}
className="symbolCard__arrowIcon"
/>
)}
{id}
</header>
);
});

export default SymbolCardHeader;
36 changes: 36 additions & 0 deletions frontend/src/components/SymbolCard/src/SymbolCardList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ListItem from '@/components/ListItem';
import { SymbolCardBodyProps } from '@/components/SymbolCard/src/SymbolCardBody';
import { showShortenedAmount } from '@/helpers/currency';
import { ReactComponent as CompanyIcon } from '@/assets/company.svg';
import { ReactComponent as IndustryIcon } from '@/assets/industry.svg';
import { ReactComponent as MarketCapIcon } from '@/assets/market_cap.svg';
import { memo } from 'react';

type SymbolCardListProps = Omit<SymbolCardBodyProps, 'price' | 'showCardInfo'>;

const SymbolCardList = ({ companyName, industry, marketCap }: SymbolCardListProps) => {
const listItems = [
{
id: 'companyName',
label: companyName,
icon: CompanyIcon
},
{
id: 'industry',
label: industry,
icon: IndustryIcon
},
{
id: 'marketCap',
label: showShortenedAmount(marketCap),
icon: MarketCapIcon
}
];

return listItems.map(({ id, label, icon }) => {
const Icon = icon;
return <ListItem key={id} Icon={<Icon />} label={label} spacing="space-between" />;
});
};

export default memo(SymbolCardList);
17 changes: 17 additions & 0 deletions frontend/src/components/SymbolCard/src/SymbolCardPrice.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { addCurrency, getInteger } from '@/helpers/currency';
import './symbolCardPrice.css';

type SymbolCardPriceProps = {
price: number;
};

const SymbolCardPrice = ({ price }: SymbolCardPriceProps) => {
return (
<div className="symbolCard__price">
<span>Price:</span>
<b>{price ? addCurrency(getInteger(price)) : '--'} </b>
</div>
);
};

export default SymbolCardPrice;
4 changes: 4 additions & 0 deletions frontend/src/components/SymbolCard/src/symbolCardBody.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.symbolCard__body {
background-color: var(--colorWhite);
padding: 8px;
}
15 changes: 15 additions & 0 deletions frontend/src/components/SymbolCard/src/symbolCardHeader.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.symbolCard__header {
position: relative;
background-color: var(--colorDark);
color: var(--colorWhite);
font-size: 18px;
font-weight: 500;
padding: 0 8px;
}

.symbolCard__arrowIcon {
position: absolute;
top: -16px;
right: -16px;
width: 40px;
}
14 changes: 14 additions & 0 deletions frontend/src/components/SymbolCard/src/symbolCardPrice.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.symbolCard__price {
display: flex;
align-items: center;
justify-content: space-between;
}

.symbolCard__price span {
font-size: 12px;
text-transform: uppercase;
}

.symbolCard__price b {
font-size: 24px;
}
38 changes: 33 additions & 5 deletions frontend/src/components/SymbolCard/symbolCard.css
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
.symbolCard {
border: 2px solid black; /* just for visibility, feel free to remove it */
margin: 10px; /* just for visibility, feel free to remove it */
padding: 10px; /* just for visibility, feel free to remove it */
cursor: pointer;
transition: box-shadow 0.3s, scale 0.3s;
}

.symbolCard__shake {
animation: shake 0.62s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
.symbolCard--active {
box-shadow: 0 0 15px 0 var(--colorDark);
scale: 1.02;
}

.symbolCard--nonactive {
scale: 0.98;
}

.symbolCard--green {
animation: greenShadow 1s forwards;
}

.symbolCard--red {
animation: redShadow 1s forwards;
}

.symbolCard--shake {
animation: greenShadow 1s forwards, shake 0.62s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
}

@keyframes shake {
Expand Down Expand Up @@ -36,3 +52,15 @@
transform: translateX(6px);
}
}

@keyframes greenShadow {
20% {
box-shadow: 0 0 15px 0 green;
}
}

@keyframes redShadow {
20% {
box-shadow: 0 0 15px 0 red;
}
}
Loading