Stock Page (/stock/:ticker)
Since the Analysis Page or “Stock Page” is the one I actually designed in Figma and therefore the one I have the best idea of, I’ll start here.
Stock Page Route
Since I’m using Vite I don’t have access to Next.js’ convenient navigation function to handle routes so react-router-dom will have to suffice. I’ll add a new dynamic route /stock/:ticker
linked to the Stock Page element.
In the stock page I first have to get all ticker data from the backend. I use axios instead of fetch to send out requests because axios is superior to fetch in pretty much every way. It provides automatic JSON parsing, better error handling, interceptors, simplified syntax, upload/download progress, etc. The list goes on but those are the main reasons why axios is my go to option for sending HTTP requests in JavaScript, at least when It’s in a project and not just a simple script. One of the biggest advantages axios gives us in a full stack application like this one is allowing us to set a base URL which more often than not is a much cleaner approach then setting a vite proxy. This way I won’t have to provide the backend URL every time I make a request but can just do it once in the axiosinstance.js and then just use relative URLs.
When sending requests with axios I also use AbortController
. This is used to manage and cancel ongoing API requests if the component unmounts or the ticker changes before the request could be completed. I do this because when a component like StockPage
fetches data in a useEffect
, there’s a chance the user navigates away (unmounting the component) or changes the URL ticker (triggering a new request) before the previous request is completed.
Without AbortController
, the asynchronous request might complete and attempt to update state on an unmounted component.
After getting the data from the API I have to pass it on. I’ll pass the company object down as props to the StockInfo
component which will hold all of the info for the stock (in the Figma design that’s the upper part of the dashboard). Also in the design I want the description to be expandable. Since this is part of the StockPage
component and might affect spacing of other components returned by it it makes sense to lift the state up from StockInfo
. So I’ll pass down state and toggle function of isDescriptionExpanded
down too.
I’ll also want to render the list of Points below the StockInfo
. Since the two lists (bullish/bearish) should look and behave identically except for the green checks & red crosses I’ll just do one PointsList
Component and pass down if points are bullish or bearish to render them conditionally. To do that I’ll just check if the points sentiment score is greater or less than 50. To avoid re-doing this possibly expensive (depending on length of lists) operation on every render I’ll use useMemo
to cache the result of the filtering until the points change.
Lastly I’ll add a loader and error component to display loading and error states and the blue frame for the stock page is finished.
Let’s now get to the individual components.
StockInfo
Format data
First I have to format all the data I get passed down so I can display it nicely. I format market cap, earnings date, and analyst rating appropriately. The formatting is mostly basic regex and simple conditionals so I won’t go into too much detail here. I’ll also determine the color of the DCF, analyst rating, forward PE, and beta based on their value to give the values a bit more meaning. For example if the Analyst rating is “Strong Sell” or “Sell” that’s a bad sign and so it will be assigned the color red, “Hold” and it will be yellow, “Buy” or “Strong Buy” and it will be green.
I first engulf the entire component in a section with the standard bg-widget-background
(This is a common theme for pretty much the entire site as you can see in the design).
Top Section
I’ll then first do the company logo followed by the company ticker which, when clicked, will link to the website. I do a hover animation using standard transition-colors
from white to green to show the user he can click the ticker.
Next to the ticker I’ll put the price of the stock and below I’ll add the title.
I mentioned a favorites page and this is the first step of implementing it.
I added a star button next to the ticker price along with a simple animation for clicking it. When clicked it will first check if local storage already contains the ticker as a favorite. If it does not it will save the ticker to an array in local storage called favorites
. If the entry exists already it removes it. In both cases it naturally also updates state to render the star as either filled or just outline.
I’ll go into more detail regarding the saving of the favorites to Local Storage in [[#Favorites]].
Metrics
Now for the individual information points at the bottom. I created a separate MetricCard
component which takes value and styling in as props. I then split the components into two rows. I wrapped both of them into one self stretching div. I then wrapped each one in it’s own flex container, adding a bit of top margin to the bottom one.
Sentiment Score
For the tickers sentiment score component I want a half circle which engulfs the sentiment score. Both the score and the half circle should change colors from red to orange to green based on the score with the half circle acting as a bar to visualize the score.
I’ll take the score as a prop, then I validate that the score is in the expected format. Next I get the score color by setting it to red if it’s under 40, orange if it’s between 40 and 60 and green if it’s greater than 60.
Now I need to construct the half circle. I first tried to import the svg i generated with figma but that turned out to be a suboptimal approach since it didn’t work well when trying to make it responsive. Since I’m not an svg professional and this is pretty much as far as my design skills take me I let ChatGPT generate the svg for this. So it’s important to note that the dynamic svg was not generated by me. Lastly I just add 0% and 100% labels under the respective ends of the half circle.
Description
If the description is expanded I’ll simply display the entire description. If it’s not I’ll only display the first 500 characters followed by three dots. I’ll then append a “See More” or “See Less” button, again depending on if it’s expanded or not. As onClick
I can just set the passed in toggle function for the description.
PointsList
First I’ll set the icon to either the green check mark or the red cross depending on the type prop that’s passed down. Both icons are exposed in the public folder.
In the design file I stacked the points into two columns next to each other for both lists. Essentially first comes a point on the left then one on the right and the next point is left again but one line below. The easiest way to implement this is to actually split the points up in two columns and rendering them separately. For that I’ll use useMemo
again so I only perform this filtering more than one time if the points change. If the point’s index is even I’ll push it to the left side, if it’s odd I’ll place it on the right side. I’ll then render a container for the left and the right side column, mapping the point values to a PointItem
component each.
PointItem
PointItem
is the individual point item. This component specifically is very interesting because I’ll have to implement two pretty challenging features aside from just the rendering of the point.
- When clicked the Point should open a
PointPreview
. The point preview isn’t included in the Figma design File. I essentially want to open a preview that pops up below the point. Similar to the criticisms card (which in turn is in the Figma design). It should show basic data of the post the point is extracted from. Title, author, date of post and of course a link to the original post. This way the user can check exactly where the point came from. - Similarly it should display a red bubble next to the point which shows how many criticisms the point has. When clicked it should expand a criticism card displaying all criticisms of the post, their validity score and a link to the comment the criticism was extracted from.
Since it’s now relevant to keep track of which of the list items is currently selected to be expanded so it doesn’t expand multiple at once, I went back and added a selectedPointIndex
state to the PointsList
component. Additionally I added a handlePointClick
function which, given an index, sets the selectedPointIndex
state to be the passed index. I then passed the index of the point, a unique key for the point, the isSelected
boolean and the handlePointClick
function all down as props to the PointItem
components.
The unique key is derived by adding up the original index with the points post URL. Unique keys with dynamic list items like these are important in React because they allow it to identify each item in the list which in turn enables it to track identity between renders. This way React can track, update and re-render only the components that changed instead of resetting everything.
The isSelected
boolean simply checks if the selectedPointIndex
is equal to given points index.
The idea of passing handlePointCLick
down is that PointItem
will have a way of notifying it’s parent component to toggle the selection state for the given item’s index.
I’ll also use handlePointClick
for the criticism list expansion. Since bot the post preview and the criticism list will be in the exact same place I’ll have to make sure they aren’t both expanded at the same time for the same point. I can do this by simply setting onPointClick
to and index of -1 (no index selected) if the criticism list is opened which will close any open post previews.
When I first implemented this I had an issue. Whenever I was clicking the criticism bubble to open the criticisms the point preview would open. I remembered that I had a similar issue some time ago on another project. After some googling I found out this was because I had a button in a button. The criticism bubble button is inside the bigger button to toggle the post preview. In JavaScript when an event (like click
) happens the event “bubbles up” through it’s parent elements. Now I don’t technically have a button in a button. To be exact it’s a button in a div. But the div is clickable. Meaning the click
event bubbles up to the div and triggers it’s onClick
and opening the post preview. I fixed the issue by putting event.stopPropagation()
at the stat of the handleCriticismToggle
function.
For the criticism button styling I set it to be a filled dark circle with an orange number when not expanded and an orange filled circle with a dark number when expanded.
Lastly also added some accessibility features to the divs/buttons like an onKeyDown
handler and tabIndex
.
CriticismList
Both the CriticismList
and the PointPreview
are of course rendered conditionally depending on if they’re expanded or not.
For the criticism list I’ll display an orange Warning icon followed by a “Criticism:” header (also in orange). Each criticism will consist of the criticism itself followed by a link icon to indicate that the comment of the criticism is embedded as an anchor tag into the criticism text meaning the user can click it to open the comment in a new tab. Then it’ll display the validity score below. I wanted the validity score to also be colored depending on it’s value so if it’s larger or equal to 75 it will be green, if it’s between 50 and 75 it will be yellow and if it’s under 50 it will be red.
PointPreview
I want to give the user the possibility of checking where Points came from and validating the source for themselves. I could just provide a link to the post however I want to add an embed that pops up when clicking on the Point which displays some key information about the post and of course includes a link.
At the top of this expendable card I’ll render the title of the reddit post as a header. Below it I’ll put the source of the post (reddit/seekingalpha), the author, and the formatted date on which the post was posted. Below all of this I’ll then put an anchor tag to the original post.
Animation
Now CriticismList
and PointPreview
are done. However right now when the user clicks on either the div or the criticism button the card just appears and everything moves down. This looks everything but smooth so I wanted to add an animation to make the card slowly open up vertically.
What I wanted to do here was a bit too complex for traditional CSS transitions so I had to use framer-motion
’s AnimatePresence
. Normally when a component gets removed from DOM (in this case setting isCriticismExpanded
to false) React just removes it. AnimatePresence
waits for the exit animation before removing it.
To design the animation, AnimatePresence
, or more specifically motion.div
, (The element that makes a regular div animatable) requires some arguments. I set initial
s (the initial state of the div) opacity, margin and height to 0 (since it has to expand), animate
’s height to auto, opacity to 1 and marginTop to 0.5rem to display it and exit
’s values to 0, 0, 0 to hide it again when collapsing. Lastly I applied overflow:hidden to make sure the content of the container doesn’t spill out during the animation and applied the div to CriticismList
and PointPreview
.