• Home
  • About
    • Hanna's Blog photo

      Hanna's Blog

      I wanna be a global developer.

    • Learn More
    • Email
    • LinkedIn
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

[C#, React] Finshark

18 Oct 2024

Reading time ~104 minutes

React Install

  • create FinShark folder

Required

  • npm
  • download node.js
  • restart your computer
  • run npm on visual studio code
React Install
  • React
  • run npm install create-react-app to create react app
React Install
  • run npx create-react-app frontend --template typescript to create react frontend
React Install
  • open extensions in visual studio code
  • download ES7+ React/Redux/React-Native snippets to use react snippets
React Install

JSX(JavaScript XML)

  • a syntax extension for JavaScript primatily used in React.
  • allows to write HTML-like code directly within JavaScript.

Image

  • create assets folder in src folder
  • create image folder in assets folder
  • download any iphone image and save it in image folder
  • name it to iphone.jpg
JSX

Card

  • create components folder in src folder
  • create Card folder in components
  • create Card.tsx and Card.css files in card folder
JSX
  • Card.tsx
  • type tsrafce to use snippets
import iphone from '../../assets/images/iphone.jpg'
import './Card.css'

type Props = {}

const Card = (props: Props) => {
  return (
    <div className='card'>
        <img 
            src={iphone}
            alt='image'
        />
        <div className='details'>
            <h2>AAPL</h2>
            <p>$110</p>
        </div>
        <p className='info'>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sequi, ex.</p>
    </div>
  )
}

export default Card

App

  • App.tsx and App.css are already in src folder

  • App.tsx

import './App.css';
import Card from './components/Card/Card';

function App() {
  return (
    <div className="App">
      <Card/>
    </div>
  );
}

export default App;
  • App.css
.card {
    width: 350px;
    height: 450px;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 50px;
    border-radius: 10px;
    overflow: hidden;
    box-shadow: rgba(50, 50, 93, 0.25) 0px 6px 12px -2px,
      rgba(0, 0, 0, 0.3) 0px 3px 7px -3px;
  }
  
  .card img {
    width: 180px;
    height: 180px;
    border-radius: 50%;
    margin: 5px;
  }
  
  .card .details {
    margin: 10px;
  }
  
  .card .details h2 {
    font-weight: 600;
  }
  
  .card .details p {
    text-transform: uppercase;
    font-weight: 300;
  }

Run

  • type cd frontend in terminal
  • type npm run start
JSX

Props

  • means properties.
  • are used to pass data from one component to another; Data Flow.
  • are read-only.

Interface vs Type

  • In TypeScript, both interface and type are used to define the structure of objects

  • interface
  • Extensibility: interfaces can be extended using the extends keyword to create more complex types.
  • Declaration merging: If there is more than two interfaces with same name, TypeScript will merge them together.

  • type
  • Cannot extend: type cannot be extended.
  • No delaration merging: If there is more than two types with same name, it occurs error.

CardList

  • create CardList folder in components folder
  • create CardList.tsx and CardList.css files in CardList folder

  • CardList.tsx
import Card from '../Card/Card'

interface Props {}

const CardList = (props: Props) => {
  return (
    <div>
        <Card companyName='Apple' ticker='APPL' price={100}/>
        <Card companyName='Microsoft' ticker='MSFT' price={200}/>
        <Card companyName='Tesla' ticker='TSLA' price={300}/>
    </div>
  )
}

export default CardList
  • Card.tsx
import iphone from '../../assets/images/iphone.jpg'

interface Props {
  companyName: string;
  ticker: string;
  price: number;
}

const Card = ({ companyName, ticker, price }: Props) => {
  return (
    <div className='card'>
        <img 
            src={iphone}
            alt='image'
        />
        <div className='details'>
            <h2>{companyName} ({ticker})</h2>
            <p>${price}</p>
        </div>
        <p className='info'>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sequi, ex.</p>
    </div>
  )
}

export default Card

App

  • App.tsx
import './App.css';
import CardList from './components/CardList/CardList';

function App() {
  return (
    <div className="App">
      <CardList/>
    </div>
  );
}

export default App;
Props

Function Components

React.FC

  • React Function Component
  • Automatic typing of childern: React.FC component can implicitly accept children.
  • Prop types enforcement: React.FC ensures that props passed to the component match the specified interface.

JSX.Element

  • JSX.Element is the standard return type for functional components in React that use JSX to render UI elements
  • JSX.Element is essentially the type definition for a React element, such as an HTML tag, a React component, or a fragment

Card

  • Card.tsx
import iphone from '../../assets/images/iphone.jpg'

interface Props {
  companyName: string;
  ticker: string;
  price: number;
}

const Card: React.FC<Props> = ({ companyName, ticker, price }: Props): JSX.Element => {
  return (
    <div className='card'>
        <img 
            src={iphone}
            alt='image'
        />
        <div className='details'>
            <h2>{companyName} ({ticker})</h2>
            <p>${price}</p>
        </div>
        <p className='info'>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sequi, ex.</p>
    </div>
  )
}

export default Card

CardList

  • CardList.tsx
import Card from '../Card/Card'

interface Props {}

const CardList: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <div>
        <Card companyName='Apple' ticker='APPL' price={100}/>
        <Card companyName='Microsoft' ticker='MSFT' price={200}/>
        <Card companyName='Tesla' ticker='TSLA' price={300}/>
    </div>
  )
}

export default CardList

Hook

  • React hooks are special function that enable stateful logic and side-effects in functional components.
  • Hooks allow to use features like state and lifecycle events without converting the component to a class component.

useState

  • a function that allows to update the state value: initialization and updating.

Search

  • create Search folder in components folter
  • create Search.tsx and Search.css in search folder

  • Search.tsx
import React, { useState } from 'react'

type Props = {}

const Search : React.FC<Props> = (props: Props): JSX.Element => {
  const [search, setSearch] = useState<string>("");
  const onClick = (e: any) => {
    setSearch(e.target.value);
    console.log(e);
  };

  return (
    <div>
        <input value={search} onChange={(e) => onClick(e)}/>
    </div>
  )
}

export default Search

App

  • App.tsx
import './App.css';
import CardList from './components/CardList/CardList';
import Search from './components/Search/Search';

function App() {
  return (
    <div className="App">
      <Search/>
      <CardList/>
    </div>
  );
}

export default App;
Hook

Event Handler

ChangeEvent

  • ChangeEvent helps strongly type the event object for form elements such as <input>, <textarea>, or <select>.
  • ChangeEvent ensures that TypeScript can provice autocomplete and type safety when accessing event properties like event.target.value.

SyntheticEvent

  • Different browsers handle slightly diffrently, but SyntheticEvent abstracts those differences. React can be worked with a consistent event object across all browers.
  • React pools event objects to improve performance. Instead of creating a new event object for every event, React reuses them, reducing memory consumption.

Search

  • Search.tsx
import React, { ChangeEvent, useState, SyntheticEvent } from 'react'

type Props = {}

const Search : React.FC<Props> = (props: Props): JSX.Element => {
  const [search, setSearch] = useState<string>("");
  const handlechange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };
  const onClick = (e: SyntheticEvent) => {
    console.log(e);
  };

  return (
    <div>
        <input value={search} onChange={(e) => handlechange(e)}/>
        <button onClick={(e) => onClick(e)}/>
    </div>
  )
}

export default Search
Event Handler

Modeling

axios

  • returns promises, making it easy to work with asynchronous code using .then() and .catch(), or with async/await.
  • can make requests using get, post, put and delete.
  • provides a straightforward mechanism to handle errors, such as failed requests or responses with error status codes.

dotenv

  • allows to define environment variables in a .env file and load them into process.env, making them accessible within your Node.js application.
  • keeps sensitive information, such as API keys or database passwords, out of your codebase.

Required

  • type npm install axios --save in terminal
  • type npm install --save-dev @types/axios in terminal
  • type npm install dotenv --save in terminal

Environment

  • open financial modeling prep to get your api key.

  • .env
  • create .env file on frontend folder
  • open .env file and type REACT_APP_API_KEY= with your api key.

Company Model

  • create company.d.tsx file on src folder

  • company.d.tsx

export interface CompanySearch {
    currency: string;
    exchangeShortName: string;
    name: string;
    stockExchange: string;
    symbol: string;
  }
  
  export interface CompanyProfile {
    symbol: string;
    price: number;
    beta: number;
    volAvg: number;
    mktCap: number;
    lastDiv: number;
    range: string;
    changes: number;
    companyName: string;
    currency: string;
    cik: string;
    isin: string;
    exchange: string;
    exchangeShortName: string;
    industry: string;
    website: string;
    description: string;
    ceo: string;
    sector: string;
    counter: string;
    fullTimeEmployees: string;
    phone: string;
    address: string;
    city: string;
    state: string;
    zip: string;
    dcfDiff: number;
    dcf: number;
    image: string;
    ipoDate: string;
    defaultImage: boolean;
    isEtf: boolean;
    isActivelyTrading: boolean;
    isAdr: boolean;
    isFund: boolean;
  }
  
  export interface CompanyKeyRatios {
    dividendYielTTM: number;
    dividendYielPercentageTTM: number;
    peRatioTTM: number;
    pegRatioTTM: number;
    payoutRatioTTM: number;
    currentRatioTTM: number;
    quickRatioTTM: number;
    cashRatioTTM: number;
    daysOfSalesOutstandingTTM: number;
    daysOfInventoryOutstandingTTM: number;
    operatingCycleTTM: number;
    daysOfPayablesOutstandingTTM: number;
    cashConversionCycleTTM: number;
    grossProfitMarginTTM: number;
    operatingProfitMarginTTM: number;
    pretaxProfitMarginTTM: number;
    netProfitMarginTTM: number;
    effectiveTaxRateTTM: number;
    returnOnAssetsTTM: number;
    returnOnEquityTTM: number;
    returnOnCapitalEmployedTTM: number;
    netIncomePerEBTTTM: number;
    ebtPerEbitTTM: number;
    ebitPerRevenueTTM: number;
    debtRatioTTM: number;
    debtEquityRatioTTM: number;
    longTermDebtToCapitalizationTTM: number;
    totalDebtToCapitalizationTTM: number;
    interestCoverageTTM: number;
    cashFlowToDebtRatioTTM: number;
    companyEquityMultiplierTTM: number;
    receivablesTurnoverTTM: number;
    payablesTurnoverTTM: number;
    inventoryTurnoverTTM: number;
    fixedAssetTurnoverTTM: number;
    assetTurnoverTTM: number;
    operatingCashFlowPerShareTTM: number;
    freeCashFlowPerShareTTM: number;
    cashPerShareTTM: number;
    operatingCashFlowSalesRatioTTM: number;
    freeCashFlowOperatingCashFlowRatioTTM: number;
    cashFlowCoverageRatiosTTM: number;
    shortTermCoverageRatiosTTM: number;
    capitalExpenditureCoverageRatioTTM: number;
    dividendPaidAndCapexCoverageRatioTTM: number;
    priceBookValueRatioTTM: number;
    priceToBookRatioTTM: number;
    priceToSalesRatioTTM: number;
    priceEarningsRatioTTM: number;
    priceToFreeCashFlowsRatioTTM: number;
    priceToOperatingCashFlowsRatioTTM: number;
    priceCashFlowRatioTTM: number;
    priceEarningsToGrowthRatioTTM: number;
    priceSalesRatioTTM: number;
    dividendYieldTTM: number;
    enterpriseValueMultipleTTM: number;
    priceFairValueTTM: number;
    dividendPerShareTTM: number;
  }
  
  export interface CompanyIncomeStatement {
    date: string;
    symbol: string;
    reportedCurrency: string;
    cik: string;
    fillingDate: string;
    acceptedDate: string;
    calendarYear: string;
    period: string;
    revenue: number;
    costOfRevenue: number;
    grossProfit: number;
    grossProfitRatio: number;
    researchAndDevelopmentExpenses: number;
    generalAndAdministrativeExpenses: number;
    sellingAndMarketingExpenses: number;
    sellingGeneralAndAdministrativeExpenses: number;
    otherExpenses: number;
    operatingExpenses: number;
    costAndExpenses: number;
    interestIncome: number;
    interestExpense: number;
    depreciationAndAmortization: number;
    ebitda: number;
    ebitdaratio: number;
    operatingIncome: number;
    operatingIncomeRatio: number;
    totalOtherIncomeExpensesNet: number;
    incomeBeforeTax: number;
    incomeBeforeTaxRatio: number;
    incomeTaxExpense: number;
    netIncome: number;
    netIncomeRatio: number;
    eps: number;
    epsdiluted: number;
    weightedAverageShsOut: number;
    weightedAverageShsOutDil: number;
    link: string;
    finalLink: string;
  }
  
  export interface CompanyBalanceSheet {
    date: string;
    symbol: string;
    reportedCurrency: string;
    cik: string;
    fillingDate: string;
    acceptedDate: string;
    calendarYear: string;
    period: string;
    cashAndCashEquivalents: number;
    shortTermInvestments: number;
    cashAndShortTermInvestments: number;
    netReceivables: number;
    inventory: number;
    otherCurrentAssets: number;
    totalCurrentAssets: number;
    propertyPlantEquipmentNet: number;
    goodwill: number;
    intangibleAssets: number;
    goodwillAndIntangibleAssets: number;
    longTermInvestments: number;
    taxAssets: number;
    otherNonCurrentAssets: number;
    totalNonCurrentAssets: number;
    otherAssets: number;
    totalAssets: number;
    accountPayables: number;
    shortTermDebt: number;
    taxPayables: number;
    deferredRevenue: number;
    otherCurrentLiabilities: number;
    totalCurrentLiabilities: number;
    longTermDebt: number;
    deferredRevenueNonCurrent: number;
    deferredTaxLiabilitiesNonCurrent: number;
    otherNonCurrentLiabilities: number;
    totalNonCurrentLiabilities: number;
    otherLiabilities: number;
    capitalLeaseObligations: number;
    totalLiabilities: number;
    preferredStock: number;
    commonStock: number;
    retainedEarnings: number;
    accumulatedOtherComprehensiveIncomeLoss: number;
    othertotalStockholdersEquity: number;
    totalStockholdersEquity: number;
    totalEquity: number;
    totalLiabilitiesAndStockholdersEquity: number;
    minorityInterest: number;
    totalLiabilitiesAndTotalEquity: number;
    totalInvestments: number;
    totalDebt: number;
    netDebt: number;
    link: string;
    finalLink: string;
  }
  
  export interface CompanyCashFlow {
    date: string;
    symbol: string;
    reportedCurrency: string;
    cik: string;
    fillingDate: string;
    acceptedDate: string;
    calendarYear: string;
    period: string;
    netIncome: number;
    depreciationAndAmortization: number;
    deferredIncomeTax: number;
    stockBasedCompensation: number;
    changeInWorkingCapital: number;
    accountsReceivables: number;
    inventory: number;
    accountsPayables: number;
    otherWorkingCapital: number;
    otherNonCashItems: number;
    netCashProvidedByOperatingActivities: number;
    investmentsInPropertyPlantAndEquipment: number;
    acquisitionsNet: number;
    purchasesOfInvestments: number;
    salesMaturitiesOfInvestments: number;
    otherInvestingActivites: number;
    netCashUsedForInvestingActivites: number;
    debtRepayment: number;
    commonStockIssued: number;
    commonStockRepurchased: number;
    dividendsPaid: number;
    otherFinancingActivites: number;
    netCashUsedProvidedByFinancingActivities: number;
    effectOfForexChangesOnCash: number;
    netChangeInCash: number;
    cashAtEndOfPeriod: number;
    cashAtBeginningOfPeriod: number;
    operatingCashFlow: number;
    capitalExpenditure: number;
    freeCashFlow: number;
    link: string;
    finalLink: string;
  }

API

  • create api.tsx file on src folder

  • api.tsx

import axios from "axios";
import { CompanySearch } from "./company";

interface SearchResponse {
  data: CompanySearch[];
}

export const searchCompanies = async (query: string) => {
  try {
    const data = await axios.get<SearchResponse>(
      `https://financialmodelingprep.com/api/v3/search?query=${query}&apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.log("error message: ", error.message);
      return error.message;
    } else {
      console.log("unexpected error: ", error);
      return "Unxpected error has occured."
    }
  }
}

index

  • index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { searchCompanies } from './api';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

console.log(searchCompanies("tsla"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
Modeling

Data Flow

  • Props

App

  • App.tsx
import { ChangeEvent, SyntheticEvent, useState } from 'react';
import './App.css';
import CardList from './components/CardList/CardList';
import Search from './components/Search/Search';

function App() {
  const [search, setSearch] = useState<string>("");
  const handlechange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };
  const onClick = (e: SyntheticEvent) => {
    console.log(e);
  };

  return (
    <div className="App">
      <Search onClick={onClick} search={search} handleChange={handlechange}/>
      <CardList/>
    </div>
  );
}

export default App;

Search

  • Search.tsx
import React, { ChangeEvent, FormEvent, SyntheticEvent } from 'react'

interface Props {
  onClick: (e: SyntheticEvent) => void;
  search: string | undefined;
  handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
}

const Search : React.FC<Props> = ({ onClick, search, handleChange }: Props): JSX.Element => {
  return (
    <div>
      <input value={search} onChange={(e) => handleChange(e)}/>
      <button onClick={(e) => onClick(e)}/>
    </div>
  )
}

export default Search

Type Narrowing

  • for type checking with if-else.
  • when type is different with the expectation, it occurs error.

App

  • App.tsx
import { ChangeEvent, SyntheticEvent, useState } from 'react';
import './App.css';
import CardList from './components/CardList/CardList';
import Search from './components/Search/Search';
import { CompanySearch } from './company';
import { searchCompanies } from './api';

function App() {
  const [search, setSearch] = useState<string>("");
  const [searchResult, setSearchResult] = useState<CompanySearch[]>([]);
  const [serverError, setServerError] = useState<string>("");

  const handlechange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };
  const onClick = async (e: SyntheticEvent) => {
    const result = await searchCompanies(search);
    if (typeof result === "string") {
      setServerError(result);
    } else if (Array.isArray(result.data)){
      setSearchResult(result.data);
    }
    console.log(searchResult);
  };

  return (
    <div className="App">
      <Search onClick={onClick} search={search} handleChange={handlechange}/>
      <CardList/>
    </div>
  );
}

export default App;
Type Narrowing

Conditional Rendering

  • A === B ? C : D renders C when it is true, D when it is false.
  • A === B ?? C renders C when it is true

App

  • App.tsx
...
  return (
    <div className="App">
      <Search onClick={onClick} search={search} handleChange={handlechange}/>
      {serverError && <h1>{serverError}</h1>}
      <CardList/>
    </div>
  );
}

export default App;

map()

  • does not modify the original array.
  • creates a new array based on the transformation applied to each element.
  • is often used for iterating over arrays to transform or process each item.

uuid(Universally Unique Identifier)

  • is typically represented as a 32-character hexadecimal string, separated by hyphens into five groups.(8-4-4-4-12)

Required

  • type npm install uuid in terminal
  • type npm install --save-dev @types/uuid in terminal

App

  • App.tsx
...
return (
    <div className="App">
      <Search onClick={onClick} search={search} handleChange={handlechange}/>
      <CardList searchResults={searchResult}/>
      {serverError && <h1>{serverError}</h1>}
    </div>
  );

CardList

import { CompanySearch } from '../../company'
import Card from '../Card/Card'
import { v4 as uuidv4 } from "uuid";

interface Props {
  searchResults: CompanySearch[];
}

const CardList: React.FC<Props> = ( { searchResults }: Props): JSX.Element => {
  return (
    <>
      {searchResults.length > 0 ? (
        searchResults.map((result) => {
          return <Card id={result.symbol} key={uuidv4()} searchResult={result}/>
        })
      ) : (<h1>No results</h1>)}
    </>
  )
}

export default CardList

Card

import { CompanySearch } from '../../company';

interface Props {
  id: string;
  searchResult: CompanySearch;
}

const Card: React.FC<Props> = ({ id, searchResult }: Props): JSX.Element => {
  return (
    <div className='card'>
      <img alt='company logo'/>
      <div className='details'>
        <h2>{searchResult.name} ({searchResult.symbol})</h2>
        <p>{searchResult.currency}</p>
      </div>
      <p className='info'>{searchResult.exchangeShortName} - {searchResult.stockExchange}</p>
    </div>
  )
}

export default Card
Lists

Forms

Portfolio

  • create Portfolio folder in components folder
  • create AddPortfolio folder in Portfolio folder
  • create AddPortfolio.tsx and AddPortfolio.css in AddPortfolio folder

  • AddPortfolio.tsx
import React, { SyntheticEvent } from 'react'

interface Props {
  onPortfolioCreate: (e: SyntheticEvent) => void;
  symbol: string;
}

const AddPortfolio: React.FC<Props> = ({ onPortfolioCreate, symbol }: Props): JSX.Element => {
  return (
    <form onSubmit={onPortfolioCreate}>
      <input readOnly={true} hidden={true} value={symbol}/>
      <button type='submit'>Add</button>
    </form>
  )
}

export default AddPortfolio

Card

  • Card.tsx
import { SyntheticEvent } from 'react';
import { CompanySearch } from '../../company';
import AddPortfolio from '../Portfolio/AddPortfolio/AddPortfolio';

interface Props {
  id: string;
  searchResult: CompanySearch;
  onPortfolioCreate: (e: SyntheticEvent) => void;
}

const Card: React.FC<Props> = ({ id, searchResult, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <div className='card'>
      <img alt='company logo'/>
      <div className='details'>
        <h2>{searchResult.name} ({searchResult.symbol})</h2>
        <p>{searchResult.currency}</p>
      </div>
      <p className='info'>{searchResult.exchangeShortName} - {searchResult.stockExchange}</p>
      <AddPortfolio onPortfolioCreate={onPortfolioCreate} symbol={searchResult.symbol}/>
    </div>
  )
}

export default Card

CardList

  • CardList.tsx
import { SyntheticEvent } from 'react';
import { CompanySearch } from '../../company'
import Card from '../Card/Card'
import { v4 as uuidv4 } from "uuid";

interface Props {
  searchResults: CompanySearch[];
  onPortfolioCreate: (e: SyntheticEvent) => void;
}

const CardList: React.FC<Props> = ( { searchResults, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <>
      {searchResults.length > 0 ? (
        searchResults.map((result) => {
          return <Card id={result.symbol} key={uuidv4()} searchResult={result} onPortfolioCreate={onPortfolioCreate}/>
        })
      ) : (<h1>No results</h1>)}
    </>
  )
}

export default CardList

App

  • App.tsx
import { ChangeEvent, SyntheticEvent, useState } from 'react';
import './App.css';
import CardList from './components/CardList/CardList';
import Search from './components/Search/Search';
import { CompanySearch } from './company';
import { searchCompanies } from './api';

function App() {
  const [search, setSearch] = useState<string>("");
  const [searchResult, setSearchResult] = useState<CompanySearch[]>([]);
  const [serverError, setServerError] = useState<string>("");

  const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };
  const onSearchClick = async (e: SyntheticEvent) => {
    e.preventDefault();

    const result = await searchCompanies(search);
    if (typeof result === "string") {
      setServerError(result);
    } else if (Array.isArray(result.data)){
      setSearchResult(result.data);
    }
    console.log(searchResult);
  };
  const onPortfolioCreate = (e: SyntheticEvent) => {
    e.preventDefault();
    console.log(e);
  }

  return (
    <div className="App">
      <Search onSearchClick={onSearchClick} search={search} handleSearchChange={handleSearchChange}/>
      <CardList searchResults={searchResult} onPortfolioCreate={onPortfolioCreate}/>
      {serverError && <h1>{serverError}</h1>}
    </div>
  );
}

export default App;

Search

  • Search.tsx
import React, { ChangeEvent, FormEvent, SyntheticEvent } from 'react'

interface Props {
  onSearchClick: (e: SyntheticEvent) => void;
  search: string | undefined;
  handleSearchChange: (e: ChangeEvent<HTMLInputElement>) => void;
}

const Search : React.FC<Props> = ({ onSearchClick, search, handleSearchChange }: Props): JSX.Element => {
  return (
    <>
      <form onSubmit={onSearchClick}>
        <input value={search} onChange={handleSearchChange}/>
        <button type='submit'/>
      </form>
    </>
  )
}

export default Search
Forms

Arrays

preventDefault()

  • stops form submission to handle it with JavaScript(e.g. forms, links and etc)
  • is a method used to prevent the default behavior of an event

…

  • creates a copy without modifying the original
  • combines multiple arrays into a single array
  • adds new elements to an array at the beginning, middle, or end
  • passes array elements as individual arguments to functions
  • captures the first elements or the rest of an array using destructuring.

CardPortfolio

  • create CardPortfolio in Portfoilo folder
  • create CardPortfolio.tsx and CardPortfolio.css in CardPortfolio folder

  • CardPortfolio.tsx
import React from 'react'

interface Props {
  portfolioValues: string;
}

const CardPortfolio: React.FC<Props> = ({ portfolioValues }: Props): JSX.Element => {
  return (
    <div>
      <h4>{portfolioValues}</h4>
      <button>X</button>
    </div>
  )
}

export default CardPortfolio

ListPortfolio

  • ListPortfolio.tsx
import React from 'react'
import CardPortfolio from '../CardPortfolio/CardPortfolio';

interface Props {
  portfolioValues: string[];
}

const ListPortfolio: React.FC<Props> = ({ portfolioValues }: Props): JSX.Element => {
  return (
    <>
      <h3>My Portfolio</h3>
      <ul>
        {portfolioValues && 
          portfolioValues.map((portfolioValues) => {return <CardPortfolio portfolioValues={portfolioValues}/>;})
        }
      </ul>
    </>
  )
}

export default ListPortfolio

App

  • App.tsx
...
  const onPortfolioCreate = (e: any) => {
    e.preventDefault();
    
    const exists = portfolioValues.find((value) => value === e.target[0].value);
    if (exists) return;
    
    const updatedPortfolio = [...portfolioValues, e.target[0].value]
    setPortfolioValues(updatedPortfolio);
  }

  return (
    <div className="App">
      <Search onSearchClick={onSearchClick} search={search} handleSearchChange={handleSearchChange}/>
      <ListPortfolio portfolioValues={portfolioValues}/>
      <CardList searchResults={searchResult} onPortfolioCreate={onPortfolioCreate}/>
      {serverError && <h1>{serverError}</h1>}
    </div>
  );
Arrays

Delete

App

  • App.tsx
...
  const onPortfolioDelete = (e: any) => {
    e.preventDefault();
    
    const removed = portfolioValues.filter((value) => {
      return value !== e.target[0].value;
    });
    setPortfolioValues(removed);
  }

  return (
    <div className="App">
      <Search onSearchClick={onSearchClick} search={search} handleSearchChange={handleSearchChange}/>
      <ListPortfolio portfolioValues={portfolioValues} onPortfolioDelete={onPortfolioDelete}/>
      <CardList searchResults={searchResult} onPortfolioCreate={onPortfolioCreate}/>
      {serverError && <h1>{serverError}</h1>}
    </div>
  );

ListPortfolio

  • ListPortfolio.tsx
import React, { SyntheticEvent } from 'react'
import CardPortfolio from '../CardPortfolio/CardPortfolio';

interface Props {
  portfolioValues: string[];
  onPortfolioDelete: (e: SyntheticEvent) => void;
}

const ListPortfolio: React.FC<Props> = ({ portfolioValues, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <>
      <h3>My Portfolio</h3>
      <ul>
        {portfolioValues && 
          portfolioValues.map((portfolioValue) => {return <CardPortfolio portfolioValue={portfolioValue} onPortfolioDelete={onPortfolioDelete}/>;})
        }
      </ul>
    </>
  )
}

export default ListPortfolio

CardPortfolio

  • CardPortfolio.tsx
import React, { SyntheticEvent } from 'react'
import DeletePortfolio from '../DeletePortfolio/DeletePortfolio';

interface Props {
  portfolioValue: string;
  onPortfolioDelete: (e: SyntheticEvent) => void;
}

const CardPortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div>
      <h4>{portfolioValue}</h4>
      <DeletePortfolio portfolioValue={portfolioValue} onPortfolioDelete={onPortfolioDelete} />
    </div>
  )
}

export default CardPortfolio

DeletePortfolio

  • create DeletePortfolio folder in Portfoilo folder
  • create DeletePortfolio.tsx and DeletePortfolio.css in DeletePortfolio folder

  • DeletePortfolio.tsx
import React, { SyntheticEvent } from 'react'

interface Props {
  portfolioValue: string;
  onPortfolioDelete: (e: SyntheticEvent) => void;
}

const DeletePortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div>
      <form onSubmit={onPortfolioDelete}>
        <input hidden={true} value={portfolioValue} />
        <button>X</button>
      </form>
      
    </div>
  )
}

export default DeletePortfolio
Delete

Tailwind

  • type npm install -D tailwindcss in terminal
  • type npx tailwindcss init in terminal
  • check document in tailwindcss

  • tailwind.config.js
module.exports = {
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  theme: {
    screens: {
      sm: "480px",
      md: "768px",
      lg: "1020px",
      xl: "1440px",
    },
    extend: {
      colors: {
        lightBlue: "hsl(215.02, 98.39%, 51.18%)",
        darkBlue: "hsl(213.86, 58.82%, 46.67%)",
        lightGreen: "hsl(156.62, 73.33%, 58.82%)",
      },
      fontFamily: {
        sans: ["Poppins", "sans-serif"],
      },
      spacing: {
        180: "32rem",
      },
    },
  },
  plugins: [],
};
  • index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Poppins&family=Rubik&display=swap");

Title

  • open public folder in frontend folder
  • open index.html and change <title>React App</title> to <title>FinShark</title>

images

  • add hero.png and logo.png in images folder

App

  • App.tsx
...
  return (
    <>
      <Navbar />
      <Search
        onSearchSubmit={onSearchSubmit}
        search={search}
        handleSearchChange={handleSearchChange}
      />
      <ListPortfolio
        portfolioValues={portfolioValues}
        onPortfolioDelete={onPortfolioDelete}
      />
      <CardList
        searchResults={searchResult}
        onPortfolioCreate={onPortfolioCreate}
      />

      {serverError && <div>Unable to connect to API</div>}
    </>
  );

Search

  • Search.tsx
...
const Search : React.FC<Props> = ({ onSearchSubmit, search, handleSearchChange }: Props): JSX.Element => {
  return (
    <section className="relative bg-gray-100">
      <div className="max-w-4xl mx-auto p-6 space-y-6">
        <form
          className="form relative flex flex-col w-full p-10 space-y-4 bg-darkBlue rounded-lg md:flex-row md:space-y-0 md:space-x-3"
          onSubmit={onSearchSubmit}
        >
          <input
            className="flex-1 p-3 border-2 rounded-lg placeholder-black focus:outline-none"
            id="search-input"
            placeholder="Search companies"
            value={search}
            onChange={handleSearchChange}
          ></input>
        </form>
      </div>
    </section>
  )
}

ListPortfolio

  • ListPortfolio.tsx
...
const ListPortfolio: React.FC<Props> = ({ portfolioValues, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <section id="portfolio">
      <h2 className="mb-3 mt-3 text-3xl font-semibold text-center md:text-4xl">
        My Portfolio
      </h2>
      <div className="relative flex flex-col items-center max-w-5xl mx-auto space-y-10 px-10 mb-5 md:px-6 md:space-y-0 md:space-x-7 md:flex-row">
        <>
          {portfolioValues.length > 0 ? (
            portfolioValues.map((portfolioValue) => {
              return (
                <CardPortfolio
                  portfolioValue={portfolioValue}
                  onPortfolioDelete={onPortfolioDelete}
                />
              );
            })
          ) : (
            <h3 className="mb-3 mt-3 text-xl font-semibold text-center md:text-xl">
              Your portfolio is empty.
            </h3>
          )}
        </>
      </div>
    </section>
  )
}

DeletePortfolio

  • DeletePortfolio.tsx
...
const DeletePortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <button className="block w-full py-3 text-white duration-200 border-2 rounded-lg bg-red-500 hover:text-red-500 hover:bg-white border-red-500">
      X
    </button>
  )
}

CardPortfolio

  • CardPortfolio.tsx
...
const CardPortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div className="flex flex-col w-full p-8 space-y-4 text-center rounded-lg shadow-lg md:w-1/3">
      <p className="pt-6 text-xl font-bold">{portfolioValue}</p>
      <DeletePortfolio
        portfolioValue={portfolioValue}
        onPortfolioDelete={onPortfolioDelete}
      />
    </div>
  )
}

AddPortfolio

  • AddPortfolio.tsx
...
const AddPortfolio: React.FC<Props> = ({ onPortfolioCreate, symbol }: Props): JSX.Element => {
  return (
    <div className="flex flex-col items-center justify-end flex-1 space-x-4 space-y-2 md:flex-row md:space-y-0">
      <form onSubmit={onPortfolioCreate}>
        <input readOnly={true} hidden={true} value={symbol} />
        <button
          type="submit"
          className="p-2 px-8 text-white bg-darkBlue rounded-lg hover:opacity-70 focus:outline-none"
        >
          Add
        </button>
      </form>
    </div>
  )
}

Navbar

  • Navbar.tsx
...
const Navbar: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <nav className="relative container mx-auto p-6">
      <div className="flex items-center justify-between">
        <div className="flex items-center space-x-20">
          <img src={logo} alt="" />
          <div className="hidden font-bold lg:flex">
            <a href="" className="text-black hover:text-darkBlue">
              Dashboard
            </a>
          </div>
        </div>
        <div className="hidden lg:flex items-center space-x-6 text-back">
          <div className="hover:text-darkBlue">Login</div>
          <a
            href=""
            className="px-8 py-3 font-bold rounded text-white bg-lightGreen hover:opacity-70"
          >
            Signup
          </a>
        </div>
      </div>
    </nav>
  )
}

Hero

  • create Hero folder in components folder
  • create Hero.tsx and Hero.css in Hero folder

  • Hero.tsx
import React from 'react'
import hero from '../../assets/images/hero.png'

interface Props {}

const Hero: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <section id="hero">
    <div className="container flex flex-col-reverse mx-auto p-8 lg:flex-row">
      <div className="flex flex-col space-y-10 mb-44 m-10 lg:m-10 xl:m-20 lg:mt:16 lg:w-1/2 xl:mb-52">
        <h1 className="text-5xl font-bold text-center lg:text-6xl lg:max-w-md lg:text-left">
          Financial data with no news.
        </h1>
        <p className="text-2xl text-center text-gray-400 lg:max-w-md lg:text-left">
          Search relevant financial documents without fear mongering and fake
          news.
        </p>
        <div className="mx-auto lg:mx-0">
          <a
            href=""
            className="py-5 px-10 text-2xl font-bold text-white bg-lightGreen rounded lg:py-4 hover:opacity-70"
          >
            Get Started
          </a>
        </div>
      </div>
      <div className="mb-24 mx-auto md:w-180 md:px-10 lg:mb-0 lg:w-1/2">
        <img src={hero} alt="" />
      </div>
    </div>
  </section>
  )
}

export default Hero

CardList

  • CardList.tsx
...
const CardList: React.FC<Props> = ( { searchResults, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <>
      {searchResults.length > 0 ? (
        searchResults.map((result) => {
          return <Card id={result.symbol} key={uuidv4()} searchResult={result} onPortfolioCreate={onPortfolioCreate}/>
        })
      ) : (
        <p className="mb-3 mt-3 text-xl font-semibold text-center md:text-xl">
          No results!
        </p>)}
    </>
  )
}

Card

  • Card.tsx
...
const Card: React.FC<Props> = ({ id, searchResult, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <div
      className="flex flex-col items-center justify-between w-full p-6 bg-slate-100 rounded-lg md:flex-row"
      key={id}
      id={id}
    >
      <h2 className="font-bold text-center text-veryDarkViolet md:text-left">
        {searchResult.name} ({searchResult.symbol})
      </h2>
      <p className="text-veryDarkBlue">{searchResult.currency}</p>
      <p className="font-bold text-veryDarkBlue">
        {searchResult.exchangeShortName} - {searchResult.stockExchange}
      </p>
      <AddPortfolio onPortfolioCreate={onPortfolioCreate} symbol={searchResult.symbol}/>
    </div>
  )
}
Tailwind

React Router

  • type npm install -save react-router in terminal
  • type npm install -save react-router-dom in terminal
  • type npm install -save @types/react-router in terminal
  • type npm install -save @types/react-router-dom in terminal

Outlet

  • is a component in React Router used to render child within a parent route.
  • acts as a placeholder whrer the content of nested routes will be displayed.

Router

  • create routes folder in src folder
  • create Routes.tsx

  • Routes.tsx
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import HomePage from "../pages/HomePage/HomePage";
import SearchPage from "../pages/SearchPage/SearchPage";
import CompanyPage from "../pages/CompanyPage/CompanyPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      { path: "", element: <HomePage/> },
      { path: "search", element: <SearchPage/> }
    ]
  }
])
  • index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import { RouterProvider } from 'react-router-dom';
import { router } from './routes/Routes';

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);

root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

reportWebVitals();

HomePage

  • create pages folder in src folder
  • create HomePage folder in pages folder
  • create HomePage.tsx and HomePage.css in HomePage folder

  • HomePage.tsx
import React from 'react'
import Hero from '../../components/Hero/Hero'

type Props = {}

const HomePage: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <div>
      <Hero/>
    </div>
  )
}

export default HomePage

SearchPage

  • create SearchPage folder in pages folder
  • create SearchPage.tsx and SearchPage.css in SearchPage folder

  • SearchPage.tsx
import React, { ChangeEvent, SyntheticEvent, useState } from 'react'
import Navbar from '../../components/Navbar/Navbar'
import Search from '../../components/Search/Search'
import ListPortfolio from '../../components/Portfolio/ListPortfolio/ListPortfolio'
import CardList from '../../components/CardList/CardList'
import { CompanySearch } from '../../company'
import { searchCompanies } from '../../api'

type Props = {}

const SearchPage = (props: Props) => {
  const [search, setSearch] = useState<string>("");
  const [portfolioValues, setPortfolioValues] = useState<string[]>([]);
  const [searchResult, setSearchResult] = useState<CompanySearch[]>([]);
  const [serverError, setServerError] = useState<string>("");

  const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };
  const onSearchSubmit = async (e: SyntheticEvent) => {
    e.preventDefault();

    const result = await searchCompanies(search);
    if (typeof result === "string") {
      setServerError(result);
    } else if (Array.isArray(result.data)){
      setSearchResult(result.data);
    }
    console.log(searchResult);
  };
  const onPortfolioCreate = (e: any) => {
    e.preventDefault();
    
    const exists = portfolioValues.find((value) => value === e.target[0].value);
    if (exists) return;
    
    const updatedPortfolio = [...portfolioValues, e.target[0].value]
    setPortfolioValues(updatedPortfolio);
  }
  const onPortfolioDelete = (e: any) => {
    e.preventDefault();
    
    const removed = portfolioValues.filter((value) => {
      return value !== e.target[0].value;
    });
    setPortfolioValues(removed);
  }

  return (
    <div>
      <Search
        onSearchSubmit={onSearchSubmit}
        search={search}
        handleSearchChange={handleSearchChange}
      />
      <ListPortfolio
        portfolioValues={portfolioValues}
        onPortfolioDelete={onPortfolioDelete}
      />
      <CardList
        searchResults={searchResult}
        onPortfolioCreate={onPortfolioCreate}
      />

      {serverError && <div>Unable to connect to API</div>}
    </div>
  )
}

export default SearchPage

Hero

  • create Hero folder in components folder
  • create Hero.tsx and Hero.css in Hero folder

  • Hero.tsx
import React from 'react'
import hero from '../../assets/images/hero.png'
import { Link } from 'react-router-dom'

interface Props {}

const Hero: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <section id="hero">
    <div className="container flex flex-col-reverse mx-auto p-8 lg:flex-row">
      <div className="flex flex-col space-y-10 mb-44 m-10 lg:m-10 xl:m-20 lg:mt:16 lg:w-1/2 xl:mb-52">
        <h1 className="text-5xl font-bold text-center lg:text-6xl lg:max-w-md lg:text-left">
          Financial data with no news.
        </h1>
        <p className="text-2xl text-center text-gray-400 lg:max-w-md lg:text-left">
          Search relevant financial documents without fear mongering and fake
          news.
        </p>
        <div className="mx-auto lg:mx-0">
          <Link
            to="/search"
            className="py-5 px-10 text-2xl font-bold text-white bg-lightGreen rounded lg:py-4 hover:opacity-70"
          >
            Get Started
          </Link>
        </div>
      </div>
      <div className="mb-24 mx-auto md:w-180 md:px-10 lg:mb-0 lg:w-1/2">
        <img src={hero} alt="" />
      </div>
    </div>
  </section>
  )
}

export default Hero

Navbar

  • Navbar.tsx
...
const Navbar: React.FC<Props> = (props: Props): JSX.Element => {
  return (
    <nav className="relative container mx-auto p-6">
      <div className="flex items-center justify-between">
        <div className="flex items-center space-x-20">
          <Link to="/">
            <img src={logo} alt="" />
          </Link>
          <div className="hidden font-bold lg:flex">
            <Link to="/search" className="text-black hover:text-darkBlue">
              Search
            </Link>
          </div>
        </div>
        <div className="hidden lg:flex items-center space-x-6 text-back">
          <div className="hover:text-darkBlue">Login</div>
          <a
            href=""
            className="px-8 py-3 font-bold rounded text-white bg-lightGreen hover:opacity-70"
          >
            Signup
          </a>
        </div>
      </div>
    </nav>
  )
}

CardPortfolio

  • CardPortfolio.tsx
...
const CardPortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div className="flex flex-col w-full p-8 space-y-4 text-center rounded-lg shadow-lg md:w-1/3">
      <Link to={`/company/${portfolioValue}`} className="pt-6 text-xl font-bold">
        {portfolioValue}
      </Link>
      <DeletePortfolio
        portfolioValue={portfolioValue}
        onPortfolioDelete={onPortfolioDelete}
      />
    </div>
  )
}

CardList

  • CardList.tsx
...
const CardList: React.FC<Props> = ( { searchResults, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <>
      {searchResults.length > 0 ? (
        searchResults.map((result) => {
          return <Card id={result.symbol} key={uuidv4()} searchResult={result} onPortfolioCreate={onPortfolioCreate}/>
        })
      ) : (
        <p className="mb-3 mt-3 text-xl font-semibold text-center md:text-xl">
          No results!
        </p>)}
    </>
  )
}

Card

  • Card.tsx
...
const Card: React.FC<Props> = ({ id, searchResult, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <div
      className="flex flex-col items-center justify-between w-full p-6 bg-slate-100 rounded-lg md:flex-row"
      key={id}
      id={id}
    >
      <p className="text-veryDarkBlue">{searchResult.currency}</p>
      <p className="font-bold text-veryDarkBlue">
        {searchResult.exchangeShortName} - {searchResult.stockExchange}
      </p>
      <AddPortfolio onPortfolioCreate={onPortfolioCreate} symbol={searchResult.symbol}/>
    </div>
  )
}

App

  • App.tsx
import { Outlet } from 'react-router';
import './App.css';
import Navbar from './components/Navbar/Navbar';

function App() {
  

  return (
    <>
      <Navbar/>
      <Outlet/>
    </>
  );
}

export default App;

useEffect

  • is a React hook used for side effects.
  • runs after the first render by default and can re-run based on changes to dependencies.

useParams

  • is a React Router hook used to access the dynamic parameters in the current URL.
  • returns an object with key-value pairs corresponding to the named parameters in the route path.
  • works with nested routes or multiple parameters and makes it easy to build dynamic, URL-driven applications.

Api

  • api.tsx
...
export const getCompanyProfile = async (query: string) => {
  try {
    const data = await axios.get<CompanyProfile[]>(
      `https://financialmodelingprep.com/api/v3/profile/${query}?apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error: any) {
    console.log("error message from API: ", error.message);

  }
}

Card

  • Card.tsx
...
const Card: React.FC<Props> = ({ id, searchResult, onPortfolioCreate }: Props): JSX.Element => {
  return (
    <div
      className="flex flex-col items-center justify-between w-full p-6 bg-slate-100 rounded-lg md:flex-row"
      key={id}
      id={id}
    >
      <Link to={`/company/${searchResult.symbol}`} className="font-bold text-center text-veryDarkViolet md:text-left">
        {searchResult.name} ({searchResult.symbol})
      </Link>
      <p className="text-veryDarkBlue">{searchResult.currency}</p>
      <p className="font-bold text-veryDarkBlue">
        {searchResult.exchangeShortName} - {searchResult.stockExchange}
      </p>
      <AddPortfolio onPortfolioCreate={onPortfolioCreate} symbol={searchResult.symbol}/>
    </div>
  )
}

CompanyPage

  • create CompanyPage folder in page folder
  • create CompanyPage.tsx and CompanyPage.css in CompanyPage folder

  • CompanyPage.tsx
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router'
import { CompanyProfile } from '../../company';
import { getCompanyProfile } from '../../api';

type Props = {}

const CompanyPage: React.FC<Props> = (props: Props): JSX.Element => {
  let { ticker } = useParams();
  const [company, setCompany] = useState<CompanyProfile>();

  useEffect(() => {
    const getCompanyInit = async () => {
      const result = await getCompanyProfile(ticker!);
      setCompany(result?.data[0]);
    }
    getCompanyInit();
  }, []);

  return (
    <>
      {company ? (
        <div>{company.companyName}</div>
      ) : (
        <p>Company not found!</p>
      )}
    </>
  )
}

export default CompanyPage

Router

  • Routes.tsx
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import HomePage from "../pages/HomePage/HomePage";
import SearchPage from "../pages/SearchPage/SearchPage";
import CompanyPage from "../pages/CompanyPage/CompanyPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      { path: "", element: <HomePage/> },
      { path: "search", element: <SearchPage/> },
      { path: "company/:ticker", element: <CompanyPage/> }
    ]
  }
])
useEffect

Dashboard

useParams

react-icons

  • type npm install react-icons --save in terminal
  • check icons in react-icons

CompanyProfile

  • create CompanyProfile folder in components folder
  • create CompanyProfile.tsx and CompanyProfile.css in CompanyProfile folder

  • CompanyProfile.tsx
import React from 'react'

type Props = {}

const CompanyProfile = (props: Props) => {
  return (
    <div>CompanyProfile</div>
  )
}

export default CompanyProfile

Router

  • Routes.tsx
...
export const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      { path: "", element: <HomePage/> },
      { path: "search", element: <SearchPage/> },
      { path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> }
        ]
      }
    ]
  }
])

CompanyDashboard

  • create CompanyDashboard folder in components folder
  • create CompanyDashboard.tsx and CompanyDashboard.css in CompanyDashboard folder

  • CompanyDashboard.tsx
import React from 'react'
import { Outlet } from 'react-router'

interface Props {
  children: React.ReactNode;
}

const CompanyDashboard = ({ children }: Props) => {
  return (
    <div className="relative md:ml-64 bg-blueGray-100 w-full">
      <div className="relative pt-20 pb-32 bg-lightBlue-500">
        <div className="px-4 md:px-6 mx-auto w-full">
          <div>
            <div className="flex flex-wrap">
              {children}
            </div>
            <div className="flex flex-wrap">
              {<Outlet />}
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default CompanyDashboard

CompanyPage

  • CompanyPage.tsx
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router'
import { CompanyProfile } from '../../company';
import { getCompanyProfile } from '../../api';
import Sidebar from '../../components/Sidebar/Sidebar';
import CompanyDashboard from '../../components/CompanyDashboard/CompanyDashboard';
import Tile from '../../components/Tile/Tile';

type Props = {}

const CompanyPage: React.FC<Props> = (props: Props): JSX.Element => {
  let { ticker } = useParams();
  const [company, setCompany] = useState<CompanyProfile>();

  useEffect(() => {
    const getCompanyInit = async () => {
      const result = await getCompanyProfile(ticker!);
      setCompany(result?.data[0]);
    }
    getCompanyInit();
  }, []);

  return (
    <>
      {company ? (
        <div className="w-full relative flex ct-docs-disable-sidebar-content overflow-x-hidden">
          <Sidebar />
          <CompanyDashboard>
            <Tile title="Company Name" subTitle={company.companyName}/>
          </CompanyDashboard>
        </div>
      ) : (
        <p>Company not found!</p>
      )}
    </>
  )
}

export default CompanyPage

Sidebar

  • Sidebar.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import { FaHome } from 'react-icons/fa';

interface Props {}

const Sidebar: React.FC<Props> = (props: Props): JSX.Element => (
  <nav className="block py-4 px-6 top-0 bottom-0 w-64 bg-white shadow-xl left-0 absolute flex-row flex-nowrap md:z-10 z-9999 transition-all duration-300 ease-in-out transform md:translate-x-0 -translate-x-full">
    <button className="md:hidden flex items-center justify-center cursor-pointer text-blueGray-700 w-6 h-10 border-l-0 border-r border-t border-b border-solid border-blueGray-100 text-xl leading-none bg-white rounded-r border border-solid border-transparent absolute top-1/2 -right-24-px focus:outline-none z-9998">
      <i className="fas fa-ellipsis-v"></i>
    </button>
    <div className="flex-col min-h-full px-0 flex flex-wrap items-center justify-between w-full mx-auto overflow-y-auto overflow-x-hidden">
      <div className="flex bg-white flex-col items-stretch opacity-100 relative mt-4 overflow-y-auto overflow-x-hidden h-auto z-40 items-center flex-1 rounded w-full">
        <div className="md:flex-col md:min-w-full flex flex-col list-none">
          <Link to="company-profile" className="flex md:min-w-full text-blueGray-500 text-xs uppercase font-bold block pt-1 pb-4 no-underline">
            <FaHome/>
            <h6 className="ml-3">Company Profile</h6>
          </Link>
        </div>
      </div>
    </div>
  </nav>
)

export default Sidebar

Tile

  • Tile.tsx
import React from 'react'

type Props = {
  title: string;
  subTitle: string;
}

const Tile = ({ title, subTitle }: Props) => {
  return (
    <div className="w-full lg:w-6/12 xl:w-3/12 px-4">
      <div className="relative flex flex-col min-w-0 break-words bg-white rounded-lg mb-6 xl:mb-0 shadow-lg">
        <div className="flex-auto p-4">
          <div className="flex flex-wrap">
            <div className="relative w-full pr-4 max-w-full flex-grow flex-1">
              <h5 className="text-blueGray-400 uppercase font-bold text-xs">
                {title}
              </h5>
              <span className="font-bold text-xl">{subTitle}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default Tile
Dashboard

Table

  • create Table folder in components folder
  • create testData.tsx, Table.tsx and Table.css in Table folder

  • testData.tsx
export const TestDataCompany = [
  {
    symbol: "AAPL",
    price: 145.85,
    beta: 1.201965,
    volAvg: 79766736,
    mktCap: 2410929717248,
    lastDiv: 0.85,
    range: "105.0-157.26",
    changes: 2.4200134,
    companyName: "Apple Inc.",
    currency: "USD",
    cik: "0000320193",
    isin: "US0378331005",
    cusip: "037833100",
    exchange: "Nasdaq Global Select",
    exchangeShortName: "NASDAQ",
    industry: "Consumer Electronics",
    website: "http://www.apple.com",
    description:
      "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related services. The company offers iPhone, a line of smartphones; Mac, a line of personal computers; iPad, a line of multi-purpose tablets; and wearables, home, and accessories comprising AirPods, Apple TV, Apple Watch, Beats products, HomePod, iPod touch, and other Apple-branded and third-party accessories. It also provides AppleCare support services; cloud services store services; and operates various platforms, including the App Store, that allow customers to discover and download applications and digital content, such as books, music, video, games, and podcasts. In addition, the company offers various services, such as Apple Arcade, a game subscription service; Apple Music, which offers users a curated listening experience with on-demand radio stations; Apple News+, a subscription news and magazine service; Apple TV+, which offers exclusive original content; Apple Card, a co-branded credit card; and Apple Pay, a cashless payment service, as well as licenses its intellectual property. The company serves consumers, and small and mid-sized businesses; and the education, enterprise, and government markets. It sells and delivers third-party applications for its products through the App Store. The company also sells its products through its retail and online stores, and direct sales force; and third-party cellular network carriers, wholesalers, retailers, and resellers. Apple Inc. was founded in 1977 and is headquartered in Cupertino, California.",
    ceo: "Mr. Timothy Cook",
    sector: "Technology",
    country: "US",
    fullTimeEmployees: "147000",
    phone: "14089961010",
    address: "1 Apple Park Way",
    city: "Cupertino",
    state: "CALIFORNIA",
    zip: "95014",
    dcfDiff: 89.92,
    dcf: 148.019,
    image: "https://financialmodelingprep.com/image-stock/AAPL.png",
    ipoDate: "1980-12-12",
    defaultImage: false,
    isEtf: false,
    isActivelyTrading: true,
    isAdr: false,
    isFund: false,
  },
];

export const testIncomeStatementData = [
  {
    date: "2022-09-24",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2022-10-28",
    acceptedDate: "2022-10-27 18:01:14",
    calendarYear: "2022",
    period: "FY",
    revenue: 394328000000,
    costOfRevenue: 223546000000,
    grossProfit: 170782000000,
    grossProfitRatio: 0.4330963056,
    researchAndDevelopmentExpenses: 26251000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 25094000000,
    otherExpenses: -334000000,
    operatingExpenses: 51345000000,
    costAndExpenses: 274891000000,
    interestIncome: 2825000000,
    interestExpense: 2931000000,
    depreciationAndAmortization: 11104000000,
    ebitda: 130541000000,
    ebitdaratio: 0.3310467428,
    operatingIncome: 119437000000,
    operatingIncomeRatio: 0.302887444,
    totalOtherIncomeExpensesNet: -334000000,
    incomeBeforeTax: 119103000000,
    incomeBeforeTaxRatio: 0.3020404333,
    incomeTaxExpense: 19300000000,
    netIncome: 99803000000,
    netIncomeRatio: 0.2530964071,
    eps: 6.15,
    epsdiluted: 6.11,
    weightedAverageShsOut: 16215963000,
    weightedAverageShsOutDil: 16325819000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019322000108/0000320193-22-000108-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019322000108/aapl-20220924.htm",
  },
  {
    date: "2021-09-25",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2021-10-29",
    acceptedDate: "2021-10-28 18:04:28",
    calendarYear: "2021",
    period: "FY",
    revenue: 365817000000,
    costOfRevenue: 212981000000,
    grossProfit: 152836000000,
    grossProfitRatio: 0.4177935963,
    researchAndDevelopmentExpenses: 21914000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 21973000000,
    otherExpenses: 258000000,
    operatingExpenses: 43887000000,
    costAndExpenses: 256868000000,
    interestIncome: 2843000000,
    interestExpense: 2645000000,
    depreciationAndAmortization: 11284000000,
    ebitda: 120233000000,
    ebitdaratio: 0.3286697994,
    operatingIncome: 108949000000,
    operatingIncomeRatio: 0.2978237753,
    totalOtherIncomeExpensesNet: 258000000,
    incomeBeforeTax: 109207000000,
    incomeBeforeTaxRatio: 0.2985290459,
    incomeTaxExpense: 14527000000,
    netIncome: 94680000000,
    netIncomeRatio: 0.2588179336,
    eps: 5.67,
    epsdiluted: 5.61,
    weightedAverageShsOut: 16701272000,
    weightedAverageShsOutDil: 16864919000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019321000105/0000320193-21-000105-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019321000105/aapl-20210925.htm",
  },
  {
    date: "2020-09-26",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2020-10-30",
    acceptedDate: "2020-10-29 18:06:25",
    calendarYear: "2020",
    period: "FY",
    revenue: 274515000000,
    costOfRevenue: 169559000000,
    grossProfit: 104956000000,
    grossProfitRatio: 0.3823324773,
    researchAndDevelopmentExpenses: 18752000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 19916000000,
    otherExpenses: 803000000,
    operatingExpenses: 38668000000,
    costAndExpenses: 208227000000,
    interestIncome: 3763000000,
    interestExpense: 2873000000,
    depreciationAndAmortization: 11056000000,
    ebitda: 77344000000,
    ebitdaratio: 0.2817478098,
    operatingIncome: 66288000000,
    operatingIncomeRatio: 0.2414731435,
    totalOtherIncomeExpensesNet: 803000000,
    incomeBeforeTax: 67091000000,
    incomeBeforeTaxRatio: 0.2443983025,
    incomeTaxExpense: 9680000000,
    netIncome: 57411000000,
    netIncomeRatio: 0.2091361128,
    eps: 3.31,
    epsdiluted: 3.28,
    weightedAverageShsOut: 17352119000,
    weightedAverageShsOutDil: 17528214000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019320000096/0000320193-20-000096-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019320000096/aapl-20200926.htm",
  },
  {
    date: "2019-09-28",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2019-10-31",
    acceptedDate: "2019-10-30 18:12:36",
    calendarYear: "2019",
    period: "FY",
    revenue: 260174000000,
    costOfRevenue: 161782000000,
    grossProfit: 98392000000,
    grossProfitRatio: 0.3781776811,
    researchAndDevelopmentExpenses: 16217000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 18245000000,
    otherExpenses: 1807000000,
    operatingExpenses: 34462000000,
    costAndExpenses: 196244000000,
    interestIncome: 4961000000,
    interestExpense: 3576000000,
    depreciationAndAmortization: 12547000000,
    ebitda: 76477000000,
    ebitdaratio: 0.2939455903,
    operatingIncome: 63930000000,
    operatingIncomeRatio: 0.2457201719,
    totalOtherIncomeExpensesNet: 1807000000,
    incomeBeforeTax: 65737000000,
    incomeBeforeTaxRatio: 0.2526655238,
    incomeTaxExpense: 10481000000,
    netIncome: 55256000000,
    netIncomeRatio: 0.2123809451,
    eps: 2.99,
    epsdiluted: 2.97,
    weightedAverageShsOut: 18471336000,
    weightedAverageShsOutDil: 18595652000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019319000119/0000320193-19-000119-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019319000119/a10-k20199282019.htm",
  },
  {
    date: "2018-09-29",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2018-11-05",
    acceptedDate: "2018-11-05 08:01:40",
    calendarYear: "2018",
    period: "FY",
    revenue: 265595000000,
    costOfRevenue: 163756000000,
    grossProfit: 101839000000,
    grossProfitRatio: 0.3834371882,
    researchAndDevelopmentExpenses: 14236000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 16705000000,
    otherExpenses: 2005000000,
    operatingExpenses: 30941000000,
    costAndExpenses: 194697000000,
    interestIncome: 5686000000,
    interestExpense: 3240000000,
    depreciationAndAmortization: 10903000000,
    ebitda: 81801000000,
    ebitdaratio: 0.3079914908,
    operatingIncome: 70898000000,
    operatingIncomeRatio: 0.2669402662,
    totalOtherIncomeExpensesNet: 2005000000,
    incomeBeforeTax: 72903000000,
    incomeBeforeTaxRatio: 0.2744893541,
    incomeTaxExpense: 13372000000,
    netIncome: 59531000000,
    netIncomeRatio: 0.2241420207,
    eps: 3,
    epsdiluted: 2.98,
    weightedAverageShsOut: 19821508000,
    weightedAverageShsOutDil: 20000436000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019318000145/0000320193-18-000145-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019318000145/a10-k20189292018.htm",
  },
  {
    date: "2017-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2017-11-03",
    acceptedDate: "2017-11-03 08:01:37",
    calendarYear: "2017",
    period: "FY",
    revenue: 229234000000,
    costOfRevenue: 141048000000,
    grossProfit: 88186000000,
    grossProfitRatio: 0.3846986049,
    researchAndDevelopmentExpenses: 11581000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 15261000000,
    otherExpenses: 2745000000,
    operatingExpenses: 26842000000,
    costAndExpenses: 167890000000,
    interestIncome: 5201000000,
    interestExpense: 2323000000,
    depreciationAndAmortization: 10157000000,
    ebitda: 71501000000,
    ebitdaratio: 0.311912718,
    operatingIncome: 61344000000,
    operatingIncomeRatio: 0.2676042821,
    totalOtherIncomeExpensesNet: 2745000000,
    incomeBeforeTax: 64089000000,
    incomeBeforeTaxRatio: 0.2795789455,
    incomeTaxExpense: 15738000000,
    netIncome: 48351000000,
    netIncomeRatio: 0.2109242085,
    eps: 2.32,
    epsdiluted: 2.3,
    weightedAverageShsOut: 20868968000,
    weightedAverageShsOutDil: 21006768000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019317000070/0000320193-17-000070-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000032019317000070/a10-k20179302017.htm",
  },
  {
    date: "2016-09-24",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2016-10-26",
    acceptedDate: "2016-10-26 16:42:16",
    calendarYear: "2016",
    period: "FY",
    revenue: 215639000000,
    costOfRevenue: 131376000000,
    grossProfit: 84263000000,
    grossProfitRatio: 0.3907595565,
    researchAndDevelopmentExpenses: 10045000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 14194000000,
    otherExpenses: 1348000000,
    operatingExpenses: 24239000000,
    costAndExpenses: 155615000000,
    interestIncome: 3999000000,
    interestExpense: 1456000000,
    depreciationAndAmortization: 10505000000,
    ebitda: 70529000000,
    ebitdaratio: 0.3270697787,
    operatingIncome: 62567000000,
    operatingIncomeRatio: 0.2901469586,
    totalOtherIncomeExpensesNet: 1348000000,
    incomeBeforeTax: 61372000000,
    incomeBeforeTaxRatio: 0.2846052894,
    incomeTaxExpense: 15685000000,
    netIncome: 45687000000,
    netIncomeRatio: 0.2118679831,
    eps: 2.09,
    epsdiluted: 2.08,
    weightedAverageShsOut: 21883280000,
    weightedAverageShsOutDil: 22001124000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000162828016020309/0001628280-16-020309-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000162828016020309/a201610-k9242016.htm",
  },
  {
    date: "2015-09-26",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2015-10-28",
    acceptedDate: "2015-10-28 16:31:09",
    calendarYear: "2015",
    period: "FY",
    revenue: 233715000000,
    costOfRevenue: 140089000000,
    grossProfit: 93626000000,
    grossProfitRatio: 0.4005990202,
    researchAndDevelopmentExpenses: 8067000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 14329000000,
    otherExpenses: 1285000000,
    operatingExpenses: 22396000000,
    costAndExpenses: 162485000000,
    interestIncome: 2921000000,
    interestExpense: 733000000,
    depreciationAndAmortization: 11257000000,
    ebitda: 82487000000,
    ebitdaratio: 0.3529384079,
    operatingIncome: 71230000000,
    operatingIncomeRatio: 0.3047729072,
    totalOtherIncomeExpensesNet: 1285000000,
    incomeBeforeTax: 72515000000,
    incomeBeforeTaxRatio: 0.3102710566,
    incomeTaxExpense: 19121000000,
    netIncome: 53394000000,
    netIncomeRatio: 0.228457737,
    eps: 2.32,
    epsdiluted: 2.31,
    weightedAverageShsOut: 23013684000,
    weightedAverageShsOutDil: 23172276000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312515356351/0001193125-15-356351-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312515356351/d17062d10k.htm",
  },
  {
    date: "2014-09-27",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2014-10-27",
    acceptedDate: "2014-10-27 17:11:55",
    calendarYear: "2014",
    period: "FY",
    revenue: 182795000000,
    costOfRevenue: 112258000000,
    grossProfit: 70537000000,
    grossProfitRatio: 0.3858803578,
    researchAndDevelopmentExpenses: 6041000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 11993000000,
    otherExpenses: -431000000,
    operatingExpenses: 18034000000,
    costAndExpenses: 130292000000,
    interestIncome: 1795000000,
    interestExpense: 384000000,
    depreciationAndAmortization: 7946000000,
    ebitda: 61813000000,
    ebitdaratio: 0.3381547635,
    operatingIncome: 52503000000,
    operatingIncomeRatio: 0.2872233923,
    totalOtherIncomeExpensesNet: 980000000,
    incomeBeforeTax: 53483000000,
    incomeBeforeTaxRatio: 0.2925845893,
    incomeTaxExpense: 13973000000,
    netIncome: 39510000000,
    netIncomeRatio: 0.2161437676,
    eps: 1.62,
    epsdiluted: 1.61,
    weightedAverageShsOut: 24342288000,
    weightedAverageShsOutDil: 24490652000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312514383437/0001193125-14-383437-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312514383437/d783162d10k.htm",
  },
  {
    date: "2013-09-28",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2013-10-30",
    acceptedDate: "2013-10-29 20:38:28",
    calendarYear: "2013",
    period: "FY",
    revenue: 170910000000,
    costOfRevenue: 106606000000,
    grossProfit: 64304000000,
    grossProfitRatio: 0.3762448072,
    researchAndDevelopmentExpenses: 4475000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 10830000000,
    otherExpenses: 1156000000,
    operatingExpenses: 15305000000,
    costAndExpenses: 121911000000,
    interestIncome: 1616000000,
    interestExpense: 136000000,
    depreciationAndAmortization: 6757000000,
    ebitda: 55756000000,
    ebitdaratio: 0.3262301796,
    operatingIncome: 50479000000,
    operatingIncomeRatio: 0.29535428,
    totalOtherIncomeExpensesNet: 1156000000,
    incomeBeforeTax: 50155000000,
    incomeBeforeTaxRatio: 0.2934585454,
    incomeTaxExpense: 13118000000,
    netIncome: 37037000000,
    netIncomeRatio: 0.2167046984,
    eps: 1.43,
    epsdiluted: 1.42,
    weightedAverageShsOut: 25909268000,
    weightedAverageShsOutDil: 26086536000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312513416534/0001193125-13-416534-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312513416534/d590790d10k.htm",
  },
  {
    date: "2012-09-29",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2012-10-31",
    acceptedDate: "2012-10-31 17:07:19",
    calendarYear: "2012",
    period: "FY",
    revenue: 156508000000,
    costOfRevenue: 87846000000,
    grossProfit: 68662000000,
    grossProfitRatio: 0.4387123981,
    researchAndDevelopmentExpenses: 3381000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 10040000000,
    otherExpenses: 522000000,
    operatingExpenses: 13421000000,
    costAndExpenses: 101267000000,
    interestIncome: 1088000000,
    interestExpense: -522000000,
    depreciationAndAmortization: 3277000000,
    ebitda: 58518000000,
    ebitdaratio: 0.3738978199,
    operatingIncome: 55241000000,
    operatingIncomeRatio: 0.3529595931,
    totalOtherIncomeExpensesNet: 522000000,
    incomeBeforeTax: 55763000000,
    incomeBeforeTaxRatio: 0.3562948859,
    incomeTaxExpense: 14030000000,
    netIncome: 41733000000,
    netIncomeRatio: 0.266650906,
    eps: 1.59,
    epsdiluted: 1.58,
    weightedAverageShsOut: 26174904000,
    weightedAverageShsOutDil: 26469940000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312512444068/0001193125-12-444068-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312512444068/d411355d10k.htm",
  },
  {
    date: "2011-09-24",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2011-10-26",
    acceptedDate: "2011-10-26 16:35:25",
    calendarYear: "2011",
    period: "FY",
    revenue: 108249000000,
    costOfRevenue: 64431000000,
    grossProfit: 43818000000,
    grossProfitRatio: 0.4047889588,
    researchAndDevelopmentExpenses: 2429000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 7599000000,
    otherExpenses: 415000000,
    operatingExpenses: 10028000000,
    costAndExpenses: 74459000000,
    interestIncome: 519000000,
    interestExpense: -415000000,
    depreciationAndAmortization: 1814000000,
    ebitda: 35604000000,
    ebitdaratio: 0.3289083502,
    operatingIncome: 33790000000,
    operatingIncomeRatio: 0.3121506896,
    totalOtherIncomeExpensesNet: 415000000,
    incomeBeforeTax: 34205000000,
    incomeBeforeTaxRatio: 0.3159844433,
    incomeTaxExpense: 8283000000,
    netIncome: 25922000000,
    netIncomeRatio: 0.2394664154,
    eps: 1,
    epsdiluted: 0.99,
    weightedAverageShsOut: 25879224000,
    weightedAverageShsOutDil: 26226060000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312511282113/0001193125-11-282113-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312511282113/d220209d10k.htm",
  },
  {
    date: "2010-09-25",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2010-10-27",
    acceptedDate: "2010-10-27 16:36:21",
    calendarYear: "2010",
    period: "FY",
    revenue: 65225000000,
    costOfRevenue: 39541000000,
    grossProfit: 25684000000,
    grossProfitRatio: 0.3937753929,
    researchAndDevelopmentExpenses: 1782000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 5517000000,
    otherExpenses: 155000000,
    operatingExpenses: 7299000000,
    costAndExpenses: 46840000000,
    interestIncome: 311000000,
    interestExpense: -155000000,
    depreciationAndAmortization: 1027000000,
    ebitda: 19412000000,
    ebitdaratio: 0.2976159448,
    operatingIncome: 18385000000,
    operatingIncomeRatio: 0.2818704484,
    totalOtherIncomeExpensesNet: 155000000,
    incomeBeforeTax: 18540000000,
    incomeBeforeTaxRatio: 0.2842468379,
    incomeTaxExpense: 4527000000,
    netIncome: 14013000000,
    netIncomeRatio: 0.2148409352,
    eps: 0.55,
    epsdiluted: 0.54,
    weightedAverageShsOut: 25464908000,
    weightedAverageShsOutDil: 25891936000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312510238044/0001193125-10-238044-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312510238044/d10k.htm",
  },
  {
    date: "2009-09-26",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2009-10-27",
    acceptedDate: "2009-10-27 16:18:29",
    calendarYear: "2009",
    period: "FY",
    revenue: 36537000000,
    costOfRevenue: 23397000000,
    grossProfit: 13140000000,
    grossProfitRatio: 0.359635438,
    researchAndDevelopmentExpenses: 1333000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 4149000000,
    otherExpenses: 326000000,
    operatingExpenses: 5482000000,
    costAndExpenses: 28879000000,
    interestIncome: 407000000,
    interestExpense: -326000000,
    depreciationAndAmortization: 703000000,
    ebitda: 8361000000,
    ebitdaratio: 0.2288365219,
    operatingIncome: 7658000000,
    operatingIncomeRatio: 0.2095957523,
    totalOtherIncomeExpensesNet: 326000000,
    incomeBeforeTax: 7984000000,
    incomeBeforeTaxRatio: 0.2185182144,
    incomeTaxExpense: 2280000000,
    netIncome: 5704000000,
    netIncomeRatio: 0.1561157183,
    eps: 0.23,
    epsdiluted: 0.22,
    weightedAverageShsOut: 25004448000,
    weightedAverageShsOutDil: 25396140000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312509214859/0001193125-09-214859-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312509214859/d10k.htm",
  },
  {
    date: "2008-09-27",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2008-11-05",
    acceptedDate: "2008-11-05 06:16:23",
    calendarYear: "2008",
    period: "FY",
    revenue: 32479000000,
    costOfRevenue: 21334000000,
    grossProfit: 11145000000,
    grossProfitRatio: 0.3431448013,
    researchAndDevelopmentExpenses: 1109000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 3761000000,
    otherExpenses: 620000000,
    operatingExpenses: 4870000000,
    costAndExpenses: 26204000000,
    interestIncome: 653000000,
    interestExpense: -620000000,
    depreciationAndAmortization: 473000000,
    ebitda: 6748000000,
    ebitdaratio: 0.2077650174,
    operatingIncome: 6275000000,
    operatingIncomeRatio: 0.1932017611,
    totalOtherIncomeExpensesNet: 620000000,
    incomeBeforeTax: 6895000000,
    incomeBeforeTaxRatio: 0.2122910188,
    incomeTaxExpense: 2061000000,
    netIncome: 4834000000,
    netIncomeRatio: 0.1488346316,
    eps: 0.2,
    epsdiluted: 0.19,
    weightedAverageShsOut: 24684576000,
    weightedAverageShsOutDil: 25259892000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000119312508224958/0001193125-08-224958-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000119312508224958/d10k.htm",
  },
  {
    date: "2007-09-29",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2007-11-15",
    acceptedDate: "2007-11-15 16:49:37",
    calendarYear: "2007",
    period: "FY",
    revenue: 24006000000,
    costOfRevenue: 15852000000,
    grossProfit: 8154000000,
    grossProfitRatio: 0.3396650837,
    researchAndDevelopmentExpenses: 782000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2963000000,
    otherExpenses: -48000000,
    operatingExpenses: 3745000000,
    costAndExpenses: 19597000000,
    interestIncome: 647000000,
    interestExpense: -599000000,
    depreciationAndAmortization: 317000000,
    ebitda: 4726000000,
    ebitdaratio: 0.1968674498,
    operatingIncome: 4409000000,
    operatingIncomeRatio: 0.1836624177,
    totalOtherIncomeExpensesNet: 599000000,
    incomeBeforeTax: 5008000000,
    incomeBeforeTaxRatio: 0.208614513,
    incomeTaxExpense: 1512000000,
    netIncome: 3496000000,
    netIncomeRatio: 0.1456302591,
    eps: 0.14,
    epsdiluted: 0.14,
    weightedAverageShsOut: 24208660000,
    weightedAverageShsOutDil: 24900176000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746907009340/0001047469-07-009340-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000104746907009340/a2181030z10-k.htm",
  },
  {
    date: "2006-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2006-12-29",
    acceptedDate: "2006-12-29 06:05:58",
    calendarYear: "2006",
    period: "FY",
    revenue: 19315000000,
    costOfRevenue: 13717000000,
    grossProfit: 5598000000,
    grossProfitRatio: 0.2898265597,
    researchAndDevelopmentExpenses: 712000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2433000000,
    otherExpenses: -29000000,
    operatingExpenses: 3145000000,
    costAndExpenses: 16862000000,
    interestIncome: 394000000,
    interestExpense: -365000000,
    depreciationAndAmortization: 225000000,
    ebitda: 3043000000,
    ebitdaratio: 0.1575459487,
    operatingIncome: 2453000000,
    operatingIncomeRatio: 0.1269997411,
    totalOtherIncomeExpensesNet: 365000000,
    incomeBeforeTax: 2818000000,
    incomeBeforeTaxRatio: 0.1458969713,
    incomeTaxExpense: 829000000,
    netIncome: 1989000000,
    netIncomeRatio: 0.1029769609,
    eps: 0.0843,
    epsdiluted: 0.0811,
    weightedAverageShsOut: 23633624000,
    weightedAverageShsOutDil: 24570728000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000110465906084288/0001104659-06-084288-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000110465906084288/a06-25759_210k.htm",
  },
  {
    date: "2005-09-24",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2005-12-01",
    acceptedDate: "2005-11-30 21:22:48",
    calendarYear: "2005",
    period: "FY",
    revenue: 13931000000,
    costOfRevenue: 9888000000,
    grossProfit: 4043000000,
    grossProfitRatio: 0.2902160649,
    researchAndDevelopmentExpenses: 534000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1859000000,
    otherExpenses: 0,
    operatingExpenses: 2393000000,
    costAndExpenses: 12281000000,
    interestIncome: 0,
    interestExpense: -165000000,
    depreciationAndAmortization: 179000000,
    ebitda: 1829000000,
    ebitdaratio: 0.1312899289,
    operatingIncome: 1650000000,
    operatingIncomeRatio: 0.1184408872,
    totalOtherIncomeExpensesNet: 165000000,
    incomeBeforeTax: 1815000000,
    incomeBeforeTaxRatio: 0.130284976,
    incomeTaxExpense: 480000000,
    netIncome: 1335000000,
    netIncomeRatio: 0.0958294451,
    eps: 0.0586,
    epsdiluted: 0.0554,
    weightedAverageShsOut: 22636292000,
    weightedAverageShsOutDil: 23992584000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000110465905058421/0001104659-05-058421-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000110465905058421/a05-20674_110k.htm",
  },
  {
    date: "2004-09-25",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2004-12-03",
    acceptedDate: "2004-12-02 18:05:49",
    calendarYear: "2004",
    period: "FY",
    revenue: 8279000000,
    costOfRevenue: 6020000000,
    grossProfit: 2259000000,
    grossProfitRatio: 0.2728590409,
    researchAndDevelopmentExpenses: 489000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1421000000,
    otherExpenses: -9000000,
    operatingExpenses: 1910000000,
    costAndExpenses: 7930000000,
    interestIncome: 64000000,
    interestExpense: 3000000,
    depreciationAndAmortization: 150000000,
    ebitda: 554000000,
    ebitdaratio: 0.0669162942,
    operatingIncome: 326000000,
    operatingIncomeRatio: 0.0393767363,
    totalOtherIncomeExpensesNet: 57000000,
    incomeBeforeTax: 383000000,
    incomeBeforeTaxRatio: 0.0462616258,
    incomeTaxExpense: 107000000,
    netIncome: 276000000,
    netIncomeRatio: 0.0333373596,
    eps: 0.0129,
    epsdiluted: 0.0121,
    weightedAverageShsOut: 20809040000,
    weightedAverageShsOutDil: 21693728000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746904035975/0001047469-04-035975-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000104746904035975/a2147337z10-k.htm",
  },
  {
    date: "2003-09-27",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2003-12-19",
    acceptedDate: "2003-12-19 17:25:45",
    calendarYear: "2003",
    period: "FY",
    revenue: 6207000000,
    costOfRevenue: 4499000000,
    grossProfit: 1708000000,
    grossProfitRatio: 0.2751731916,
    researchAndDevelopmentExpenses: 471000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1212000000,
    otherExpenses: -5000000,
    operatingExpenses: 1683000000,
    costAndExpenses: 6182000000,
    interestIncome: 69000000,
    interestExpense: 8000000,
    depreciationAndAmortization: 113000000,
    ebitda: 202000000,
    ebitdaratio: 0.032543902,
    operatingIncome: 89000000,
    operatingIncomeRatio: 0.0143386499,
    totalOtherIncomeExpensesNet: 3000000,
    incomeBeforeTax: 92000000,
    incomeBeforeTaxRatio: 0.0148219752,
    incomeTaxExpense: 24000000,
    netIncome: 69000000,
    netIncomeRatio: 0.0111164814,
    eps: 0.0036,
    epsdiluted: 0.0032,
    weightedAverageShsOut: 20195336000,
    weightedAverageShsOutDil: 20354096000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746903041604/0001047469-03-041604-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000104746903041604/a2124888z10-k.htm",
  },
  {
    date: "2002-09-28",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2002-12-19",
    acceptedDate: "2002-12-19 17:20:21",
    calendarYear: "2002",
    period: "FY",
    revenue: 5742000000,
    costOfRevenue: 4139000000,
    grossProfit: 1603000000,
    grossProfitRatio: 0.2791710206,
    researchAndDevelopmentExpenses: 447000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1111000000,
    otherExpenses: 5000000,
    operatingExpenses: 1558000000,
    costAndExpenses: 5697000000,
    interestIncome: 118000000,
    interestExpense: 11000000,
    depreciationAndAmortization: 118000000,
    ebitda: 286000000,
    ebitdaratio: 0.0498084291,
    operatingIncome: 168000000,
    operatingIncomeRatio: 0.0292580982,
    totalOtherIncomeExpensesNet: -81000000,
    incomeBeforeTax: 87000000,
    incomeBeforeTaxRatio: 0.0151515152,
    incomeTaxExpense: 22000000,
    netIncome: 65000000,
    netIncomeRatio: 0.0113200975,
    eps: 0.0032,
    epsdiluted: 0.0032,
    weightedAverageShsOut: 19881232000,
    weightedAverageShsOutDil: 20259960000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746902007674/0001047469-02-007674-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000104746902007674/a2096490z10-k.htm",
  },
  {
    date: "2001-09-29",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2001-12-21",
    acceptedDate: "2001-12-21 00:00:00",
    calendarYear: "2001",
    period: "FY",
    revenue: 5363000000,
    costOfRevenue: 4128000000,
    grossProfit: 1235000000,
    grossProfitRatio: 0.2302815588,
    researchAndDevelopmentExpenses: 441000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1138000000,
    otherExpenses: 8000000,
    operatingExpenses: 1579000000,
    costAndExpenses: 5707000000,
    interestIncome: 218000000,
    interestExpense: 16000000,
    depreciationAndAmortization: 102000000,
    ebitda: -16000000,
    ebitdaratio: -0.0029834048,
    operatingIncome: -344000000,
    operatingIncomeRatio: -0.0641432034,
    totalOtherIncomeExpensesNet: 292000000,
    incomeBeforeTax: -52000000,
    incomeBeforeTaxRatio: -0.0096960656,
    incomeTaxExpense: -15000000,
    netIncome: -37000000,
    netIncomeRatio: -0.0068991236,
    eps: -0.0019,
    epsdiluted: -0.0019,
    weightedAverageShsOut: 19354328000,
    weightedAverageShsOutDil: 19354328000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000091205701544436/0000912057-01-544436-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000091205701544436/a2066171z10-k405.htm",
  },
  {
    date: "2000-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "2000-12-14",
    acceptedDate: "2000-12-14 00:00:00",
    calendarYear: "2000",
    period: "FY",
    revenue: 7983000000,
    costOfRevenue: 5817000000,
    grossProfit: 2166000000,
    grossProfitRatio: 0.271326569,
    researchAndDevelopmentExpenses: 380000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1166000000,
    otherExpenses: 6000000,
    operatingExpenses: 1546000000,
    costAndExpenses: 7363000000,
    interestIncome: 210000000,
    interestExpense: 21000000,
    depreciationAndAmortization: 84000000,
    ebitda: 920000000,
    ebitdaratio: 0.1152448954,
    operatingIncome: 836000000,
    operatingIncomeRatio: 0.1047225354,
    totalOtherIncomeExpensesNet: 256000000,
    incomeBeforeTax: 1092000000,
    incomeBeforeTaxRatio: 0.1367906802,
    incomeTaxExpense: 306000000,
    netIncome: 786000000,
    netIncomeRatio: 0.0984592259,
    eps: 0.0432,
    epsdiluted: 0.0389,
    weightedAverageShsOut: 18175808000,
    weightedAverageShsOutDil: 20178144000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000091205700053623/0000912057-00-053623-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000091205700053623/a2032880z10-k.txt",
  },
  {
    date: "1999-09-25",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1999-12-22",
    acceptedDate: "1999-12-22 00:00:00",
    calendarYear: "1999",
    period: "FY",
    revenue: 6134000000,
    costOfRevenue: 4438000000,
    grossProfit: 1696000000,
    grossProfitRatio: 0.2764916857,
    researchAndDevelopmentExpenses: 314000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 996000000,
    otherExpenses: -2000000,
    operatingExpenses: 1310000000,
    costAndExpenses: 5748000000,
    interestIncome: 144000000,
    interestExpense: 47000000,
    depreciationAndAmortization: 85000000,
    ebitda: 613000000,
    ebitdaratio: 0.0999347897,
    operatingIncome: 528000000,
    operatingIncomeRatio: 0.0860776003,
    totalOtherIncomeExpensesNet: 148000000,
    incomeBeforeTax: 676000000,
    incomeBeforeTaxRatio: 0.1102054125,
    incomeTaxExpense: 75000000,
    netIncome: 601000000,
    netIncomeRatio: 0.0979784806,
    eps: 0.0375,
    epsdiluted: 0.0323,
    weightedAverageShsOut: 16033584000,
    weightedAverageShsOutDil: 19506368000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000091205799010244/0000912057-99-010244-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/000091205799010244/0000912057-99-010244.txt",
  },
  {
    date: "1998-09-25",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1998-12-23",
    acceptedDate: "1998-12-23 00:00:00",
    calendarYear: "1998",
    period: "FY",
    revenue: 5941000000,
    costOfRevenue: 4462000000,
    grossProfit: 1479000000,
    grossProfitRatio: 0.2489479886,
    researchAndDevelopmentExpenses: 310000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 908000000,
    otherExpenses: -8000000,
    operatingExpenses: 1218000000,
    costAndExpenses: 5680000000,
    interestIncome: 100000000,
    interestExpense: 62000000,
    depreciationAndAmortization: 111000000,
    ebitda: 504000000,
    ebitdaratio: 0.084834203,
    operatingIncome: 299000000,
    operatingIncomeRatio: 0.0503282276,
    totalOtherIncomeExpensesNet: -64000000,
    incomeBeforeTax: 329000000,
    incomeBeforeTaxRatio: 0.0553778825,
    incomeTaxExpense: 20000000,
    netIncome: 309000000,
    netIncomeRatio: 0.0520114459,
    eps: 0.0209,
    epsdiluted: 0.0188,
    weightedAverageShsOut: 14781088000,
    weightedAverageShsOutDil: 18806704000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746998044981/0001047469-98-044981-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/0001047469-98-044981.txt",
  },
  {
    date: "1997-09-26",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1997-12-05",
    acceptedDate: "1997-12-05 00:00:00",
    calendarYear: "1997",
    period: "FY",
    revenue: 7081000000,
    costOfRevenue: 5713000000,
    grossProfit: 1368000000,
    grossProfitRatio: 0.1931930518,
    researchAndDevelopmentExpenses: 860000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1286000000,
    otherExpenses: 3000000,
    operatingExpenses: 2146000000,
    costAndExpenses: 7859000000,
    interestIncome: 82000000,
    interestExpense: 71000000,
    depreciationAndAmortization: 118000000,
    ebitda: -575000000,
    ebitdaratio: -0.0812032199,
    operatingIncome: -778000000,
    operatingIncomeRatio: -0.1098714871,
    totalOtherIncomeExpensesNet: -267000000,
    incomeBeforeTax: -1045000000,
    incomeBeforeTaxRatio: -0.1475780257,
    incomeTaxExpense: 281000000,
    netIncome: -1326000000,
    netIncomeRatio: -0.1872616862,
    eps: -0.0939,
    epsdiluted: -0.0939,
    weightedAverageShsOut: 14118944000,
    weightedAverageShsOutDil: 14118944000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000104746997006960/0001047469-97-006960-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/0001047469-97-006960.txt",
  },
  {
    date: "1996-09-27",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1996-12-19",
    acceptedDate: "1996-12-19 00:00:00",
    calendarYear: "1996",
    period: "FY",
    revenue: 9833000000,
    costOfRevenue: 8865000000,
    grossProfit: 968000000,
    grossProfitRatio: 0.0984440151,
    researchAndDevelopmentExpenses: 604000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1568000000,
    otherExpenses: -3000000,
    operatingExpenses: 2172000000,
    costAndExpenses: 11037000000,
    interestIncome: 60000000,
    interestExpense: 60000000,
    depreciationAndAmortization: 156000000,
    ebitda: -991000000,
    ebitdaratio: -0.1007830774,
    operatingIncome: -1204000000,
    operatingIncomeRatio: -0.1224448286,
    totalOtherIncomeExpensesNet: -91000000,
    incomeBeforeTax: -1295000000,
    incomeBeforeTaxRatio: -0.1316993796,
    incomeTaxExpense: -479000000,
    netIncome: -816000000,
    netIncomeRatio: -0.0829858639,
    eps: -0.0589,
    epsdiluted: -0.0589,
    weightedAverageShsOut: 13858208000,
    weightedAverageShsOutDil: 13858208000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019396000023/0000320193-96-000023-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/0000320193-96-000023.txt",
  },
  {
    date: "1995-09-29",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1995-12-19",
    acceptedDate: "1995-12-19 00:00:00",
    calendarYear: "1995",
    period: "FY",
    revenue: 11062000000,
    costOfRevenue: 8204000000,
    grossProfit: 2858000000,
    grossProfitRatio: 0.2583619599,
    researchAndDevelopmentExpenses: 614000000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1583000000,
    otherExpenses: -1000000,
    operatingExpenses: 2197000000,
    costAndExpenses: 10401000000,
    interestIncome: 100000000,
    interestExpense: 48000000,
    depreciationAndAmortization: 127000000,
    ebitda: 887000000,
    ebitdaratio: 0.0801844151,
    operatingIncome: 684000000,
    operatingIncomeRatio: 0.0618333032,
    totalOtherIncomeExpensesNet: -10000000,
    incomeBeforeTax: 674000000,
    incomeBeforeTaxRatio: 0.0609293075,
    incomeTaxExpense: 250000000,
    netIncome: 424000000,
    netIncomeRatio: 0.038329416,
    eps: 0.0308,
    epsdiluted: 0.0308,
    weightedAverageShsOut: 13781264000,
    weightedAverageShsOutDil: 13781264000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019395000016/0000320193-95-000016-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/0000320193-95-000016.txt",
  },
  {
    date: "1994-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1994-12-13",
    acceptedDate: "1994-12-13 00:00:00",
    calendarYear: "1994",
    period: "FY",
    revenue: 9188748000,
    costOfRevenue: 6844915000,
    grossProfit: 2343833000,
    grossProfitRatio: 0.2550764261,
    researchAndDevelopmentExpenses: 564303000,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1384111000,
    otherExpenses: 8685000,
    operatingExpenses: 1948414000,
    costAndExpenses: 8793329000,
    interestIncome: 43284000,
    interestExpense: 39653000,
    depreciationAndAmortization: 167958000,
    ebitda: 615346000,
    ebitdaratio: 0.0669673387,
    operatingIncome: 522274000,
    operatingIncomeRatio: 0.0568384289,
    totalOtherIncomeExpensesNet: -21988000,
    incomeBeforeTax: 500286000,
    incomeBeforeTaxRatio: 0.0544455023,
    incomeTaxExpense: 190108000,
    netIncome: 310178000,
    netIncomeRatio: 0.0337562854,
    eps: 0.0233,
    epsdiluted: 0.0233,
    weightedAverageShsOut: 13298320000,
    weightedAverageShsOutDil: 13298320000,
    link: "https://www.sec.gov/Archives/edgar/data/320193/000032019394000016/0000320193-94-000016-index.htm",
    finalLink:
      "https://www.sec.gov/Archives/edgar/data/320193/0000320193-94-000016.txt",
  },
  {
    date: "1993-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1993-09-30",
    acceptedDate: "1993-09-30 00:00:00",
    calendarYear: "1993",
    period: "FY",
    revenue: 7977000000,
    costOfRevenue: 5082700000,
    grossProfit: 2894300000,
    grossProfitRatio: 0.3628306381,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2617800000,
    otherExpenses: 166100000,
    operatingExpenses: 2783900000,
    costAndExpenses: 7866600000,
    interestIncome: 0,
    interestExpense: -58600000,
    depreciationAndAmortization: 166100000,
    ebitda: 247200000,
    ebitdaratio: 0.0309890936,
    operatingIncome: 81100000,
    operatingIncomeRatio: 0.0101667293,
    totalOtherIncomeExpensesNet: 58600000,
    incomeBeforeTax: 139700000,
    incomeBeforeTaxRatio: 0.0175128494,
    incomeTaxExpense: 53100000,
    netIncome: 86600000,
    netIncomeRatio: 0.0108562116,
    eps: 0.0065,
    epsdiluted: 0.0065,
    weightedAverageShsOut: 13342000000,
    weightedAverageShsOutDil: 13424000000,
    link: null,
    finalLink: null,
  },
  {
    date: "1992-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1992-09-30",
    acceptedDate: "1992-09-30 00:00:00",
    calendarYear: "1992",
    period: "FY",
    revenue: 7086500000,
    costOfRevenue: 3774200000,
    grossProfit: 3312300000,
    grossProfitRatio: 0.4674098638,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2289400000,
    otherExpenses: 217200000,
    operatingExpenses: 2506600000,
    costAndExpenses: 6280800000,
    interestIncome: 0,
    interestExpense: -99400000,
    depreciationAndAmortization: 217200000,
    ebitda: 973300000,
    ebitdaratio: 0.1373456572,
    operatingIncome: 805700000,
    operatingIncomeRatio: 0.113695054,
    totalOtherIncomeExpensesNet: 49800000,
    incomeBeforeTax: 855500000,
    incomeBeforeTaxRatio: 0.1207225005,
    incomeTaxExpense: 325100000,
    netIncome: 530400000,
    netIncomeRatio: 0.0748465392,
    eps: 0.0387,
    epsdiluted: 0.0387,
    weightedAverageShsOut: 13718880000,
    weightedAverageShsOutDil: 13729632000,
    link: null,
    finalLink: null,
  },
  {
    date: "1991-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1991-09-30",
    acceptedDate: "1991-09-30 00:00:00",
    calendarYear: "1991",
    period: "FY",
    revenue: 6308800000,
    costOfRevenue: 3109700000,
    grossProfit: 3199100000,
    grossProfitRatio: 0.5070853411,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2547400000,
    otherExpenses: 204400000,
    operatingExpenses: 2751800000,
    costAndExpenses: 5861500000,
    interestIncome: 0,
    interestExpense: -104800000,
    depreciationAndAmortization: 204400000,
    ebitda: 599300000,
    ebitdaratio: 0.0949942937,
    operatingIncome: 447300000,
    operatingIncomeRatio: 0.0709009637,
    totalOtherIncomeExpensesNet: 52400000,
    incomeBeforeTax: 499700000,
    incomeBeforeTaxRatio: 0.0792068222,
    incomeTaxExpense: 189900000,
    netIncome: 309800000,
    netIncomeRatio: 0.0491060107,
    eps: 0.023,
    epsdiluted: 0.023,
    weightedAverageShsOut: 13448682171,
    weightedAverageShsOutDil: 13448682171,
    link: null,
    finalLink: null,
  },
  {
    date: "1990-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1990-09-30",
    acceptedDate: "1990-09-30 00:00:00",
    calendarYear: "1990",
    period: "FY",
    revenue: 5558400000,
    costOfRevenue: 2403500000,
    grossProfit: 3154900000,
    grossProfitRatio: 0.5675913932,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 2240200000,
    otherExpenses: 202700000,
    operatingExpenses: 2442900000,
    costAndExpenses: 4846400000,
    interestIncome: 0,
    interestExpense: -133000000,
    depreciationAndAmortization: 202700000,
    ebitda: 848200000,
    ebitdaratio: 0.1525978699,
    operatingIncome: 712000000,
    operatingIncomeRatio: 0.1280944157,
    totalOtherIncomeExpensesNet: 66500000,
    incomeBeforeTax: 778500000,
    incomeBeforeTaxRatio: 0.1400582902,
    incomeTaxExpense: 303600000,
    netIncome: 474900000,
    netIncomeRatio: 0.0854382556,
    eps: 0.0338,
    epsdiluted: 0.0338,
    weightedAverageShsOut: 14071111111,
    weightedAverageShsOutDil: 14071111111,
    link: null,
    finalLink: null,
  },
  {
    date: "1989-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1989-09-30",
    acceptedDate: "1989-09-30 00:00:00",
    calendarYear: "1989",
    period: "FY",
    revenue: 5284000000,
    costOfRevenue: 2570000000,
    grossProfit: 2714000000,
    grossProfitRatio: 0.5136260409,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1954900000,
    otherExpenses: 124800000,
    operatingExpenses: 2079700000,
    costAndExpenses: 4649700000,
    interestIncome: 0,
    interestExpense: -220000000,
    depreciationAndAmortization: 124800000,
    ebitda: 649100000,
    ebitdaratio: 0.1228425435,
    operatingIncome: 634300000,
    operatingIncomeRatio: 0.1200416351,
    totalOtherIncomeExpensesNet: 110000000,
    incomeBeforeTax: 744300000,
    incomeBeforeTaxRatio: 0.1408591976,
    incomeTaxExpense: 290300000,
    netIncome: 454000000,
    netIncomeRatio: 0.0859197578,
    eps: 0.0316,
    epsdiluted: 0.0316,
    weightedAverageShsOut: 14363841808,
    weightedAverageShsOutDil: 14363841808,
    link: null,
    finalLink: null,
  },
  {
    date: "1988-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1988-09-30",
    acceptedDate: "1988-09-30 00:00:00",
    calendarYear: "1988",
    period: "FY",
    revenue: 4071400000,
    costOfRevenue: 1913200000,
    grossProfit: 2158200000,
    grossProfitRatio: 0.5300879304,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 1460200000,
    otherExpenses: 77700000,
    operatingExpenses: 1537900000,
    costAndExpenses: 3451100000,
    interestIncome: 0,
    interestExpense: -71700000,
    depreciationAndAmortization: 77700000,
    ebitda: 662200000,
    ebitdaratio: 0.1626467554,
    operatingIncome: 620300000,
    operatingIncomeRatio: 0.1523554551,
    totalOtherIncomeExpensesNet: 35900000,
    incomeBeforeTax: 656200000,
    incomeBeforeTaxRatio: 0.1611730609,
    incomeTaxExpense: 255900000,
    netIncome: 400300000,
    netIncomeRatio: 0.0983199882,
    eps: 0.0275,
    epsdiluted: 0.0275,
    weightedAverageShsOut: 14556363636,
    weightedAverageShsOutDil: 14556363636,
    link: null,
    finalLink: null,
  },
  {
    date: "1987-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1987-09-30",
    acceptedDate: "1987-09-30 00:00:00",
    calendarYear: "1987",
    period: "FY",
    revenue: 2661100000,
    costOfRevenue: 1225700000,
    grossProfit: 1435400000,
    grossProfitRatio: 0.5394009996,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 993400000,
    otherExpenses: 70500000,
    operatingExpenses: 1063900000,
    costAndExpenses: 2289600000,
    interestIncome: 0,
    interestExpense: -77800000,
    depreciationAndAmortization: 70500000,
    ebitda: 403100000,
    ebitdaratio: 0.1514787118,
    operatingIncome: 371500000,
    operatingIncomeRatio: 0.1396039232,
    totalOtherIncomeExpensesNet: 38900000,
    incomeBeforeTax: 410400000,
    incomeBeforeTaxRatio: 0.1542219383,
    incomeTaxExpense: 192900000,
    netIncome: 217500000,
    netIncomeRatio: 0.0817331179,
    eps: 0.0148,
    epsdiluted: 0.0148,
    weightedAverageShsOut: 14674698795,
    weightedAverageShsOutDil: 14674698795,
    link: null,
    finalLink: null,
  },
  {
    date: "1986-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1986-09-30",
    acceptedDate: "1986-09-30 00:00:00",
    calendarYear: "1986",
    period: "FY",
    revenue: 1901900000,
    costOfRevenue: 840000000,
    grossProfit: 1061900000,
    grossProfitRatio: 0.5583364004,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 737300000,
    otherExpenses: 51100000,
    operatingExpenses: 788400000,
    costAndExpenses: 1628400000,
    interestIncome: 0,
    interestExpense: -72500000,
    depreciationAndAmortization: 51100000,
    ebitda: 288400000,
    ebitdaratio: 0.1516378358,
    operatingIncome: 273500000,
    operatingIncomeRatio: 0.1438035649,
    totalOtherIncomeExpensesNet: 36300000,
    incomeBeforeTax: 309800000,
    incomeBeforeTaxRatio: 0.1628897418,
    incomeTaxExpense: 155800000,
    netIncome: 154000000,
    netIncomeRatio: 0.0809716599,
    eps: 0.0107,
    epsdiluted: 0.0107,
    weightedAverageShsOut: 14373333333,
    weightedAverageShsOutDil: 14373333333,
    link: null,
    finalLink: null,
  },
  {
    date: "1985-09-30",
    symbol: "AAPL",
    reportedCurrency: "USD",
    cik: "0000320193",
    fillingDate: "1985-09-30",
    acceptedDate: "1985-09-30 00:00:00",
    calendarYear: "1985",
    period: "FY",
    revenue: 1918300000,
    costOfRevenue: 1076000000,
    grossProfit: 842300000,
    grossProfitRatio: 0.4390866913,
    researchAndDevelopmentExpenses: 0,
    generalAndAdministrativeExpenses: 0,
    sellingAndMarketingExpenses: 0,
    sellingGeneralAndAdministrativeExpenses: 653200000,
    otherExpenses: 41800000,
    operatingExpenses: 695000000,
    costAndExpenses: 1771000000,
    interestIncome: 0,
    interestExpense: 54500000,
    depreciationAndAmortization: 41800000,
    ebitda: 216300000,
    ebitdaratio: 0.1127560861,
    operatingIncome: 147300000,
    operatingIncomeRatio: 0.0767867383,
    totalOtherIncomeExpensesNet: -27300000,
    incomeBeforeTax: 120000000,
    incomeBeforeTaxRatio: 0.0625553876,
    incomeTaxExpense: 58800000,
    netIncome: 61200000,
    netIncomeRatio: 0.0319032477,
    eps: 0.0045,
    epsdiluted: 0.0045,
    weightedAverageShsOut: 13708800000,
    weightedAverageShsOutDil: 13708800000,
    link: null,
    finalLink: null,
  },
];
  • Table.tsx
import React from 'react'
import { testIncomeStatementData } from './testData'

const data = testIncomeStatementData;

type Props = {}

type Company = (typeof data)[0];

const configs = [
  {
    label: "Year",
    render: (company: Company) => company.acceptedDate
  },
  {
    label: "Cost of Revenue",
    render: (company: Company) => company.costOfRevenue
  }
]

const Table = (props: Props) => {
  const renderedRows = data.map((company) => {
    return (
      <tr key={company.cik}>
        {configs.map((val: any) => {
          return (
            <td className="p-4 whitespace-nowrap text-sm font-normal text-gray-900">
              {val.render(company)}
            </td>
        )})}
      </tr>
    )
  })
  const renderedHeaders = configs.map((config) => {
    return (
      <th className="p-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider" key={config.label}>
        {config.label}
      </th>
    )
  })

  return (
    <div className="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8">
      <table>
        <thead className="min-w-full divide-y divide-gray-200 m-5">
          {renderedHeaders}
        </thead>
        <tbody>
          {renderedRows}
        </tbody>
      </table>
    </div>
  )
}

export default Table

DesignPage

  • create DesginPage folder in pages folder
  • create DesignPage.tsx and DesignPage.css in DesignPage folder

  • DesignPage.tsx
import React from 'react'
import Table from '../../components/Table/Table'

type Props = {}

const DesignPage = (props: Props) => {
  return (
    <>
      <h1>FinShark Design Page</h1>
      <h2>This is Finshark's design page. This is where we well house various design aspects of the app</h2>
      <Table />
    </>
  )
}

export default DesignPage

Router

  • Routes.tsx
import { createBrowserRouter } from "react-router-dom";
import App from "../App";
import HomePage from "../pages/HomePage/HomePage";
import SearchPage from "../pages/SearchPage/SearchPage";
import CompanyPage from "../pages/CompanyPage/CompanyPage";
import CompanyProfile from "../components/CompanyProfile/CompanyProfile";
import DesignPage from "../pages/DesignPage/DesignPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      { path: "", element: <HomePage/> },
      { path: "search", element: <SearchPage/> },
      { path: "design-guide", element: <DesignPage/> },
      { path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> }
        ]
      }
    ]
  }
])
Table

RatioList

  • create RatioList folder in components folder
  • create RatioList.tsx and RatioList.css in RatioList folder

  • RatioList.tsx
import React from 'react'
import { TestDataCompany } from '../Table/testData'

type Props = {}

const data = TestDataCompany[0];

type Company = typeof data;

const config = [
  {
    label: "Company Name",
    render: (company: Company) => company.companyName,
    subTitle: "This is the company name"
  },
  {
    label: "Company Name",
    render: (company: Company) => company.companyName,
    subTitle: "This is the company name"
  }
]

const RatioList = (props: Props) => {
  const renderedRows = config.map(row => {
    return (
      <li className="py-3 sm:py-4">
        <div className="flex items-center space-x-4">
          <div className="flex-1 min-w-0">
            <p className="text-sm font-medium text-gray-900 truncate">
              {row.label}
            </p>
            <p className="text-sm text-gray-500 truncate">
              {row.subTitle && row.subTitle}
            </p>
          </div>
          <div className="inline-flex items-center text-base font-semibold text-gray-900">
            {row.render(data)}
          </div>
        </div>
      </li>
    )
  })
  return (
    <div className="bg-white shadow rounded-lg mb-4 p-4 sm:p-6 h-full">
      <ul className="divide-y divided-gray-200">
        {renderedRows}
      </ul>
    </div>
  )
}

export default RatioList

DesignPage

  • DesignPage.tsx
import React from 'react'
import Table from '../../components/Table/Table'
import RatioList from '../../components/RatioList/RatioList'

type Props = {}

const DesignPage = (props: Props) => {
  return (
    <>
      <h1>FinShark Design Page</h1>
      <h2>This is Finshark's design page. This is where we well house various design aspects of the app</h2>
      <RatioList />
      <Table />
    </>
  )
}

export default DesignPage
RatioList

Company Profile

useOutletContext

  • is a hook provided by React Router to access context data passed through Outlet.

  • CompanyProfile.tsx

import React, { useEffect, useState } from 'react'
import { CompanyKeyMetrics } from '../../company';
import { useOutletContext } from 'react-router';
import { getKeyMetrics } from '../../api';
import RatioList from '../RatioList/RatioList';

type Props = {}

const tableConfig = [
  {
    label: "Market Cap",
    render: (company: CompanyKeyMetrics) => company.marketCapTTM,
    subTitle: "Total value of all a company's shares of stock",
  },
  {
    label: "Current Ratio",
    render: (company: CompanyKeyMetrics) => company.currentRatioTTM,
    subTitle:
      "Measures the companies ability to pay short term debt obligations",
  },
  {
    label: "Return On Equity",
    render: (company: CompanyKeyMetrics) => company.roeTTM,
    subTitle:
      "Return on equity is the measure of a company's net income divided by its shareholder's equity",
  },
  {
    label: "Return On Assets",
    render: (company: CompanyKeyMetrics) => company.returnOnTangibleAssetsTTM,
    subTitle:
      "Return on assets is the measure of how effective a company is using its assets",
  },
  {
    label: "Free Cashflow Per Share",
    render: (company: CompanyKeyMetrics) => company.freeCashFlowPerShareTTM,
    subTitle:
      "Return on assets is the measure of how effective a company is using its assets",
  },
  {
    label: "Book Value Per Share TTM",
    render: (company: CompanyKeyMetrics) => company.bookValuePerShareTTM,
    subTitle:
      "Book value per share indicates a firm's net asset value (total assets - total liabilities) on per share basis",
  },
  {
    label: "Divdend Yield TTM",
    render: (company: CompanyKeyMetrics) => company.dividendYieldTTM,
    subTitle: "Shows how much a company pays each year relative to stock price",
  },
  {
    label: "Capex Per Share TTM",
    render: (company: CompanyKeyMetrics) => company.capexPerShareTTM,
    subTitle:
      "Capex is used by a company to aquire, upgrade, and maintain physical assets",
  },
  {
    label: "Graham Number",
    render: (company: CompanyKeyMetrics) => company.grahamNumberTTM,
    subTitle:
      "This is the upperbouind of the price range that a defensive investor should pay for a stock",
  },
  {
    label: "PE Ratio",
    render: (company: CompanyKeyMetrics) => company.peRatioTTM,
    subTitle:
      "This is the upperbouind of the price range that a defensive investor should pay for a stock",
  },
];

const CompanyProfile = (props: Props) => {
  const ticker = useOutletContext<string>();
  const[companyData, setCompanyData] = useState<CompanyKeyMetrics>();

  useEffect(() => {
    const getCompanyKeyMetrics = async () => {
      const value = await getKeyMetrics(ticker);
      setCompanyData(value?.data[0]);
    };
    getCompanyKeyMetrics();
  }, []);

  return (
    <>
      { companyData ? (
        <RatioList data={companyData} config={tableConfig}/>
      ):(
        <>Loading...</>
      )}
    </>
  )
}

export default CompanyProfile

CompanyDashboard

  • CompanyDashboard.tsx
import React from 'react'
import { Outlet } from 'react-router'

interface Props {
  children: React.ReactNode;
  ticker: string;
}

const CompanyDashboard = ({ children, ticker }: Props) => {
  return (
    <div className="relative md:ml-64 bg-blueGray-100 w-full">
      <div className="relative pt-20 pb-32 bg-lightBlue-500">
        <div className="px-4 md:px-6 mx-auto w-full">
          <div>
            <div className="flex flex-wrap">
              {children}
            </div>
            <div className="flex flex-wrap">
              {<Outlet context={ticker}/>}
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}

export default CompanyDashboard

RatioList

  • RatioList.tsx
type Props = {
  config: any;
  data: any;
}

const RatioList = ({ config, data }: Props) => {
  const renderedRows = config.map((row: any) => {
    return (
      <li className="py-3 sm:py-4">
        <div className="flex items-center space-x-4">
          <div className="flex-1 min-w-0">
            <p className="text-sm font-medium text-gray-900 truncate">
              {row.label}
            </p>
            <p className="text-sm text-gray-500 truncate">
              {row.subTitle && row.subTitle}
            </p>
          </div>
          <div className="inline-flex items-center text-base font-semibold text-gray-900">
            {row.render(data)}
          </div>
        </div>
      </li>
    )
  })
  return (
    <div className="bg-white shadow rounded-lg ml-4 mt-4 mb-4 p-4 sm:p-6 h-full">
      <ul className="divide-y divided-gray-200">
        {renderedRows}
      </ul>
    </div>
  )
}

export default RatioList

Company Model

  • company.d.ts
...
  export interface CompanyKeyMetrics {
    revenuePerShareTTM: number;
    netIncomePerShareTTM: number;
    operatingCashFlowPerShareTTM: number;
    freeCashFlowPerShareTTM: number;
    cashPerShareTTM: number;
    bookValuePerShareTTM: number;
    tangibleBookValuePerShareTTM: number;
    shareholdersEquityPerShareTTM: number;
    interestDebtPerShareTTM: number;
    marketCapTTM: number;
    enterpriseValueTTM: number;
    peRatioTTM: number;
    priceToSalesRatioTTM: number;
    pocfratioTTM: number;
    pfcfRatioTTM: number;
    pbRatioTTM: number;
    ptbRatioTTM: number;
    evToSalesTTM: number;
    enterpriseValueOverEBITDATTM: number;
    evToOperatingCashFlowTTM: number;
    evToFreeCashFlowTTM: number;
    earningsYieldTTM: number;
    freeCashFlowYieldTTM: number;
    debtToEquityTTM: number;
    debtToAssetsTTM: number;
    netDebtToEBITDATTM: number;
    currentRatioTTM: number;
    interestCoverageTTM: number;
    incomeQualityTTM: number;
    dividendYieldTTM: number;
    dividendYieldPercentageTTM: number;
    payoutRatioTTM: number;
    salesGeneralAndAdministrativeToRevenueTTM: number;
    researchAndDevelopementToRevenueTTM: number;
    intangiblesToTotalAssetsTTM: number;
    capexToOperatingCashFlowTTM: number;
    capexToRevenueTTM: number;
    capexToDepreciationTTM: number;
    stockBasedCompensationToRevenueTTM: number;
    grahamNumberTTM: number;
    roicTTM: number;
    returnOnTangibleAssetsTTM: number;
    grahamNetNetTTM: number;
    workingCapitalTTM: number;
    tangibleAssetValueTTM: number;
    netCurrentAssetValueTTM: number;
    investedCapitalTTM: number;
    averageReceivablesTTM: number;
    averagePayablesTTM: number;
    averageInventoryTTM: number;
    daysSalesOutstandingTTM: number;
    daysPayablesOutstandingTTM: number;
    daysOfInventoryOnHandTTM: number;
    receivablesTurnoverTTM: number;
    payablesTurnoverTTM: number;
    inventoryTurnoverTTM: number;
    roeTTM: number;
    capexPerShareTTM: number;
    dividendPerShareTTM: number;
    debtToMarketCapTTM: number;
  }

Api

  • api.tsx
...
export const getKeyMetrics = async (query: string) => {
  try {
    const data = await axios.get<CompanyKeyMetrics[]>(
      `https://financialmodelingprep.com/api/v3/key-metrics-ttm/${query}?apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error: any) {
    console.log("error message from API: ", error.message);
  }
}

CompanyPage

  • CompanyPage.tsx
...
return (
    <>
      {company ? (
        <div className="w-full relative flex ct-docs-disable-sidebar-content overflow-x-hidden">
          <Sidebar />
          <CompanyDashboard ticker={ticker!}>
            <Tile title="Company Name" subTitle={company.companyName}/>
          </CompanyDashboard>
        </div>
      ) : (
        <p>Company not found!</p>
      )}
    </>
  )

DesignPage

  • DesignPage.tsx
import React from 'react'
import Table from '../../components/Table/Table'
import RatioList from '../../components/RatioList/RatioList'
import { testIncomeStatementData } from '../../components/Table/testData';

type Props = {}

const tableConfig = [
  {
    label: "Market Cap",
    render: (company: any) => company.marketCapTTM,
    subTitle: "Total value of all a company's shares of stock",
  }
];

const DesignPage = (props: Props) => {
  return (
    <>
      <h1>FinShark Design Page</h1>
      <h2>This is Finshark's design page. This is where we well house various design aspects of the app</h2>
      <RatioList data={testIncomeStatementData} config={tableConfig}/>
      <Table />
    </>
  )
}

export default DesignPage
CompanyProfile

Income Statement

  • create IncomeStatement folder in components folder
  • create IncomeStatement.tsx and IncomeStatement.css in IncomeStatement folder

  • IncomeStatement.tsx
import React, { useEffect, useState } from 'react'
import { CompanyIncomeStatement } from '../../company';
import { useOutletContext } from 'react-router';
import { getIncomeStatement } from '../../api';
import Table from '../Table/Table';

type Props = {}

const configs = [
  {
    label: "Date",
    render: (company: CompanyIncomeStatement) => company.date,
  },
  {
    label: "Revenue",
    render: (company: CompanyIncomeStatement) => company.revenue,
  },
  {
    label: "Cost Of Revenue",
    render: (company: CompanyIncomeStatement) => company.costOfRevenue,
  },
  {
    label: "Depreciation",
    render: (company: CompanyIncomeStatement) =>
      company.depreciationAndAmortization,
  },
  {
    label: "Operating Income",
    render: (company: CompanyIncomeStatement) => company.operatingIncome,
  },
  {
    label: "Income Before Taxes",
    render: (company: CompanyIncomeStatement) => company.incomeBeforeTax,
  },
  {
    label: "Net Income",
    render: (company: CompanyIncomeStatement) => company.netIncome,
  },
  {
    label: "Net Income Ratio",
    render: (company: CompanyIncomeStatement) => company.netIncomeRatio,
  },
  {
    label: "Earnings Per Share",
    render: (company: CompanyIncomeStatement) => company.eps,
  },
  {
    label: "Earnings Per Diluted",
    render: (company: CompanyIncomeStatement) => company.epsdiluted,
  },
  {
    label: "Gross Profit Ratio",
    render: (company: CompanyIncomeStatement) => company.grossProfitRatio,
  },
  {
    label: "Opearting Income Ratio",
    render: (company: CompanyIncomeStatement) => company.operatingIncomeRatio,
  },
  {
    label: "Income Before Taxes Ratio",
    render: (company: CompanyIncomeStatement) => company.incomeBeforeTaxRatio,
  },
];

const IncomeStatement = (props: Props) => {
  const ticker = useOutletContext<string>();
  const [incomeStatement, setIncomeStatement] = useState<CompanyIncomeStatement[]>();
  
  useEffect(() => {
    const getCompanyIncomeStatement = async () => {
      const result = await getIncomeStatement(ticker!);
      setIncomeStatement(result!.data);
    }
    getCompanyIncomeStatement();
  }, []);

  return (
    <>
      { incomeStatement ? (
        <Table configs={configs} data={incomeStatement}/>
      ):(
        <>Loading...</>
      )}
    </>
  )
}

export default IncomeStatement

Sidebar

  • Sidebar.tsx
...
<Link to="company-profile" className="flex md:min-w-full text-blueGray-500 text-xs uppercase font-bold block pt-1 pb-4 no-underline">
            <FaHome/>
            <h6 className="ml-3">Company Profile</h6>
          </Link>
          <Link to="income-statement" className="flex md:min-w-full text-blueGray-500 text-xs uppercase font-bold block pt-1 pb-4 no-underline">
            <FaHome/>
            <h6 className="ml-3">Income Statement</h6>
          </Link>
...

Table

  • Table.tsx
type Props = {
  configs: any;
  data: any;
}

const Table = ({ configs, data }: Props) => {
  const renderedRows = data.map((company: any) => {
    return (
      <tr key={company.cik}>
        {configs.map((val: any) => {
          return (
            <td className="p-3">
              {val.render(company)}
            </td>
        )})}
      </tr>
    )
  })
  const renderedHeaders = configs.map((configs: any) => {
    return (
      <th className="p-3" key={configs.label}>
        {configs.label}
      </th>
    )
  })

  return (
    <div className="bg-white shadow rounded-lg p-4 sm:p-6 xl:p-8">
      <table>
        <thead className="min-w-full divide-y divide-gray-200 m-5">
          {renderedHeaders}
        </thead>
        <tbody>
          {renderedRows}
        </tbody>
      </table>
    </div>
  )
}

export default Table

DesignPage

  • DesignPage.tsx
...
const DesignPage = (props: Props) => {
  return (
    <>
      <h1>FinShark Design Page</h1>
      <h2>This is Finshark's design page. This is where we well house various design aspects of the app</h2>
      <RatioList data={testIncomeStatementData} config={tableConfig}/>
      <Table data={testIncomeStatementData} configs={tableConfig} />
    </>
  )
...

Routes

  • Routes.tsx
...
{ path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> },
          { path: "income-statement", element: <IncomeStatement/> }
        ]
      }
...

Api

  • api.tsx
...
export const getIncomeStatement = async (query: string) => {
  try {
    const data = await axios.get<CompanyIncomeStatement[]>(
      `https://financialmodelingprep.com/api/v3/income-statement/${query}?apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error: any) {
    console.log("error message from API: ", error.message);
  }
}
Income Statement

Balance Sheet

  • create BalanceSheet folder in components folder
  • create BalanceSheet.tsx and BalanceSheet.css in BalanceSheet folder

  • BalanceSheet.tsx
import React, { useEffect, useState } from 'react'
import { CompanyBalanceSheet, CompanyCashFlow } from '../../company';
import { useOutletContext } from 'react-router';
import { getBalanceSheet } from '../../api';
import RatioList from '../RatioList/RatioList';

type Props = {}

const config = [
  {
    label: "Cash",
    render: (company: CompanyBalanceSheet) => company.cashAndCashEquivalents,
  },
  {
    label: "Inventory",
    render: (company: CompanyBalanceSheet) => company.inventory,
  },
  {
    label: "Other Current Assets",
    render: (company: CompanyBalanceSheet) => company.otherCurrentAssets,
  },
  {
    label: "Minority Interest",
    render: (company: CompanyBalanceSheet) => company.minorityInterest,
  },
  {
    label: "Other Non-Current Assets",
    render: (company: CompanyBalanceSheet) => company.otherNonCurrentAssets,
  },
  {
    label: "Long Term Debt",
    render: (company: CompanyBalanceSheet) => company.longTermDebt,
  },
  {
    label: "Total Debt",
    render: (company: CompanyBalanceSheet) => company.otherCurrentLiabilities,
  },
];

const BalanceSheet = (props: Props) => {
  const ticker = useOutletContext<string>();
  const [balanceSheet, setBalanceSheet] = useState<CompanyBalanceSheet>();

  useEffect(() => {
    const getCompanyBalanceSheet = async() => {
      const result = await getBalanceSheet(ticker!)
      setBalanceSheet(result?.data[0]);
    }
    getCompanyBalanceSheet();
  }, [])

  return (
    <>
      { balanceSheet ? (
        <RatioList config={config} data={balanceSheet}/>
      ) : (
        <>Loading...</>
      )}
    </>
  )
}

export default BalanceSheet

Api

  • api.tsx
...
export const getBalanceSheet = async (query: string) => {
  try {
    const data = await axios.get<CompanyBalanceSheet[]>(
      `https://financialmodelingprep.com/api/v3/balance-sheet-statement/${query}?apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error: any) {
    console.log("error message from API: ", error.message);
  }
}

Routes

  • Routes.tsx
...
{ path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> },
          { path: "income-statement", element: <IncomeStatement/> },
          { path: "balance-sheet", element: <BalanceSheet/> }
        ]
      }
...

Sidebar

  • Sidebar.tsx
...
<Link to="balance-sheet" className="flex md:min-w-full text-blueGray-500 text-xs uppercase font-bold block pt-1 pb-4 no-underline">
            <FaHome/>
            <h6 className="ml-3">Balance Sheet</h6>
          </Link>
...
  • RatioList.tsx
...
 return (
    <div className="bg-white shadow rounded-lg ml-4 mt-4 mb-4 p-4 sm:p-6 w-full">
      <ul className="divide-y divided-gray-200">
        {renderedRows}
      </ul>
    </div>
  )
  • RatioList.tsx
...
 return (
    <div className="bg-white shadow rounded-lg ml-4 mt-4 mb-4 p-4 sm:p-6 w-full">
      <ul className="divide-y divided-gray-200">
        {renderedRows}
      </ul>
    </div>
  )
Balance Sheet

Cash Flow Statement

  • create CashFlowStatement folder in components folder
  • create CashFlowStatement.tsx and CashFlowStatement.css in CashFlowStatement folder

  • CashFlowStatement.tsx
import React, { useEffect, useState } from 'react'
import { CompanyCashFlow } from '../../company';
import { getCashFlowStatement } from '../../api';
import { useOutletContext } from 'react-router';
import RatioList from '../RatioList/RatioList';

type Props = {}

const config = [
  {
    label: "Date",
    render: (company: CompanyCashFlow) => company.date,
  },
  {
    label: "Operating Cashflow",
    render: (company: CompanyCashFlow) => company.operatingCashFlow,
  },
  {
    label: "Property/Machinery Cashflow",
    render: (company: CompanyCashFlow) =>
      company.investmentsInPropertyPlantAndEquipment,
  },
  {
    label: "Other Investing Cashflow",
    render: (company: CompanyCashFlow) => company.otherInvestingActivites,
  },
  {
    label: "Debt Cashflow",
    render: (company: CompanyCashFlow) =>
      company.netCashUsedProvidedByFinancingActivities,
  },
  {
    label: "CapEX",
    render: (company: CompanyCashFlow) => company.capitalExpenditure,
  },
  {
    label: "Free Cash Flow",
    render: (company: CompanyCashFlow) => company.freeCashFlow,
  },
];

const CashFlowStatement = (props: Props) => {
  const ticker = useOutletContext<string>();
  const [cashFlowStatement, setCashFlowStatement] = useState<CompanyCashFlow>();
  useEffect(() => {
    const getCompanyCashFlowStatement = async () => {
      const result = await getCashFlowStatement(ticker);
      setCashFlowStatement(result?.data[0]);
    }
    getCompanyCashFlowStatement();
  }, []);

  return (
    <>
      { cashFlowStatement ? (
        <RatioList config={config} data={cashFlowStatement}/>
      ) : (
        <>Loading...</>
      )}
    </>
  )
}

export default CashFlowStatement

Api

  • api.tsx
...
export const getCashFlowStatement = async (query: string) => {
  try {
    const data = await axios.get<CompanyCashFlow[]>(
      `https://financialmodelingprep.com/api/v3/cash-flow-statement/${query}?apikey=${process.env.REACT_APP_API_KEY}`
    );
    return data;
  } catch (error: any) {
    console.log("error message from API: ", error.message);
  }
}
...

Routes

  • Routes.tsx
...
{ path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> },
          { path: "income-statement", element: <IncomeStatement/> },
          { path: "balance-sheet", element: <BalanceSheet/> },
          { path: "cashflow-statement", element: <CashFlowStatement/> }
        ]
      }
...

Routes

  • Routes.tsx
...
{ path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> },
          { path: "income-statement", element: <IncomeStatement/> },
          { path: "balance-sheet", element: <BalanceSheet/> },
          { path: "cashflow-statement", element: <CashFlowStatement/> }
        ]
      }
...

Routes

  • Sidebar.tsx
...
<Link to="cashflow-statement" className="flex md:min-w-full text-blueGray-500 text-xs uppercase font-bold block pt-1 pb-4 no-underline">
            <FaHome/>
            <h6 className="ml-3">Cash Flow Statement</h6>
          </Link>
...
Cash Flow Statement

Spinner

  • type npm install react-spinners
  • create Spinner folder in components folder
  • create Spinner.tsx and Spinner.css in Spinner folder

  • Spinner.tsx
import { ClipLoader } from 'react-spinners';
import "./Spinner.css"

type Props = {
  isLoading? : boolean;
}

const Spinner = ({ isLoading = true }: Props) => {
  return (
    <>
      <div id="loading-spinner">
        <ClipLoader color="#36d7b7" loading={isLoading} size={35} aria-label="Loading Spinner" data-testid="loader"/>
      </div>
    </>
  )
}

export default Spinner

BalanceSheet

  • BalanceSheet.tsx
...
return (
    <>
      { balanceSheet ? (
        <RatioList config={config} data={balanceSheet}/>
      ) : (
        <Spinner/>
      )}
    </>
  )

CashFlowStatement

  • CashFlowStatement.tsx
...
return (
    <>
      { cashFlowStatement ? (
        <RatioList config={config} data={cashFlowStatement}/>
      ) : (
        <Spinner/>
      )}
    </>
  )

CashFlowStatement

  • CompanyProfile.tsx
...
return (
    <>
      { companyData ? (
        <RatioList data={companyData} config={tableConfig}/>
      ):(
        <Spinner/>
      )}
    </>
  )

IncomeStatement

  • IncomeStatement.tsx
...
return (
    <>
      { incomeStatement ? (
        <Table configs={configs} data={incomeStatement}/>
      ):(
        <Spinner/>
      )}
    </>
  )

CompanyPage

  • CompanyPage.tsx
...
return (
    <>
      {company ? (
        <div className="w-full relative flex ct-docs-disable-sidebar-content overflow-x-hidden">
          <Sidebar />
          <CompanyDashboard ticker={ticker!}>
            <Tile title="Company Name" subTitle={company.companyName}/>
          </CompanyDashboard>
        </div>
      ) : (
        <Spinner/>
      )}
    </>
  )

Formatting

Number Formatting

  • create Helpers folder in components folder
  • create NumberFormatting.tsx in Helpers folder

  • NumberFormatting.tsx
export const formatLargeMonetaryNumber: any = (number: number) => {
  if (number < 0) {
    return "-" + formatLargeMonetaryNumber(-1 * number);
  }
  if (number < 1000) {
    return "$" + number;
  } else if (number >= 1000 && number < 1_000_000) {
    return "$" + (number / 1000).toFixed(1) + "K";
  } else if (number >= 1_000_000 && number < 1_000_000_000) {
    return "$" + (number / 1_000_000).toFixed(1) + "M";
  } else if (number >= 1_000_000_000 && number < 1_000_000_000_000) {
    return "$" + (number / 1_000_000_000).toFixed(1) + "B";
  } else if (number >= 1_000_000_000_000 && number < 1_000_000_000_000_000) {
    return "$" + (number / 1_000_000_000_000).toFixed(1) + "T";
  }
};

export const formatLargeNonMonetaryNumber: any = (number: number) => {
  if (number < 0) {
    return "-" + formatLargeMonetaryNumber(-1 * number);
  }
  if (number < 1000) {
    return number;
  } else if (number >= 1000 && number < 1_000_000) {
    return (number / 1000).toFixed(1) + "K";
  } else if (number >= 1_000_000 && number < 1_000_000_000) {
    return (number / 1_000_000).toFixed(1) + "M";
  } else if (number >= 1_000_000_000 && number < 1_000_000_000_000) {
    return (number / 1_000_000_000).toFixed(1) + "B";
  } else if (number >= 1_000_000_000_000 && number < 1_000_000_000_000_000) {
    return (number / 1_000_000_000_000).toFixed(1) + "T";
  }
};

export const formatRatio = (ratio: number) => {
  return (Math.round(ratio * 100) / 100).toFixed(2);
};

BalanceSheet

  • BalanceSheet.tsx
const config = [
  {
    label: <div className="font-bold">Total Assets</div>,
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.totalAssets)
  },
  {
    label: "Current Assets",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.totalCurrentAssets)
  },
  {
    label: "Total Cash",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.cashAndCashEquivalents)
  },
  {
    label: "Property & equipment",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.propertyPlantEquipmentNet)
  },
  {
    label: "Intangible Assets",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.intangibleAssets)
  },
  {
    label: "Long Term Debt",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.longTermDebt)
  },
  {
    label: "Total Debt",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.otherCurrentLiabilities)
  },
  {
    label: <div className="font-bold">Total Liabilites</div>,
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.totalLiabilities)
  },
  {
    label: "Current Liabilities",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.totalCurrentLiabilities)
  },
  {
    label: "Long-Term Debt",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.longTermDebt)
  },
  {
    label: "Long-Term Income Taxes",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.otherLiabilities)
  },
  {
    label: "Stakeholder's Equity",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.totalStockholdersEquity)
  },
  {
    label: "Retained Earnings",
    render: (company: CompanyBalanceSheet) => formatLargeMonetaryNumber(company.retainedEarnings)
  },
];
...

CashFlowStatement

  • CashFlowStatement.tsx
const config = [
  {
    label: "Date",
    render: (company: CompanyCashFlow) => company.date
  },
  {
    label: "Operating Cashflow",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.operatingCashFlow)
  },
  {
    label: "Investing Cashflow",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.netCashUsedForInvestingActivites)
  },
  {
    label: "Financing Cashflow",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.netCashUsedProvidedByFinancingActivities)
  },
  {
    label: "Cash At End of Period",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.cashAtEndOfPeriod)
  },
  {
    label: "CapEX",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.capitalExpenditure)
  },
  {
    label: "Issuance Of Stock",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.commonStockIssued)
  },
  {
    label: "Free Cash Flow",
    render: (company: CompanyCashFlow) => formatLargeMonetaryNumber(company.freeCashFlow)
  },
];
...

CompanyProfile

  • CompanyProfile.tsx
const tableConfig = [
  {
    label: "Market Cap",
    render: (company: CompanyKeyMetrics) => formatLargeMonetaryNumber(company.marketCapTTM),
    subTitle: "Total value of all a company's shares of stock",
  },
  {
    label: "Current Ratio",
    render: (company: CompanyKeyMetrics) => formatRatio(company.currentRatioTTM),
    subTitle:
      "Measures the companies ability to pay short term debt obligations",
  },
  {
    label: "Return On Equity",
    render: (company: CompanyKeyMetrics) => formatRatio(company.roeTTM),
    subTitle:
      "Return on equity is the measure of a company's net income divided by its shareholder's equity",
  },
  {
    label: "Return On Assets",
    render: (company: CompanyKeyMetrics) => formatRatio(company.returnOnTangibleAssetsTTM),
    subTitle:
      "Return on assets is the measure of how effective a company is using its assets",
  },
  {
    label: "Free Cashflow Per Share",
    render: (company: CompanyKeyMetrics) => formatRatio(company.freeCashFlowPerShareTTM),
    subTitle:
      "Return on assets is the measure of how effective a company is using its assets",
  },
  {
    label: "Book Value Per Share TTM",
    render: (company: CompanyKeyMetrics) => formatRatio(company.bookValuePerShareTTM),
    subTitle:
      "Book value per share indicates a firm's net asset value (total assets - total liabilities) on per share basis",
  },
  {
    label: "Divdend Yield TTM",
    render: (company: CompanyKeyMetrics) => formatRatio(company.dividendYieldTTM),
    subTitle: "Shows how much a company pays each year relative to stock price",
  },
  {
    label: "Capex Per Share TTM",
    render: (company: CompanyKeyMetrics) => formatRatio(company.capexPerShareTTM),
    subTitle:
      "Capex is used by a company to aquire, upgrade, and maintain physical assets",
  },
  {
    label: "Graham Number",
    render: (company: CompanyKeyMetrics) => formatRatio(company.grahamNumberTTM),
    subTitle:
      "This is the upperbouind of the price range that a defensive investor should pay for a stock",
  },
  {
    label: "PE Ratio",
    render: (company: CompanyKeyMetrics) => formatRatio(company.peRatioTTM),
    subTitle:
      "This is the upperbouind of the price range that a defensive investor should pay for a stock",
  },
];
...

IncomeStatement

  • IncomeStatement.tsx
const configs = [
  {
    label: "Date",
    render: (company: CompanyIncomeStatement) => company.date,
  },
  {
    label: "Revenue",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.revenue)
  },
  {
    label: "Cost Of Revenue",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.costOfRevenue)
  },
  {
    label: "Depreciation",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.depreciationAndAmortization)
  },
  {
    label: "Operating Income",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.operatingIncome)
  },
  {
    label: "Income Before Taxes",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.incomeBeforeTax)
  },
  {
    label: "Net Income",
    render: (company: CompanyIncomeStatement) => formatLargeMonetaryNumber(company.netIncome)
  },
  {
    label: "Net Income Ratio",
    render: (company: CompanyIncomeStatement) => formatRatio(company.netIncomeRatio)
  },
  {
    label: "Earnings Per Share",
    render: (company: CompanyIncomeStatement) => formatRatio(company.eps)
  },
  {
    label: "Earnings Per Diluted",
    render: (company: CompanyIncomeStatement) => formatRatio(company.epsdiluted)
  },
  {
    label: "Gross Profit Ratio",
    render: (company: CompanyIncomeStatement) => formatRatio(company.grossProfitRatio)
  },
  {
    label: "Opearting Income Ratio",
    render: (company: CompanyIncomeStatement) => formatRatio(company.operatingIncomeRatio)
  },
  {
    label: "Income Before Taxes Ratio",
    render: (company: CompanyIncomeStatement) => formatRatio(company.incomeBeforeTaxRatio)
  },
];
...
Formatting

Api

  • create Api application as .net Asp Core Web Api with visual studio
Api

SQL Server Management Studio

  • download SQL Server
  • download SQL Server Management Studio
SQL Server Management studio

Models

  • create Model folder in Api project
Models

Stock.cs

using System.ComponentModel.DataAnnotations.Schema;

namespace Api.Models
{
    public class Stock
    {
        public int Id { get; set; }
        public string Symbol { get; set; } = string.Empty;
        public string CompanyName { get; set; } = string.Empty;
        [Column(TypeName="decimal(18, 2)")]
        public decimal Purchase { get; set; }
        [Column(TypeName="decimal(18, 2)")]
        public decimal LastDiv { get; set; }
        public string Industry { get; set; } = string.Empty;
        public long MarketCap { get; set; }
        public List<Comment> Comments { get; set; } = new List<Comment>();
    }
}

Comment.cs

namespace Api.Models
{
    public class Comment
    {
        public int Id { get; set; }
        public string Title { get; set; } = string.Empty;
        public string Content { get; set; } = string.Empty;
        public DateTime CreatedOn { get; set; } = DateTime.Now;
        public int? StockId { get; set; }
        public Stock? Stock { get; set; }
    }
}

Entity Framework

  • open Nuget Package Manager in Api project
  • download Microsoft.EntityFrameworkCore.Design
  • download Microsoft.EntityFrameworkCore.Tools
  • download Microsoft.EntityFrameworkCore.SqlServer
Entity Framework

SQL Server Management Studio

  • create new finshark database
Entity Framework

DBContext

  • create Data folder in Api project
  • create ApplicationDBContext.cs in Data folder

  • ApplicationDBContext.cs
using Api.Models;
using Microsoft.EntityFrameworkCore;

namespace Api.Data
{
    public class ApplicationDBContext : DbContext
    {
        public ApplicationDBContext(DbContextOptions dbContextOptions): base(dbContextOptions)
        {
        }

        public DbSet<Stock> Stocks { get; set; }
        public DbSet<Comment> Comments { get; set; }
    }
}

Program

  • Program.cs
...
builder.Services.AddDbContext<ApplicationDBContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});

var app = builder.Build();
...
  • appsettigs.json
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=YOUR_DESKTOP;Initial Catalog=finshark;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  },
  ...
}

Database

  • open Developer Power Shell in visual studio
  • type doenet ef migrations add init
  • type doenet ef database update
Entity Framework

SQL Server Management Studio

  • right click dbo.Stocks table in finshark database
  • click Edit Top 200 Rows
  • insert some values
Controllers

Stock

Controller

  • create StockController.cs in Controllers folder

  • StockController.cs

using Api.Dtos.Stock;
using Api.Interfaces;
using Api.Mappers;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
{
    [Route("api/stock")]
    [ApiController]
    public class StockController : ControllerBase
    {
        private readonly IStockRepository _stockRepository;

        public StockController(IStockRepository stockRepository)
        {
            _stockRepository = stockRepository;
        }

        [HttpGet]
        public async Task<IActionResult> GetAll()
        {
            var stock = await _stockRepository.GetAllAsync();
            var stockDto = stock.Select(s => s.ToStockDto());

            return Ok(stock);
        }

        [HttpGet]
        [Route("{id}")]
        public async Task<IActionResult> GetById([FromRoute] int id)
        {
            var stock = await _stockRepository.GetByIdAsync(id);

            if (stock == null)
            {
                return NotFound();
            }

            return Ok(stock.ToStockDto());
        }

        [HttpPost]
        public async Task<IActionResult> Create([FromBody] CreateStockRequestDto stockDto)
        {
            var stockModel = stockDto.ToStockFromCreateDto();
            await _stockRepository.CreateAsync(stockModel);

            return CreatedAtAction(nameof(GetById), new { id = stockModel.Id}, stockModel.ToStockDto());
        }

        [HttpPut]
        [Route("{id}")]
        public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateStockRequestDto updateDto)
        {
            var stockModel = await _stockRepository.UpdateAsync(id, updateDto);

            if (stockModel == null)
            {
                return NotFound();
            }

            return Ok(stockModel.ToStockDto());
        }

        [HttpDelete]
        [Route("{id}")]
        public async Task<IActionResult> Delete([FromRoute] int id)
        {
            var stockModel = await _stockRepository.DeleteAsync(id);

            if (stockModel == null)
            {
                return NotFound();
            }

            return NoContent();
        }
    }
}

DTOs

  • create Dtos folder in Api folder
  • create Stock folder in Dtos folder
  • create StockDto.cs in Stock folder

  • StockDto.cs
using Api.Dtos.Comment;

namespace Api.Dtos.Stock
{
    public class StockDto
    {
        public int Id { get; set; }
        public string Symbol { get; set; } = string.Empty;
        public string CompanyName { get; set; } = string.Empty;
        public decimal Purchase { get; set; }
        public decimal LastDiv { get; set; }
        public string Industry { get; set; } = string.Empty;
        public long MarketCap { get; set; }
        public List<CommentDto>? Comments { get; set; }
    }
}
  • create CreateStockRequestDto.cs in Stock folder

  • CreateStockRequestDto.cs

namespace Api.Dtos.Stock
{
    public class CreateStockRequestDto
    {
        public string Symbol { get; set; } = string.Empty;
        public string CompanyName { get; set; } = string.Empty;
        public decimal Purchase { get; set; }
        public decimal LastDiv { get; set; }
        public string Industry { get; set; } = string.Empty;
        public long MarketCap { get; set; }
    }
}
  • create UpdateStockRequestDto.cs in Stock folder

  • UpdateStockRequestDto.cs

namespace Api.Dtos.Stock
{
    public class UpdateStockRequestDto
    {
        public string Symbol { get; set; } = string.Empty;
        public string CompanyName { get; set; } = string.Empty;
        public decimal Purchase { get; set; }
        public decimal LastDiv { get; set; }
        public string Industry { get; set; } = string.Empty;
        public long MarketCap { get; set; }
    }
}

Mapper

  • create Mappers folder in Api folder
  • create StockMappers.cs in Mappers folder

  • StockMappers.cs
using Api.Dtos.Stock;
using Api.Models;

namespace Api.Mappers
{
    public static class StockMappers
    {
        public static StockDto ToStockDto(this Stock stockModel)
        {
            return new StockDto
            {
                Id = stockModel.Id,
                Symbol = stockModel.Symbol,
                CompanyName = stockModel.CompanyName,
                Purchase = stockModel.Purchase,
                LastDiv = stockModel.LastDiv,
                Industry = stockModel.Industry,
                MarketCap = stockModel.MarketCap,
                Comments = stockModel.Comments.Select(c => c.ToCommentDto()).ToList(),
            };
        }

        public static Stock ToStockFromCreateDto(this CreateStockRequestDto stockDto)
        {
            return new Stock
            {
                Symbol = stockDto.Symbol,
                CompanyName = stockDto.CompanyName,
                Purchase = stockDto.Purchase,
                LastDiv = stockDto.LastDiv,
                Industry = stockDto.Industry,
                MarketCap = stockDto.MarketCap
            };
        }
    }
}

Interfaces

  • create Interfaces folder in Api folder
  • create IStockRepository.cs in Interfaces folder

  • IStockRepository.cs
using Api.Dtos.Stock;
using Api.Models;

namespace Api.Interfaces
{
    public interface IStockRepository
    {
        Task<List<Stock>> GetAllAsync();
        Task<Stock?> GetByIdAsync(int id);
        Task<Stock?> CreateAsync(Stock stockModel);
        Task<Stock?> UpdateAsync(int id, UpdateStockRequestDto stockModel);
        Task<Stock?> DeleteAsync(int id);
        Task<bool> StockExists(int id);
    }
}

Repository

  • create Repository folder in Api folder
  • create StockRepository.cs in Repository folder

  • StockRepository.cs
using Api.Data;
using Api.Dtos.Stock;
using Api.Interfaces;
using Api.Models;
using Microsoft.EntityFrameworkCore;

namespace Api.Repository
{
    public class StockRepository : IStockRepository
    {
        private readonly ApplicationDBContext _context;

        public StockRepository(ApplicationDBContext context)
        {
            _context = context;
        }

        public async Task<List<Stock>> GetAllAsync()
        {
            return await _context.Stocks.Include(c => c.Comments).ToListAsync();
        }

        public async Task<Stock?> GetByIdAsync(int id)
        {
            return await _context.Stocks.FirstOrDefaultAsync(x => x.Id == id);
        }

        public async Task<bool> StockExists(int id)
        {
            return await _context.Stocks.AnyAsync(s => s.Id == id);
        }

        public async Task<Stock?> CreateAsync(Stock stockModel)
        {
            await _context.Stocks.AddAsync(stockModel);
            await _context.SaveChangesAsync();
            return stockModel;
        }

        public async Task<Stock?> UpdateAsync(int id, UpdateStockRequestDto stockModel)
        {
            var existingStock = await _context.Stocks.FirstOrDefaultAsync(x => x.Id == id);

            if (existingStock == null)
            {
                return null;
            }

            existingStock.Symbol = stockModel.Symbol;
            existingStock.CompanyName = stockModel.CompanyName;
            existingStock.Purchase = stockModel.Purchase;
            existingStock.LastDiv = stockModel.LastDiv;
            existingStock.MarketCap = stockModel.MarketCap;

            await _context.SaveChangesAsync();
            return existingStock;
        }

        public async Task<Stock?> DeleteAsync(int id)
        {
            var stockModel = await _context.Stocks.FirstOrDefaultAsync(x => x.Id == id);

            if (stockModel == null)
            {
                return null;
            }

            _context.Stocks.Remove(stockModel);
            await _context.SaveChangesAsync();
            return stockModel;
        }
    }
}

Program

  • Program.cs
...
builder.Services.AddScoped<IStockRepository, StockRepository>();
...
Stock

Comment

Controller

  • create CommentController.cs in Controllers folder

  • CommentController.cs

using Api.Dtos.Comment;
using Api.Interfaces;
using Api.Mappers;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
{
    [Route("api/comment")]
    [ApiController]
    public class CommentController : ControllerBase
    {
        private readonly ICommentRepository _commentRepository;
        private readonly IStockRepository _stockRepository;

        public CommentController(ICommentRepository commentRepository, IStockRepository stockRepository)
        {
            _commentRepository = commentRepository;
            _stockRepository = stockRepository;
        }

        [HttpGet]
        public async Task<IActionResult> GetAll()
        {
            var comments = await _commentRepository.GetAllAsync();

            var commentDto = comments.Select(s => s.ToCommentDto());

            return Ok(commentDto);
        }

        [HttpGet]
        [Route("{id}")]
        public async Task<IActionResult> GetById([FromRoute] int id)
        {
            var comment = await _commentRepository.GetByIdAsync(id);

            if (comment == null)
            {
                return NotFound();
            }

            return Ok(comment.ToCommentDto());
        }

        [HttpPost]
        [Route("{stockId}")]
        public async Task<IActionResult> Create([FromRoute] int stockId, [FromBody] CreateCommentDto commentDto)
        {
            if (!await _stockRepository.StockExists(stockId))
            {
                return BadRequest("Stock does not exist");
            }

            var commentModel = commentDto.ToCommentFromCreate(stockId);
            await _commentRepository.CreateAsync(commentModel);

            return CreatedAtAction(nameof(GetById), new { id = commentModel.Id }, commentModel.ToCommentDto());
        }

        [HttpPut]
        [Route("{id}")]
        public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateCommentRequestDto updateDto)
        {
            var comment = await _commentRepository.UpdateAsync(id, updateDto.ToCommentFromUpdate());

            if (comment == null)
            {
                return NotFound("Comment not found");
            }

            return Ok(comment.ToCommentDto());
        }

        [HttpDelete]
        [Route("{id}")]
        public async Task<IActionResult> Delete([FromRoute] int id)
        {
            var commentModel = await _commentRepository.DeleteAsync(id);

            if (commentModel == null)
            {
                return NotFound("Comment does not exist");
            }

            return Ok(commentModel);
        }
    }
}

DTOs

  • create Stock folder in Dtos folder
  • create CommentDto.cs in Comment folder

  • StockDto.cs
namespace Api.Dtos.Comment
{
    public class CommentDto
    {
        public int Id { get; set; }
        public string Title { get; set; } = string.Empty;
        public string Content { get; set; } = string.Empty;
        public DateTime CreatedOn { get; set; } = DateTime.Now;
        public int? StockId { get; set; }
    }
}
  • create CreateCommentRequestDto.cs in Comment folder

  • CreateCommentRequestDto.cs

namespace Api.Dtos.Comment
{
    public class CreateCommentRequestDto
    {
        public string Title { get; set; } = string.Empty;
        public string Content { get; set; } = string.Empty;
    }
}
  • create UpdateCommentRequestDto.cs in Comment folder

  • UpdateCommentRequestDto.cs

namespace Api.Dtos.Comment
{
    public class UpdateCommentRequestDto
    {
        public string Title { get; set; } = string.Empty;
        public string Content { get; set; } = string.Empty;
    }
}

Mapper

  • create CommentMapper.cs in Mappers folder

  • CommentMapper.cs

using Api.Dtos.Comment;
using Api.Models;

namespace Api.Mappers
{
    public static class CommentMapper
    {
        public static CommentDto ToCommentDto(this Comment commentModel)
        {
            return new CommentDto
            {
                Id = commentModel.Id,
                Title = commentModel.Title,
                Content = commentModel.Content,
                CreatedOn = commentModel.CreatedOn,
                StockId = commentModel.StockId
            };
        }

        public static Comment ToCommentFromCreate(this CreateCommentRequestDto commentDto, int stockId)
        {
            return new Comment
            {
                Title = commentDto.Title,
                Content = commentDto.Content,
                StockId = stockId
            };
        }

        public static Comment ToCommentFromUpdate(this UpdateCommentRequestDto commentDto)
        {
            return new Comment
            {
                Title = commentDto.Title,
                Content = commentDto.Content
            };
        }
    }
}

Interfaces

  • create ICommentRepository.cs in Interfaces folder

  • ICommentRepository.cs

using Api.Models;

namespace Api.Interfaces
{
    public interface ICommentRepository
    {
        Task<List<Comment>> GetAllAsync();
        Task<Comment?> GetByIdAsync(int id);
        Task<Comment> CreateAsync(Comment commentModel);
        Task<Comment?> UpdateAsync(int id, Comment commentModel);
        Task<Comment?> DeleteAsync(int id);
    }
}

Repository

  • create CommentRepository.cs in Repository folder

  • CommentRepository.cs

using Api.Data;
using Api.Interfaces;
using Api.Models;
using Microsoft.EntityFrameworkCore;

namespace Api.Repository
{
    public class CommentRepository : ICommentRepository
    {
        private readonly ApplicationDBContext _context;

        public CommentRepository(ApplicationDBContext context)
        {
            _context = context;
        }

        public async Task<List<Comment>> GetAllAsync()
        {
            return await _context.Comments.ToListAsync();
        }

        public async Task<Comment?> GetByIdAsync(int id)
        {
            return await _context.Comments.FirstOrDefaultAsync(x => x.Id == id);
        }

        public async Task<Comment> CreateAsync(Comment commentModel)
        {
            await _context.Comments.AddAsync(commentModel);
            await _context.SaveChangesAsync();

            return commentModel;
        }

        public async Task<Comment?> UpdateAsync(int id, Comment commentModel)
        {
            var existingComment = await _context.Comments.FirstOrDefaultAsync(x => x.Id == id);

            if (existingComment == null)
            {
                return null;
            }

            existingComment.Title = commentModel.Title;
            existingComment.Content = commentModel.Content;

            await _context.SaveChangesAsync();
            return existingComment;
        }

        public async Task<Comment?> DeleteAsync(int id)
        {
            var commentModel = await _context.Comments.FirstOrDefaultAsync(x => x.Id == id);

            if (commentModel == null)
            {
                return null;
            }

            _context.Comments.Remove(commentModel);
            await _context.SaveChangesAsync();

            return commentModel;
        }
    }
}

Program

  • Program.cs
...
builder.Services.AddScoped<IStockRepository, StockRepository>();
builder.Services.AddScoped<ICommentRepository, CommentRepository>();
...
Comment

Data Validation

Controller Validation

  • You can set route’s type with :{type}
  • If you use data annotation in Dtos, you can alert to user with error message by ModelState

  • StockController.cs
...
[HttpGet]
public async Task<IActionResult> GetAll()
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stock = await _stockRepository.GetAllAsync();
    var stockDto = stock.Select(s => s.ToStockDto());

    return Ok(stock);
}

[HttpGet]
[Route("{id:int}")]
public async Task<IActionResult> GetById([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stock = await _stockRepository.GetByIdAsync(id);

    if (stock == null)
    {
        return NotFound();
    }

    return Ok(stock.ToStockDto());
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateStockRequestDto stockDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stockModel = stockDto.ToStockFromCreateDto();
    await _stockRepository.CreateAsync(stockModel);

    return CreatedAtAction(nameof(GetById), new { id = stockModel.Id}, stockModel.ToStockDto());
}

[HttpPut]
[Route("{id:int}")]
public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateStockRequestDto updateDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stockModel = await _stockRepository.UpdateAsync(id, updateDto);

    if (stockModel == null)
    {
        return NotFound();
    }

    return Ok(stockModel.ToStockDto());
}

[HttpDelete]
[Route("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stockModel = await _stockRepository.DeleteAsync(id);

    if (stockModel == null)
    {
        return NotFound();
    }

    return NoContent();
}
  • CommentController.cs
...
[HttpGet]
public async Task<IActionResult> GetAll()
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var comments = await _commentRepository.GetAllAsync();
    var commentDto = comments.Select(s => s.ToCommentDto());

    return Ok(commentDto);
}

[HttpGet]
[Route("{id:int}")]
public async Task<IActionResult> GetById([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var comment = await _commentRepository.GetByIdAsync(id);

    if (comment == null)
    {
        return NotFound();
    }

    return Ok(comment.ToCommentDto());
}

[HttpPost]
[Route("{stockId:int}")]
public async Task<IActionResult> Create([FromRoute] int stockId, [FromBody] CreateCommentRequestDto commentDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (!await _stockRepository.StockExists(stockId))
    {
        return BadRequest("Stock does not exist");
    }

    var commentModel = commentDto.ToCommentFromCreate(stockId);
    await _commentRepository.CreateAsync(commentModel);

    return CreatedAtAction(nameof(GetById), new { id = commentModel.Id }, commentModel.ToCommentDto());
}

[HttpPut]
[Route("{id:int}")]
public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateCommentRequestDto updateDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var comment = await _commentRepository.UpdateAsync(id, updateDto.ToCommentFromUpdate());

    if (comment == null)
    {
        return NotFound("Comment not found");
    }

    return Ok(comment.ToCommentDto());
}

[HttpDelete]
[Route("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var commentModel = await _commentRepository.DeleteAsync(id);

    if (commentModel == null)
    {
        return NotFound("Comment does not exist");
    }

    return Ok(commentModel);
}

Data Annotations

  • CreateCommentRequestDto.cs
public class CreateCommentRequestDto
{
    [Required]
    [MinLength(5,ErrorMessage = "Title must be 5 chaaracters")]
    [MaxLength(280, ErrorMessage = "Title cannot be over 280 characters")]
    public string Title { get; set; } = string.Empty;
    [Required]
    [MinLength(5, ErrorMessage = "Content must be 5 chaaracters")]
    [MaxLength(280, ErrorMessage = "Content cannot be over 280 characters")]
    public string Content { get; set; } = string.Empty;
}
  • UpdateCommentRequestDto.cs
public class UpdateCommentRequestDto
{
    [Required]
    [MinLength(5, ErrorMessage = "Title must be 5 chaaracters")]
    [MaxLength(280, ErrorMessage = "Title cannot be over 280 characters")]
    public string Title { get; set; } = string.Empty;
    [Required]
    [MinLength(5, ErrorMessage = "Content must be 5 chaaracters")]
    [MaxLength(280, ErrorMessage = "Content cannot be over 280 characters")]
    public string Content { get; set; } = string.Empty;
}
  • CreateStockRequestDto.cs
public class CreateStockRequestDto
{
    [Required]
    [MaxLength(10, ErrorMessage = "Symbol cannot be over 10 characters")]
    public string Symbol { get; set; } = string.Empty;
    [Required]
    [MaxLength(10, ErrorMessage = "Company cannot be over 10 characters")]
    public string CompanyName { get; set; } = string.Empty;
    [Required]
    [Range(1, 1000000000)]
    public decimal Purchase { get; set; }
    [Required]
    [Range(0.001, 100)]
    public decimal LastDiv { get; set; }
    [Required]
    [MaxLength(10, ErrorMessage = "Industry cannot be over 10 characters")]
    public string Industry { get; set; } = string.Empty;
    [Required]
    [Range(1, 5000000000)]
    public long MarketCap { get; set; }
}
  • UpdateStockRequestDto.cs
public class UpdateStockRequestDto
{
    [Required]
    [MaxLength(10, ErrorMessage = "Symbol cannot be over 10 characters")]
    public string Symbol { get; set; } = string.Empty;
    [Required]
    [MaxLength(10, ErrorMessage = "Company cannot be over 10 characters")]
    public string CompanyName { get; set; } = string.Empty;
    [Required]
    [Range(1, 1000000000)]
    public decimal Purchase { get; set; }
    [Required]
    [Range(0.001, 100)]
    public decimal LastDiv { get; set; }
    [Required]
    public string Industry { get; set; } = string.Empty;
    [Required]
    [Range(1, 5000000000)]
    public long MarketCap { get; set; }
}
Data Validation

Filtering

Program

  • Download Newtonsoft.Json in Nuget Package Manager
  • Download Microsoft.AspNetCore.Mvc.NewtonsoftJson in Nuget Package Manager

  • Program.cs
...
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    });
...

Program

  • Download Newtonsoft.Json in Nuget Package Manager
  • Download Microsoft.AspNetCore.Mvc.NewtonsoftJson in Nuget Package Manager

  • Program.cs
...
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    });
...

Helpers

  • create Helpers folder in Api folder
  • create QueryObject.cs in Helpers folder

  • QueryObject.cs
namespace Api.Helpers
{
    public class QueryObject
    {
        public string? Symbol { get; set; } = null;
        public string? CompanyName { get; set; } = null;
        public string? SortBy { get; set; } = null;
        public bool IsDecsending { get; set; } = false;
        public int PageNumber { get; set; } = 1;
        public int PageSize { get; set; } = 20;
    }
}

Interfaces

  • IStockRepository.cs
Task<List<Stock>> GetAllAsync(QueryObject query);
...

Repository

  • StockRepository.cs
public async Task<List<Stock>> GetAllAsync(QueryObject query)
{
    var stocks = _context.Stocks.Include(c => c.Comments).AsQueryable();

    if (!string.IsNullOrWhiteSpace(query.CompanyName))
    {
        stocks = stocks.Where(s => s.CompanyName.Contains(query.CompanyName));
    }

    if (!string.IsNullOrWhiteSpace(query.Symbol)) 
    {
        stocks = stocks.Where(s => s.Symbol.Contains(query.Symbol));
    }

    if (!string.IsNullOrWhiteSpace(query.SortBy))
    {
        if (query.SortBy.Equals("Symbol", StringComparison.OrdinalIgnoreCase))
        {
            stocks = query.IsDecsending ? stocks.OrderByDescending(s => s.Symbol) : stocks.OrderBy(s => s.Symbol);
        }
    }

    var skipNumber = (query.PageNumber - 1) * query.PageSize;

    return await stocks.Skip(skipNumber).Take(query.PageSize).ToListAsync();
}
...

Controller

  • StockController.cs
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] QueryObject query)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stock = await _stockRepository.GetAllAsync(query);
    var stockDto = stock.Select(s => s.ToStockDto());

    return Ok(stock);
}
...
Filtering

Identify

IdentityDbContext

  • is a specialized DbContext provided by the Entity Framework (EF) to handle authentication and authorization.
  • includes several built-in DbSets (tables) that support the default identity functionality, like users, roles and claims.

IdentityRole

  • is the built-in role model from ASP.NET Core Identity, representing user roles in the application.
  • allows to define roles like “Admin”, “User” and etc.

Authentication

  • in an ASP.NET Core application can use JWT (JSON Web Token) Bearer Authentication.
  • JwtBearerDefaults.AuthenticationScheme uses the JWT Bearer token approach.

TokenValidationParameters

  • ensures that only valid and authorized tokens are accepted.

app.useAuthentication()

  • is a middleware that enables the authentication system within the request pipeline.
  • should be placed before app.UseAuthorization()

Nuget Packages

  • Download Microsoft.AspNetCore.Authentication.JwtBearer in Nuget Package Manager
  • Download Microsoft.AspNetCore.Identity.EntityFrameworkCore in Nuget Package Manager
  • Download Microsoft.Extensions.Identity.Core in Nuget Package Manager

Data

  • ApplicationDBContext.cs
public class ApplicationDBContext : IdentityDbContext<AppUser>
...

Models

  • create AppUser.cs in Models folder

  • AppUser.cs

using Microsoft.AspNetCore.Identity;

namespace Api.Models
{
    public class AppUser : IdentityUser
    {

    }
}

appsettigs

  • appsettigs.json
...
"JWT": {
  "Issuer": "http://localhost:5246",
  "Audience": "http://localhost:5246",
  "SigningKey":  "swordrish"
}

Program

  • Program.cs
...
builder.Services.AddIdentity<AppUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequireUppercase = true;
    options.Password.RequireNonAlphanumeric = true;
    options.Password.RequiredLength = 12;
}).AddEntityFrameworkStores<ApplicationDBContext>();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme =
    options.DefaultChallengeScheme =
    options.DefaultForbidScheme =
    options.DefaultScheme =
    options.DefaultSignInScheme =
    options.DefaultSignOutScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["JWT:Issuer"],
        ValidateAudience = true,
        ValidAudience = builder.Configuration["JWT:Audience"],
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            System.Text.Encoding.UTF8.GetBytes(builder.Configuration["JWT:SigningKey"]))
    };
});
...
app.UseAuthentication();
...

Database

  • type dotnet ef migrations add Identity in developer power shell
  • type dotnet ef database update in developer power shell
Identify

Registration

UserManager

  • provides a high-level API for managing user-related operations in applications that use ASP.NET Identity for authentication and authorization.

OnModelCreating

  • provides a place to customize the model created by Entity Framework Core before generateing the database.

DTOs

  • create RegisterDto.cs in Dtos folder

  • RegisterDto.cs

using System.ComponentModel.DataAnnotations;

namespace Api.Dtos.Account
{
    public class RegisterDto
    {
        [Required]
        public string? UserName { get; set; }
        [Required]
        [EmailAddress]
        public string? Email { get; set; }
        [Required]
        public string? Password { get; set; }
    }
}

Controller

  • create AccountController.cs in Controllers folder

  • AccountController.cs

using Api.Dtos.Account;
using Api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
{
    [Route("api/account")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        private readonly UserManager<AppUser> _userManager;

        public AccountController(UserManager<AppUser> userManager)
        {
            _userManager = userManager;   
        }

        [HttpPost("register")]
        public async Task<IActionResult> Register([FromBody] RegisterDto registerDto)
        {
            try
            {
                if (!ModelState.IsValid) 
                { 
                    return BadRequest(ModelState);
                }

                var appUser = new AppUser
                {
                    UserName = registerDto.UserName,
                    Email = registerDto.Email
                };

                var createUser = await _userManager.CreateAsync(appUser, registerDto.Password);

                if (createUser.Succeeded) 
                {
                    var roleResult = await _userManager.AddToRoleAsync(appUser, "User");
                    if (roleResult.Succeeded)
                    {
                        return Ok("User created");
                    }
                    else
                    {
                        return StatusCode(500, roleResult.Errors);
                    }
                }
                else
                {
                    return StatusCode(500, createUser.Errors);
                }
            }
            catch (Exception e)
            {
                return StatusCode(500, e);
            }
        }
    }
}

ApplicationDBContext

  • ApplicationDBContext.cs
...
protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    List<IdentityRole> roles = new List<IdentityRole>
    {
        new IdentityRole
        {
            Name = "Admin",
            NormalizedName = "ADMIN"
        },
        new IdentityRole
        {
            Name = "User",
            NormalizedName = "USER"
        }
    };

    builder.Entity<IdentityRole>().HasData(roles);
}

Database

  • type dotnet ef migrations add SeedRole in developer power shell
  • type dotnet ef database update in developer power shell
Registration

Token

JWT (Json Web Token)

  • is used in applications to securely transmit information for stateless, secure and cross-platform authentication.
  • allows applications to manage user sessions without relying on centralized server-side session.
  • you can decode your JWT in https://jwt.io/

DTOs

  • create NewUserDto.cs in Dtos folder

  • NewUserDto.cs

namespace Api.Dtos.Account
{
    public class NewUserDto
    {
        public string UserName { get; set; }
        public string Email { get; set; }
        public string Token { get; set; }
    }
}

Interfaces

  • create ITokenService.cs in Interfaces folder

  • ITokenService.cs

using Api.Models;

namespace Api.Interfaces
{
    public interface ITokenService
    {
        string CreateToken(AppUser appUser);
    }
}

Services

  • create Services folder
  • create TokenService.cs in Services folder

  • TokenService.cs
using Api.Interfaces;
using Api.Models;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Api.Service
{
    public class TokenService : ITokenService
    {
        private readonly IConfiguration _configuration;
        private readonly SymmetricSecurityKey _symmetricSecurityKey;

        public TokenService(IConfiguration configuration)
        {
            _configuration = configuration;
            _symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JWT:SigningKey"]));
        }

        public string CreateToken(AppUser appUser)
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Email, appUser.Email),
                new Claim(JwtRegisteredClaimNames.GivenName, appUser.UserName)
            };

            var signingCredentials = new SigningCredentials(_symmetricSecurityKey, SecurityAlgorithms.HmacSha512Signature);
            var securityTokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.Now.AddDays(7),
                SigningCredentials = signingCredentials,
                Issuer = _configuration["JWT:Issuer"],
                Audience = _configuration["JWT:Audience"]
            };

            var jwtSecutiryTokenHandler = new JwtSecurityTokenHandler();

            var token = jwtSecutiryTokenHandler.CreateToken(securityTokenDescriptor);

            return jwtSecutiryTokenHandler.WriteToken(token);
        }
    }
}

Controllers

  • AccountController.cs
private readonly UserManager<AppUser> _userManager;
private readonly ITokenService _tokenService;

public AccountController(UserManager<AppUser> userManager, ITokenService tokenService)
{
    _userManager = userManager;   
    _tokenService = tokenService;
}
...
        if (roleResult.Succeeded)
        {
            return Ok(
                new NewUserDto
                {
                    UserName = appUser.UserName,
                    Email = appUser.Email,
                    Token = _tokenService.CreateToken(appUser)
                });
        }

Program

  • Program.cs
...
builder.Services.AddScoped<ITokenService, TokenService>();
...

appsettigs

  • appsettigs.json
...
"SigningKey":  "iamgoodiamcooliambeautifuliamprettyiamwonderfuliamgeniousiamcleveriamthebest"
...
Token

Login

DTOs

  • create LoginDto.cs in Dtos folder

  • LoginDto.cs

using System.ComponentModel.DataAnnotations;

namespace Api.Dtos.Account
{
    public class LoginDto
    {
        [Required]
        public string UserName { get; set; }
        [Required]
        public string Password { get; set; }
    }
}

Program

  • Program.cs
...
builder.Services.AddSwaggerGen(option =>
{
    option.SwaggerDoc("v1", new OpenApiInfo { Title = "Demo API", Version = "v1" });
    option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Description = "Please enter a valid token",
        Name = "Authorization",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "Bearer"
    });
    option.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            new string[]{}
        }
    });
});
...

Controllers

  • AccountController.cs
public class AccountController : ControllerBase
{
    private readonly UserManager<AppUser> _userManager;
    private readonly ITokenService _tokenService;
    private readonly SignInManager<AppUser> _signInManager;

    public AccountController(UserManager<AppUser> userManager, ITokenService tokenService, SignInManager<AppUser> signInManager)
    {
        _userManager = userManager;   
        _tokenService = tokenService;
        _signInManager = signInManager;
    }

    [HttpPost("login")]
    public async Task<IActionResult> Login(LoginDto loginDto)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        var user = await _userManager.Users.FirstOrDefaultAsync(u => u.UserName == loginDto.UserName.ToLower());

        if (user == null)
        {
            return Unauthorized("Invalid username!");
        }

        var result = await _signInManager.CheckPasswordSignInAsync(user, loginDto.Password, false);

        if (!result.Succeeded) 
        {
            return Unauthorized("Username not found/or password incorrect");
        }

        return Ok(
            new NewUserDto
            {
                UserName = user.UserName,
                Email = user.Email,
                Token = _tokenService.CreateToken(user)
            });
    }
...
  • CommentController.cs
public class AccountController : ControllerBase
...
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll()
...
[HttpGet]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> GetById([FromRoute] int id)
...
[HttpPost]
[Authorize]
[Route("{stockId:int}")]
public async Task<IActionResult> Create([FromRoute] int stockId, [FromBody] CreateCommentRequestDto commentDto)
...
[HttpPut]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateCommentRequestDto updateDto)
...
[HttpDelete]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id)
...
  • StockController.cs
public class AccountController : ControllerBase
...
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll([FromQuery] QueryObject query)
...
[HttpGet]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> GetById([FromRoute] int id)
...
[HttpPost]
[Authorize]
public async Task<IActionResult> Create([FromBody] CreateStockRequestDto stockDto)
...
[HttpPut]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> Update([FromRoute] int id, [FromBody] UpdateStockRequestDto updateDto)
...
[HttpDelete]
[Authorize]
[Route("{id:int}")]
public async Task<IActionResult> Delete([FromRoute] int id)
...
Token

Portfolio

Database Relationship

  • means like n:n, 1:n, n:1 and 1:1.
  • HasOne sets up 1:1 or 1:n relationships between entities.
  • WithMany sets up n:1 or n:n relationships between entities.
  • HasKey sets a primary key from a composite key in an entity
  • HasForeignKey sets a foreign key to refer an entity

Models

  • create Portfolio.cs in Models folder

  • Portfolio.cs

using System.ComponentModel.DataAnnotations.Schema;

namespace Api.Models
{
    [Table("Portfolios")]
    public class Portfolio
    {
        public string AppUserId { get; set; }
        public int StockId { get; set; }
        public AppUser AppUser { get; set; }
        public Stock Stock { get; set; }
    }
}
  • AppUser.cs
public class AppUser : IdentityUser
{
    public List<Portfolio> Portfolios { get; set; } = new List<Portfolio>();
}
  • Comment.cs
[Table("Comments")]
public class Comment
{
...
  • Stock.cs
using System.ComponentModel.DataAnnotations.Schema;

namespace Api.Models
{
    [Table("Stocks")]
    public class Stock
    {
        public int Id { get; set; }
        public string Symbol { get; set; } = string.Empty;
        public string CompanyName { get; set; } = string.Empty;
        [Column(TypeName="decimal(18, 2)")]
        public decimal Purchase { get; set; }
        [Column(TypeName="decimal(18, 2)")]
        public decimal LastDiv { get; set; }
        public string Industry { get; set; } = string.Empty;
        public long MarketCap { get; set; }
        public List<Comment> Comments { get; set; } = new List<Comment>();
        public List<Portfolio> Portfolios { get; set; } = new List<Portfolio>();
    }
}

Data

  • ApplicationDBContext.cs
...
public DbSet<Portfolio> Portfolios { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<Portfolio>(x => x.HasKey(p => new { p.AppUserId, p.StockId }));

    builder.Entity<Portfolio>()
      .HasOne(p => p.AppUser)
      .WithMany(u => u.Portfolios)
      .HasForeignKey(p => p.AppUserId);

    builder.Entity<Portfolio>()
      .HasOne(p => p.Stock)
      .WithMany(s => s.Portfolios)
      .HasForeignKey(p => p.StockId);
...

Extensions

  • create Extensions folder in Api folder
  • create ClaimsExtensions.cs in Extensions folder

  • ClaimsExtensions.cs
using System.Security.Claims;

namespace Api.Extensions
{
    public static class ClaimsExtensions
    {
        public static string GetUsername(this ClaimsPrincipal user)
        {
            return user.Claims.SingleOrDefault(x => x.Type.Equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname")).Value;
        }
    }
}

Interfaces

  • create IPortfolioRepository.cs in Interfaces folder

  • IPortfolioRepository.cs

using Api.Models;

namespace Api.Interfaces
{
    public interface IPortfolioRepository
    {
        Task<List<Stock>> GetUserPortfolio(AppUser appUser);
        Task<Portfolio> CreateAsync (Portfolio portfolio);
        Task<Portfolio> DeleteAsync (AppUser appUser, string symbol);
    }
}
  • IStockRepository.cs
...
Task<Stock?>  GetBySymbolAsync(string symbol);
...

Repository

  • create PortfolioRepository.cs in Repository folder

  • IPortfolioRepository.cs

using Api.Data;
using Api.Interfaces;
using Api.Models;
using Microsoft.EntityFrameworkCore;

namespace Api.Repository
{
    public class PortfolioRepository : IPortfolioRepository
    {
        private readonly ApplicationDBContext _context;

        public PortfolioRepository(ApplicationDBContext context)
        {
            _context = context;
        }

        public async Task<List<Stock>> GetUserPortfolio(AppUser appUser)
        {
            return await _context.Portfolios.Where(u => u.AppUserId == appUser.Id)
                .Select(stock => new Stock
                {
                    Id = stock.StockId,
                    Symbol = stock.Stock.Symbol,
                    CompanyName = stock.Stock.CompanyName,
                    Purchase = stock.Stock.Purchase,
                    LastDiv = stock.Stock.LastDiv,
                    MarketCap = stock.Stock.MarketCap
                }).ToListAsync();
        }

        public async Task<Portfolio> CreateAsync(Portfolio portfolio)
        {
            await _context.Portfolios.AddAsync(portfolio);
            await _context.SaveChangesAsync();
            return portfolio;
        }

        public async Task<Portfolio> DeleteAsync(AppUser appUser, string symbol)
        {
            var portfolioModel = await _context.Portfolios.FirstOrDefaultAsync(x => x.AppUserId == appUser.Id 
                && x.Stock.Symbol.ToLower() == symbol.ToLower());
            
            if (portfolioModel == null)
            {
                return null;
            }

            _context.Portfolios.Remove(portfolioModel);
            await _context.SaveChangesAsync();

            return portfolioModel;
        }
    }
}
  • StockRepository.cs
...
 public async Task<Stock?> GetBySymbolAsync(string symbol)
 {
     return await _context.Stocks.FirstOrDefaultAsync(x => x.Symbol == symbol);
 }
...

Program

  • Program.cs
...
builder.Services.AddScoped<IPortfolioRepository, PortfolioRepository>();
...

PortfolioController

  • PortfolioController.cs
using Api.Extensions;
using Api.Interfaces;
using Api.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace Api.Controllers
{
    [Route("api/portfolio")]
    [ApiController]
    public class PortfolioController : ControllerBase
    {
        private readonly UserManager<AppUser> _userManager;
        private readonly IStockRepository _stockRepository;
        private readonly IPortfolioRepository _portfolioRepository;

        public PortfolioController(UserManager<AppUser> userManager, IStockRepository stockRepository, IPortfolioRepository portfolioRepository)
        {
            _userManager = userManager;
            _stockRepository = stockRepository;
            _portfolioRepository = portfolioRepository;
        }

        [HttpGet]
        [Authorize]
        public async Task<IActionResult> GetUserPortfolio()
        {
            var username = User.GetUsername();
            var appUser = await _userManager.FindByNameAsync(username);
            var userPortfolio = await _portfolioRepository.GetUserPortfolio(appUser);

            return Ok(userPortfolio);
        }

        [HttpPost]
        [Authorize]
        public async Task<IActionResult> AddPortfolio(string symbol)
        {
            var username = User.GetUsername();
            var appUser = await _userManager.FindByNameAsync(username);
            var stock = await _stockRepository.GetBySymbolAsync(symbol);

            if (stock == null)
            {
                return BadRequest("Stock not found");
            }

            var userPortfolio = await _portfolioRepository.GetUserPortfolio(appUser);

            if (userPortfolio.Any(e => e.Symbol.ToLower() == symbol.ToLower()))
            {
                return BadRequest("Cannot add same stock to portfolio");
            }

            var portfolioModel = new Portfolio
            {
                StockId = stock.Id,
                AppUserId = appUser.Id
            };

            await _portfolioRepository.CreateAsync(portfolioModel);

            if (portfolioModel == null) 
            {
                return StatusCode(500, "Cloud not create");
            }
            else
            {
                return Created();
            }
        }

        [HttpDelete]
        [Authorize]
        public async Task<IActionResult> DeletePortfolio(string symbol)
        {
            var username = User.GetUsername();
            var appUser = await _userManager.FindByNameAsync(username);
            var userPortfolio = await _portfolioRepository.GetUserPortfolio(appUser);
            
            var filteredStock = userPortfolio.Where(u => u.Symbol.ToLower() == symbol.ToLower()).ToList();

            if (filteredStock.Count() == 1) 
            {
                await _portfolioRepository.DeleteAsync(appUser, symbol);
            }
            else
            {
                return BadRequest("Stock not in your portfolio");
            }

            return Ok();
        }
    }
}

Database

  • type dotnet ef migrations add ManyToMany
  • type dotnet ef database update
Portfolio

Comment

DTOs

  • CommentDto.cs
...
public string CreatedBy { get; set; } = string.Empty;
...

Models

  • Comment.cs
...
public string? AppUserId { get; set; }
public AppUser AppUser { get; set; }
...

Mappers

  • CommentMapper.cs
...
public static CommentDto ToCommentDto(this Comment commentModel)
{
    return new CommentDto
    {
        Id = commentModel.Id,
        Title = commentModel.Title,
        Content = commentModel.Content,
        CreatedOn = commentModel.CreatedOn,
        CreatedBy = commentModel.AppUser.UserName,
        StockId = commentModel.StockId
    };
}
...

Repository

  • CommentRepository.cs
...
public async Task<List<Comment>> GetAllAsync()
{
    return await _context.Comments.Include(c => c.AppUser).ToListAsync();
}

public async Task<Comment?> GetByIdAsync(int id)
{
    return await _context.Comments.Include(c => c.AppUser).FirstOrDefaultAsync(x => x.Id == id);
}
...
  • StockRepository.cs
...
public async Task<List<Stock>> GetAllAsync(QueryObject query)
{
    var stocks = _context.Stocks.Include(c => c.Comments).ThenInclude(a => a.AppUser).AsQueryable();
...

Controllers

  • CommentController.cs
...
private readonly UserManager<AppUser> _userManager;

public CommentController(ICommentRepository commentRepository, IStockRepository stockRepository, UserManager<AppUser> userManager)
{
    _commentRepository = commentRepository;
    _stockRepository = stockRepository;
    _userManager = userManager;
}

...
[HttpPost]
[Authorize]
[Route("{stockId:int}")]
public async Task<IActionResult> Create([FromRoute] int stockId, [FromBody] CreateCommentRequestDto commentDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    if (!await _stockRepository.StockExists(stockId))
    {
        return BadRequest("Stock does not exist");
    }

    var username = User.GetUsername();
    var appUser = await _userManager.FindByNameAsync(username);
    var commentModel = commentDto.ToCommentFromCreate(stockId);
    commentModel.AppUserId = appUser.Id;

    await _commentRepository.CreateAsync(commentModel);

    return CreatedAtAction(nameof(GetById), new { id = commentModel.Id }, commentModel.ToCommentDto());
}
...
  • StockController.cs
...
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll([FromQuery] QueryObject query)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var stock = await _stockRepository.GetAllAsync(query);
    var stockDto = stock.Select(s => s.ToStockDto()).ToList();

    return Ok(stockDto);
}
...

Database

  • type dotnet ef migrations CommentOneToOne
  • tpye dotnet ef database update
Comment

HttpClient

DTOs

  • create FinancialModelingPrepStock.cs in Dtos folder

  • FinancialModelingPrepStock.cs

namespace api.Dtos.Stock
{
    public class FinancialModelingPrepStock
    {
        public string symbol { get; set; }
        public double price { get; set; }
        public double beta { get; set; }
        public int volAvg { get; set; }
        public long mktCap { get; set; }
        public double lastDiv { get; set; }
        public string range { get; set; }
        public double changes { get; set; }
        public string companyName { get; set; }
        public string currency { get; set; }
        public string cik { get; set; }
        public string isin { get; set; }
        public string cusip { get; set; }
        public string exchange { get; set; }
        public string exchangeShortName { get; set; }
        public string industry { get; set; }
        public string website { get; set; }
        public string description { get; set; }
        public string ceo { get; set; }
        public string sector { get; set; }
        public string country { get; set; }
        public string fullTimeEmployees { get; set; }
        public string phone { get; set; }
        public string address { get; set; }
        public string city { get; set; }
        public string state { get; set; }
        public string zip { get; set; }
        public double dcfDiff { get; set; }
        public double dcf { get; set; }
        public string image { get; set; }
        public string ipoDate { get; set; }
        public bool defaultImage { get; set; }
        public bool isEtf { get; set; }
        public bool isActivelyTrading { get; set; }
        public bool isAdr { get; set; }
        public bool isFund { get; set; }
    }
}

Interfaces

  • create IFinancialModelingPrepService.cs in Interfaces folder

  • IFinancialModelingPrepService.cs

using Api.Models;

namespace Api.Interfaces
{
    public interface IFinancialModelingPrepService
    {
        Task<Stock?> FindStockBySymbolAsync(string symbol);
    }
}

Services

  • create FinancialModelingPrepService.cs in Services folder

  • FinancialModelingPrepService.cs

using api.Dtos.Stock;
using Api.Interfaces;
using Api.Mappers;
using Api.Models;
using Newtonsoft.Json;

namespace Api.Service
{
    public class FinancialModelingPrepService : IFinancialModelingPrepService
    {
        private HttpClient _httpClient;
        private IConfiguration _configuration;

        public FinancialModelingPrepService(HttpClient httpClient, IConfiguration configuration)
        {
            _httpClient = httpClient;
            _configuration = configuration;
        }

        public async Task<Stock?> FindStockBySymbolAsync(string symbol)
        {
            try
            {
                var result = await _httpClient.GetAsync($"https://financialmodelingprep.com/api/v3/profile/{symbol}?apikey={_configuration["FinancialModelingPrepKey"]}");
                if (result.IsSuccessStatusCode)
                {
                    var content = await result.Content.ReadAsStringAsync();
                    var tasks = JsonConvert.DeserializeObject<FinancialModelingPrepStock[]>(content);
                    var stock = tasks[0];

                    if (stock != null) 
                    {
                        return stock.ToStockFromFinancialModelingPrep();
                    }
                }

                return null;
            }
            catch (Exception e) 
            {
                Console.WriteLine(e);
                return null;
            }
        }
    }
}

appsettigs

  • appsettigs.json
...
"FinancialModelingPrepKey": "{YOUR_FINANCIAL_MODELING_PREP_APIKEY}",
...

Mappers

  • StockMapper.cs
...
public static Stock ToStockFromFinancialModelingPrep(this FinancialModelingPrepStock financialModelingPrepStock)
{
    return new Stock
    {
        Symbol = financialModelingPrepStock.symbol,
        CompanyName = financialModelingPrepStock.companyName,
        Purchase = (decimal)financialModelingPrepStock.price,
        LastDiv = (decimal)financialModelingPrepStock.lastDiv,
        Industry = financialModelingPrepStock.industry,
        MarketCap = financialModelingPrepStock.mktCap
    };
}

Program

  • Program.cs
...
builder.Services.AddScoped<IFinancialModelingPrepService, FinancialModelingPrepService>();
builder.Services.AddHttpClient<IFinancialModelingPrepService, FinancialModelingPrepService>();
...

Controllers

  • PortfolioController.cs
...
private readonly UserManager<AppUser> _userManager;
private readonly IStockRepository _stockRepository;
private readonly IPortfolioRepository _portfolioRepository;
private readonly IFinancialModelingPrepService _financialModelingPrepService;

public PortfolioController(UserManager<AppUser> userManager, IStockRepository stockRepository, IPortfolioRepository portfolioRepository, IFinancialModelingPrepService financialModelingPrepService)
{
    _userManager = userManager;
    _stockRepository = stockRepository;
    _portfolioRepository = portfolioRepository;
    _financialModelingPrepService = financialModelingPrepService;
}
...
public async Task<IActionResult> AddPortfolio(string symbol)
{
    var username = User.GetUsername();
    var appUser = await _userManager.FindByNameAsync(username);
    var stock = await _stockRepository.GetBySymbolAsync(symbol);

    if (stock == null)
    {
        stock = await _financialModelingPrepService.FindStockBySymbolAsync(symbol);

        if (stock == null)
        {
            return BadRequest("Stock not found");
        }
        else
        {
            await _stockRepository.CreateAsync(stock);
        }
    }

    var userPortfolio = await _portfolioRepository.GetUserPortfolio(appUser);

    if (userPortfolio.Any(e => e.Symbol.ToLower() == symbol.ToLower()))
    {
        return BadRequest("Cannot add same stock to portfolio");
    }

    var portfolioModel = new Portfolio
    {
        StockId = stock.Id,
        AppUserId = appUser.Id
    };

    await _portfolioRepository.CreateAsync(portfolioModel);

    if (portfolioModel == null) 
    {
        return StatusCode(500, "Cloud not create");
    }
    else
    {
        return Created();
    }
}
...
HttpClient
  • CommentController.cs
...
private readonly UserManager<AppUser> _userManager;
private readonly ICommentRepository _commentRepository;
private readonly IStockRepository _stockRepository;
private readonly IFinancialModelingPrepService _financialModelingPrepService;

public CommentController(UserManager<AppUser> userManager, ICommentRepository commentRepository, IStockRepository stockRepository, IFinancialModelingPrepService financialModelingPrepService)
{
    _userManager = userManager;
    _commentRepository = commentRepository;
    _stockRepository = stockRepository;
    _financialModelingPrepService = financialModelingPrepService;
}
...
[HttpPost]
[Authorize]
[Route("{symbol:alpha}")]
public async Task<IActionResult> Create([FromRoute] string symbol, [FromBody] CreateCommentRequestDto commentDto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    
    var stock = await _stockRepository.GetBySymbolAsync(symbol);

    if (stock == null)
    {
        stock = await _financialModelingPrepService.FindStockBySymbolAsync(symbol);

        if (stock == null)
        {
            return BadRequest("Stock does not exists");
        }
        else
        {
            await _stockRepository.CreateAsync(stock);
        }
    }

    var username = User.GetUsername();
    var appUser = await _userManager.FindByNameAsync(username);
    var commentModel = commentDto.ToCommentFromCreate(stock.Id);
    commentModel.AppUserId = appUser.Id;

    await _commentRepository.CreateAsync(commentModel);

    return CreatedAtAction(nameof(GetById), new { id = commentModel.Id }, commentModel.ToCommentDto());
}
...
HttpClient

OrderBy

Helpers

  • create CommentQueryObject.cs in Helpers folder

  • CommentQueryObject.cs

namespace Api.Helpers
{
    public class CommentQueryObject
    {
        public string Symbol { get; set; }
        public bool IsDecsending { get; set; } = true;
    }
}

Interfaces

  • ICommentRepository.cs
...
Task<List<Comment>> GetAllAsync(CommentQueryObject queryObject);
...

Repository

  • CommentRepository.cs
...
public async Task<List<Comment>> GetAllAsync(CommentQueryObject queryObject)
{
    var comments = _context.Comments.Include(c => c.AppUser).AsQueryable();

    if (!string.IsNullOrWhiteSpace(queryObject.Symbol))
    {
        comments = comments.Where(s => s.Stock.Symbol == queryObject.Symbol);
    }

    if (queryObject.IsDecsending)
    {
        comments = comments.OrderByDescending(c => c.CreatedOn);
    }

    return await comments.ToListAsync();
}
...

Controllers

  • CommentController.cs
...
public async Task<IActionResult> GetAll([FromQuery] CommentQueryObject queryObject)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var comments = await _commentRepository.GetAllAsync(queryObject);
    var commentDto = comments.Select(s => s.ToCommentDto());

    return Ok(commentDto);
}
...
HttpClient

CORS(Cross-Origin Resource Sharing)

  • is a browser security feature that restricts web pages from making requests to a different origin.
  • APIs are hosted on different domains than the websites that consume them.
  • CORS allows controlled access to these APIs.

Program

  • Program.cs
...
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetIsOriginAllowed(origin => true));
...

CommentController

  • CommentController.cs
...
[HttpGet]
[Authorize]
public async Task<IActionResult> GetAll([FromQuery] CommentQueryObject queryObject)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    var comments = await _commentRepository.GetAllAsync(queryObject);
    var commentDto = comments.Select(s => s.ToCommentDto());

    return Ok(commentDto);
}
...

AuthService

  • type npm install react-toastify in terminal for toasting.

App

  • App.tsx
import { Outlet } from 'react-router';
import './App.css';
import Navbar from './components/Navbar/Navbar';
import { ToastContainer } from 'react-toastify';
import "react-toastify/dist/ReactToastify.css";

function App() {
  
  return (
    <>
      <Navbar/>
      <Outlet/>
      <ToastContainer />
    </>
  );
}

export default App;

ErrorHandler

  • create helpers folder in src
  • create ErrorHandler.tsx in helpers folder

  • ErrorHandler.tsx
import axios from "axios"
import { toast } from "react-toastify";

export const handleError = (error: any) => {
  if(axios.isAxiosError(error)) {
    var err = error.response;

    if(Array.isArray(err?.data.errors)) {
      for(let val of err?.data.errors) {
        toast.warning(val.description);
      }
    } else if (typeof err?.data.errors === 'object') {
      for(let e in err?.data.errors) {
        toast.warning(err.data.errors[e][0]);
      }
    } else if (err?.data) {
      toast.warning(err.data);
    } else if (err?.status === 401) {
      toast.warning("Please login");
      window.history.pushState({}, "LoginPage", "/login");
    } else if (err) {
      toast.warning(err?.data);
    }
  }
}
  • Array of errors (Array.isArray): Handles multiple errors and displays each one as a warning.
  • Object of errors (typeof === ‘object’): Iterates through the object’s keys and displays the first error message for each key.
  • General error message (err?.data): Displays a single error message.
  • Authentication error (401 status): Prompts the user to log in and redirects them to the login page.
  • Unexpected errors: Displays the raw error object as a warning.

User

  • create models folder in src
  • create User.tsx in models folder

  • User.tsx
export type UserProfileToken = {
  userName: string;
  email: string;
  token: string;
}

AuthService

  • create services folder in src
  • create AuthService.tsx in services folder

  • AuthService.tsx
import axios from "axios";
import { handleError } from "../helpers/ErrorHandler";
import { UserProfileToken } from "../models/User";

const api = "http://localhost:7242/api/";

export const loginAPI = async (username: string, password: string) => {
  try {
    const data = await axios.post<UserProfileToken>(api + "account/login", {
      username: username,
      password: password
    });

    return data;
  } catch (error) {
    handleError(error);
  }
};

export const registerAPI = async (email: string, username: string, password: string) => {
  try {
    const data = await axios.post<UserProfileToken>(api + "account/register", {
      email: email,
      username: username,
      password: password
    });

    return data;
  } catch (error) {
    handleError(error);
  }
};

Context

  • a component with no JSX that is global
  • is can be shared

User

  • User.tsx
export type UserProfile = {
  userName: string;
  email: string;
}

useAuth

  • create contexts folder in src
  • create useAuth.tsx in contexts folder

  • useAuth.tsx
import { createContext, useEffect, useState } from "react";
import { UserProfile } from "../models/User"
import { useNavigate } from "react-router";
import { loginAPI, registerAPI } from "../services/AuthService";
import { toast } from "react-toastify";
import React from "react";
import axios from "axios";

type UserContextType = {
  user: UserProfile | null;
  token: string | null;
  registerUser: (email: string, username: string, password: string) => void;
  loginUser: (username: string, password: string) => void;
  logout: () => void;
  isLoggedIn: () => boolean;
};

type Props = { children: React.ReactNode };

const UserContext = createContext<UserContextType>({} as UserContextType);

export const UserProvider = ({ children }: Props) => {
  const navigate = useNavigate();
  const [token, setToken] = useState<string | null>(null);
  const [user, setUser] = useState<UserProfile | null>(null);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    const user = localStorage.getItem("user");
    const token = localStorage.getItem("token");
    if(user && token) {
      setUser(JSON.parse(user));
      setToken(token);
      axios.defaults.headers.common["Authorization"] = "Bearer " + token;
    }
    setIsReady(true);
  }, []);

  const registerUser = async (email: string, username: string, password: string) => {
    await registerAPI(email, username, password).then((res) => {
      if(res) {
        localStorage.setItem("token", res?.data.token);
        const userObj = {
          userName: res?.data.userName,
          email: res?.data.email
        };
        localStorage.setItem("user", JSON.stringify(userObj));
        setToken(res?.data.token!);
        setUser(userObj!);
        toast.success("Login Success!");
        navigate("/search");
      }
    }).catch((e) => toast.warning("Server error occured"));
  };

  const loginUser = async (username: string, password: string) => {
    await loginAPI(username, password).then((res) => {
      if(res) {
        localStorage.setItem("token", res?.data.token);
        const userObj = {
          userName: res?.data.userName,
          email: res?.data.email
        };
        localStorage.setItem("user", JSON.stringify(userObj));
        setToken(res?.data.token!);
        setUser(userObj!);
        toast.success("Login Success!");
        navigate("/search");
      }
    }).catch((e) => toast.warning("Server error occured"));
  };

  const isLoggedIn = () => {
    return !!user;
  };

  const logout = () => {
    localStorage.removeItem("token");
    localStorage.removeItem("user");
    setUser(null);
    setToken("");
    navigate("/");
  };

  return (
    <UserContext.Provider value=>
      {isReady ? children : null}
    </UserContext.Provider>
  )
};

export const useAuth = () => React.useContext(UserContext);

App

  • App.tsx
function App() {
  
  return (
    <>
      <UserProvider>
        <Navbar/>
        <Outlet/>
        <ToastContainer />
      </UserProvider>
    </>
  );
}

export default App;

Login

  • type npm install react-hook-form yup @hookform/resolvers in terminal to use form

useForm

  • is a popular utility in libraries like React Hook Form, allowing developers to manage form state, handle validation, and submit logic in React applications.

LoginPage

  • create LoginPage folder in pages
  • create LoginPage.tsx and LoginPage.css files in LoginPage folder

  • LoginPage.tsx
import React from 'react'
import * as Yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useAuth } from '../../contexts/useAuth';
import { useForm } from 'react-hook-form';

type Props = {};

type LoginFormInputs = {
  userName: string;
  password: string;
};

const validation = Yup.object().shape({
  userName: Yup.string().required("Username is required"),
  password: Yup.string().required("Password is required")
});

const LoginPage = (props: Props) => {
  const { loginUser } = useAuth();
  const { register, handleSubmit, formState: { errors } } = useForm<LoginFormInputs>({ resolver: yupResolver(validation) });
  const handleLogin = (form: LoginFormInputs) => {
    loginUser(form.userName, form.password);
  };

  return (
    <section className="bg-gray-50 dark:bg-gray-900">
      <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
        <div className="w-full bg-white rounded-lg shadow dark:border md:mb-20 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
          <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
            <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
              Sign in to your account
            </h1>
            <form className="space-y-4 md:space-y-6" onSubmit={handleSubmit(handleLogin)}>
              <div>
                <label
                  htmlFor="username"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Username
                </label>
                <input
                  type="text"
                  id="username"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  placeholder="Username"
                  {...register("userName")}
                />
                {errors.userName ? <p>{errors.userName.message}</p> : ""}
              </div>
              <div>
                <label
                  htmlFor="password"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Password
                </label>
                <input
                  type="password"
                  id="password"
                  placeholder="••••••••"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  {...register("password")}
                />
              </div>
              <div className="flex items-center justify-between">
                <a
                  href="#"
                  className="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Forgot password?
                </a>
              </div>
              <button
                type="submit"
                className="w-full bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
              >
                Sign in
              </button>
              <p className="text-sm font-light text-gray-500 dark:text-gray-400">
                Don’t have an account yet?{" "}
                <a
                  href="#"
                  className="font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Sign up
                </a>
              </p>
            </form>
          </div>
        </div>
      </div>
    </section>
  )
}

export default LoginPage

Routes

  • Routes.tsx
export const router = createBrowserRouter([
  {
    path: "/",
    element: <App/>,
    children: [
      { path: "", element: <HomePage/> },
      { path: "login", element: <LoginPage /> },
      { path: "search", element: <SearchPage/> },
      { path: "design-guide", element: <DesignPage/> },
      { path: "company/:ticker", element: <CompanyPage/>,
        children: [
          { path: "company-profile", element: <CompanyProfile/> },
          { path: "income-statement", element: <IncomeStatement/> },
          { path: "balance-sheet", element: <BalanceSheet/> },
          { path: "cashflow-statement", element: <CashFlowStatement/> }
        ]
      }
    ]
  }
])
Login

Register

RegisterPage

  • create RegisterPage folder in pages folder
  • create RegisterPage.tsx and RegisterPage.css in RegisterPage folder

  • RegisterPage.tsx
import React from 'react'
import * as Yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useAuth } from '../../contexts/useAuth';
import { useForm } from 'react-hook-form';

type Props = {}

type RegisterFormInputs = {
  email: string;
  userName: string;
  password: string;
};

const validation = Yup.object().shape({
  email: Yup.string().required("Email is required"),
  userName: Yup.string().required("Username is required"),
  password: Yup.string().required("Password is required")
});

const RegisterPage = (props: Props) => {
  const { registerUser } = useAuth();
  const { register, handleSubmit, formState: { errors } } = useForm<RegisterFormInputs>({ resolver: yupResolver(validation) });
  const handleLogin = (form: RegisterFormInputs) => {
    registerUser(form.email, form.userName, form.password);
  };

  return (
    <section className="bg-gray-50 dark:bg-gray-900">
      <div className="flex flex-col items-center justify-center px-6 py-8 mx-auto md:h-screen lg:py-0">
        <div className="w-full bg-white rounded-lg shadow dark:border md:mb-20 sm:max-w-md xl:p-0 dark:bg-gray-800 dark:border-gray-700">
          <div className="p-6 space-y-4 md:space-y-6 sm:p-8">
            <h1 className="text-xl font-bold leading-tight tracking-tight text-gray-900 md:text-2xl dark:text-white">
              Sign in to your account
            </h1>
            <form className="space-y-4 md:space-y-6" onSubmit={handleSubmit(handleLogin)}>
              <div>
                <label
                  htmlFor="email"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Email
                </label>
                <input
                  type="text"
                  id="email"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  placeholder="Email"
                  {...register("email")}
                />
                {errors.email ? <p>{errors.email.message}</p> : ""}
              </div>
              <div>
                <label
                  htmlFor="username"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Username
                </label>
                <input
                  type="text"
                  id="username"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  placeholder="Username"
                  {...register("userName")}
                />
                {errors.userName ? <p>{errors.userName.message}</p> : ""}
              </div>
              <div>
                <label
                  htmlFor="password"
                  className="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
                >
                  Password
                </label>
                <input
                  type="password"
                  id="password"
                  placeholder="••••••••"
                  className="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
                  {...register("password")}
                />
              </div>
              <div className="flex items-center justify-between">
                <a
                  href="#"
                  className="text-sm font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Forgot password?
                </a>
              </div>
              <button
                type="submit"
                className="w-full bg-primary-600 hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800"
              >
                Sign in
              </button>
              <p className="text-sm font-light text-gray-500 dark:text-gray-400">
                Don’t have an account yet?{" "}
                <a
                  href="#"
                  className="font-medium text-primary-600 hover:underline dark:text-primary-500"
                >
                  Sign up
                </a>
              </p>
            </form>
          </div>
        </div>
      </div>
    </section>
  )
}

export default RegisterPage

Routes

  • Routes.tsx
...
{ path: "login", element: <LoginPage /> },
{ path: "register", element: <RegisterPage /> },
...
Register

Protected Routes

  • is for protecting to show pages with authorization.
  • navigates directly to login page when the page is protected.

ProtectedRoute

  • create ProtectedRoute.tsx in routes folder

  • ProtectedRoute.tsx

import React from 'react'
import { Navigate, useLocation } from 'react-router';
import { useAuth } from '../contexts/useAuth';

type Props = { children: React.ReactNode };

const ProtectedRoute = ({ children }: Props) => {
  const location = useLocation();
  const { isLoggedIn } = useAuth();

  return isLoggedIn() 
    ? <>{children}</> 
    : <Navigate to="/login" state= replace />
}

export default ProtectedRoute

Routes

  • Routes.tsx
...
{ path: "search", element: <ProtectedRoute><SearchPage/></ProtectedRoute> },
{ path: "design-guide", element: <DesignPage/> },
{ path: "company/:ticker", element: <ProtectedRoute><CompanyPage/></ProtectedRoute>,
...

Logout

Navbar

  • Navbar.tsx
import React from 'react'
import logo from "../../assets/images/logo.png";
import { Link } from 'react-router-dom';
import { useAuth } from '../../contexts/useAuth';

interface Props {

}

const Navbar: React.FC<Props> = (props: Props): JSX.Element => {
  const { isLoggedIn, user, logout } = useAuth();

  return (
    <nav className="relative container mx-auto p-6">
      <div className="flex items-center justify-between">
        <div className="flex items-center space-x-20">
          <Link to="/">
            <img src={logo} alt="" />
          </Link>
          <div className="hidden font-bold lg:flex">
            <Link to="/search" className="text-black hover:text-darkBlue">
              Search
            </Link>
          </div>
        </div>
        {isLoggedIn() 
          ? <div className="hidden lg:flex items-center space-x-6 text-back">
              <div className="hover:text-darkBlue">Welcome, {user?.userName}</div>
              <a
                onClick={logout}
                className="px-8 py-3 font-bold rounded text-white bg-lightGreen hover:opacity-70"
              >
                Logout
              </a>
            </div>
          : <div className="hidden lg:flex items-center space-x-6 text-back">
              <Link to="/login" className="hover:text-darkBlue" >Login</Link>
              <Link to="/register" className="px-8 py-3 font-bold rounded text-white bg-lightGreen hover:opacity-70">Signup</Link>
            </div>
        }
      </div>
    </nav>
  )
}

export default Navbar
Logout

Comment

{…register()}

  • is same with value={title} onChange={(e) => setTitle(e.target.value)}.
  • is making easy to use validation and to manage value.

Comment

  • create Comment.tsx in models folder

  • Comment.tsx

import axios from "axios";
import { CommentPost } from "../models/Comment";
import { handleError } from "../helpers/ErrorHandler";

const api = "https://localhost:7242/api/comment/";

export const commentPostAPI = async (title: string, content: string, symbol: string) => {
  try {
    const data = await axios.post<CommentPost>(api + `${symbol}`, {
      title: title,
      content: content
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

CommentService

  • create CommentService.tsx in services folder

  • CommentService.tsx

import axios from "axios";
import { CommentPost } from "../models/Comment";
import { handleError } from "../helpers/ErrorHandler";

const api = "https://localhost:7242/api/comment/";

export const commentPostAPI = async (title: string, content: string, symbol: string) => {
  try {
    const data = await axios.post<CommentPost>(api + `${symbol}`, {
      title: title,
      content: content
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

StockComment

  • create StockComment folder in components folder
  • create StockComment.tsx and StockComment.css in StockComment folder

  • StockComment.tsx
import React from 'react'
import StockCommentForm from './StockCommentForm/StockCommentForm';
import { commentPostAPI } from '../../services/CommentService';
import { toast } from 'react-toastify';

type Props = {
  stockSymbol: string;
}

type CommentFormInputs = {
  title: string;
  content: string;
};

const StockComment = ({ stockSymbol }: Props) => {
  const handleComment = (e: CommentFormInputs) => {
    commentPostAPI(e.title, e.content, stockSymbol).then((res) => {
      if (res) {
        toast.success("Comment created successfully!");
      }
    }).catch((e) => {
      toast.warning(e);
    });
  }

  return (
    <StockCommentForm symbol={stockSymbol} handleComment={handleComment}/>
  )
}

export default StockComment

StockCommentForm

  • create StockCommentForm folder in StockComment folder
  • create StockCommentForm.tsx and StockCommentForm.css in StockCommentForm folder

  • StockCommentForm.tsx
import React from 'react'
import * as Yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from 'react-hook-form';

type Props = {
  symbol: string;
  handleComment: (e: CommentFormInputs) => void;
};

type CommentFormInputs = {
  title: string;
  content: string;
};

const validation = Yup.object().shape({
  title: Yup.string().required("Title is required"),
  content: Yup.string().required("Content is required")
});

const StockCommentForm = ({ symbol, handleComment }: Props) => {
  const { register, handleSubmit, formState: { errors } } = useForm<CommentFormInputs>({ resolver: yupResolver(validation) });

  return (
    <form className="mt-4 ml-4" onSubmit={handleSubmit(handleComment)}>
      <input
        type="text"
        id="title"
        className="mb-3 bg-white border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
        placeholder="Title"
        {...register("title")}
      />
      {errors.title ? <p>{errors.title.message}</p> : ""}
      <div className="py-2 px-4 mb-4 bg-white rounded-lg rounded-t-lg border border-gray-200 dark:bg-gray-800 dark:border-gray-700">
        <label htmlFor="comment" className="sr-only">
          Your comment
        </label>
        <textarea
          id="comment"
          rows={6}
          className="px-0 w-full text-sm text-gray-900 border-0 focus:ring-0 focus:outline-none dark:text-white dark:placeholder-gray-400 dark:bg-gray-800"
          placeholder="Write a comment..."
          {...register("content")}
        ></textarea>
      </div>
      <button
        type="submit"
        className="inline-flex items-center py-2.5 px-4 text-xs font-medium text-center text-white bg-lightGreen rounded-lg focus:ring-4 focus:ring-primary-200 dark:focus:ring-primary-900 hover:bg-primary-800"
      >
        Post comment
      </button>
    </form>
  )
}

export default StockCommentForm

CompanyProfile

...
return (
  <>
    { companyData 
      ? <>
          <RatioList data={companyData} config={tableConfig} />
          <StockComment stockSymbol={ticker} />
        </>
      : <Spinner/>
    }
  </>
)
...
Comment

Comment List

Comment

  • Comment.ts
...
export type CommentGet = {
  title: string;
  content: string;
  createdBy: string;
};

CommentService

  • CommentService.tsx
import axios from "axios";
import { CommentGet, CommentPost } from "../models/Comment";
import { handleError } from "../helpers/ErrorHandler";

const api = "https://localhost:7242/api/comment/";
const token = localStorage.getItem('token');

export const commentPostAPI = async (title: string, content: string, symbol: string) => {
  try {
    const data = await axios.post<CommentPost>(api + `${symbol}`, {
      title: title,
      content: content,
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

export const commentsGetAPI = async (symbol: string) => {
  try {
    const data = await axios.get<CommentGet[]>(api + `?symbol=${symbol}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

StockComment

  • StockComment.tsx
import React, { useEffect, useState } from 'react'
import StockCommentForm from './StockCommentForm/StockCommentForm';
import { commentPostAPI, commentsGetAPI } from '../../services/CommentService';
import { toast } from 'react-toastify';
import { CommentGet } from '../../models/Comment';
import Spinner from '../Spinner/Spinner';
import StockCommentList from '../StockCommentList/StockCommentList';

type Props = {
  stockSymbol: string;
}

type CommentFormInputs = {
  title: string;
  content: string;
};

const StockComment = ({ stockSymbol }: Props) => {
  const [comments, setComments] = useState<CommentGet[] | null>(null);
  const [loading, setLoading] = useState<boolean>();

  useEffect(() => {
    getComments();
  }, [])

  const handleComment = (e: CommentFormInputs) => {
    commentPostAPI(e.title, e.content, stockSymbol).then((res) => {
      if (res) {
        toast.success("Comment created successfully!");
        getComments();
      }
    }).catch((e) => {
      toast.warning(e);
    });
  }

  const getComments = () => {
    setLoading(true);
    commentsGetAPI(stockSymbol).then((res) => {
      setLoading(false);
      setComments(res?.data!);
    });
  }
  return (
    <div className="flex flex-col">
      {loading ? <Spinner /> : <StockCommentList comments={comments!} />}
      <StockCommentForm symbol={stockSymbol} handleComment={handleComment} />
    </div>
  )
}

export default StockComment

StockCommentList

  • create StockCommentList folder in components folder
  • create StockCommentList.tsx and StockCommentList.css files in StockCommentList folder

  • StockCommentList.tsx
import React from 'react'
import { CommentGet } from '../../models/Comment';
import StockCommentListItem from '../StockCommentListItem/StockCommentListItem';

type Props = {
  comments: CommentGet[];
}

const StockCommentList = ({comments}: Props) => {
  return (
    <>
      {comments 
        ? comments.map((comment) => {
          return <StockCommentListItem comment={comment} />;
          })
        : ""}
    </>
  )
}

export default StockCommentList

StockCommentListItem

  • create StockCommentListItem folder in components folder
  • create StockCommentListItem.tsx and StockCommentListItem.css files in StockCommentListItem folder

  • StockCommentListItem.tsx
import React from 'react'
import { CommentGet } from '../../models/Comment'

type Props = {
  comment: CommentGet;
}

const StockCommentListItem = ({comment}: Props) => {
  return (
    <div className="relative grid grid-cols-1 gap-4 ml-4 p-4 mb-8 w-full border rounded-lg bg-white shadow-lg">
      <div className="relative flex gap-4">
        <div className="flex flex-col w-full">
          <div className="flex flex-row justify-between">
            <p className=" relative text-xl whitespace-nowrap truncate overflow-hidden">
              {comment.title}
            </p>
          </div>
          <p className="text-dark text-sm">@{comment.createdBy}</p>
        </div>
      </div>
      <p className="-mt-4 text-gray-500">{comment.content}</p>
    </div>
  )
}

export default StockCommentListItem
Comment List

Portfolio Service

Portfolio

  • create Portfolio.ts in models folder

  • Portfolio.ts

export type PortfolioGet = {
  id: number;
  symbol: string;
  companyName: string;
  purchase: number;
  lastDiv: number;
  industry: string;
  marketCap: number;
  comments: any;
}

export type PortfolioPost = {
  symbol: string;
}

PortfolioService

  • create PortfolioService.ts in services folder

  • PortfolioService.tsx

import axios from "axios";
import { PortfolioGet, PortfolioPost } from "../models/Portfolio";
import { handleError } from "../helpers/ErrorHandler";

const api="https://localhost:7242/api/portfolio";
const token = localStorage.getItem('token');

export const portfolioAddAPI = async (symbol: string) => {
  try {
    const data = await axios.post<PortfolioPost>(api + `?symbol=${symbol}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

export const portfolioDeleteAPI = async (symbol: string) => {
  try {
    const data = await axios.delete<PortfolioPost>(api + `?symbol=${symbol}`, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

export const portfolioGetAPI = async () => {
  try {
    const data = await axios.get<PortfolioGet[]>(api, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });

    return data;
  } catch (error) {
    handleError(error);
  }
}

CardPortfolio

  • CardPortfolio.tsx
import React, { SyntheticEvent } from 'react'
import DeletePortfolio from '../DeletePortfolio/DeletePortfolio';
import { Link } from 'react-router-dom';
import { PortfolioGet } from '../../../models/Portfolio';

interface Props {
  portfolioValue: PortfolioGet;
  onPortfolioDelete: (e: SyntheticEvent) => void;
}

const CardPortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div className="flex flex-col w-full p-8 space-y-4 text-center rounded-lg shadow-lg md:w-1/3">
      <Link to={`/company/${portfolioValue}`} className="pt-6 text-xl font-bold">
        {portfolioValue.symbol}
      </Link>
      <DeletePortfolio
        portfolioValue={portfolioValue.symbol}
        onPortfolioDelete={onPortfolioDelete}
      />
    </div>
  )
}

export default CardPortfolio

DeletePortfolio

  • DeletePortfolio.tsx
import React, { SyntheticEvent } from 'react'

interface Props {
  portfolioValue: string;
  onPortfolioDelete: (e: SyntheticEvent) => void;
}

const DeletePortfolio: React.FC<Props> = ({ portfolioValue, onPortfolioDelete }: Props): JSX.Element => {
  return (
    <div>
      <form onSubmit={onPortfolioDelete}>
        <input hidden={true} value={portfolioValue} />
        <button className="block w-full py-3 text-white duration-200 border-2 rounded-lg bg-red-500 hover:text-red-500 hover:bg-white border-red-500">
          X
        </button>
      </form>
    </div>
  )
}

export default DeletePortfolio

ListPortfolio

  • ListPortfolio.tsx
...
interface Props {
  portfolioValues: PortfolioGet[];
  onPortfolioDelete: (e: SyntheticEvent) => void;
}
...

SearchPage

  • SearchPage.tsx
import React, { ChangeEvent, SyntheticEvent, useEffect, useState } from 'react'
import Navbar from '../../components/Navbar/Navbar'
import Search from '../../components/Search/Search'
import ListPortfolio from '../../components/Portfolio/ListPortfolio/ListPortfolio'
import CardList from '../../components/CardList/CardList'
import { CompanySearch } from '../../company'
import { searchCompanies } from '../../api'
import { PortfolioGet } from '../../models/Portfolio'
import { portfolioAddAPI, portfolioDeleteAPI, portfolioGetAPI } from '../../services/PortfolioService'
import { toast } from 'react-toastify'

type Props = {}

const SearchPage = (props: Props) => {
  const [search, setSearch] = useState<string>("");
  const [portfolioValues, setPortfolioValues] = useState<PortfolioGet[] | null>([]);
  const [searchResult, setSearchResult] = useState<CompanySearch[]>([]);
  const [serverError, setServerError] = useState<string>("");

  const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
    setSearch(e.target.value);
    console.log(e);
  };

  useEffect(() => {
    getPortfolio();
  }, []);

  const getPortfolio = () => {
    portfolioGetAPI().then((res) => {
      if (res?.data) {
        setPortfolioValues(res?.data);
      }
    }).catch((e) => {
      toast.warning("Could not get portfolio values!");
    });
  };

  const onSearchSubmit = async (e: SyntheticEvent) => {
    e.preventDefault();

    const result = await searchCompanies(search);
    if (typeof result === "string") {
      setServerError(result);
    } else if (Array.isArray(result.data)){
      setSearchResult(result.data);
    }
    console.log(searchResult);
  };

  const onPortfolioCreate = (e: any) => {
    e.preventDefault();
    
    console.log("HHH");
    portfolioAddAPI(e.target[0].value).then((res) => {
      if (res?.status === 204) {
        toast.success("Stock added to portfolio!");
        getPortfolio();
      }
    }).catch((e) => {
      toast.warning("Could not get portfolio values!");
    });
  };

  const onPortfolioDelete = (e: any) => {
    e.preventDefault();
    
    portfolioDeleteAPI(e.target[0].value).then((res) => {
      if (res?.status === 200) {
        toast.success("Stock deleted to portfolio!");
        getPortfolio();
      }
    }).catch((e) => {
      toast.warning("Could not get portfolio values!");
    });
  };

  return (
    <div>
      <Search
        onSearchSubmit={onSearchSubmit}
        search={search}
        handleSearchChange={handleSearchChange}
      />
      <ListPortfolio
        portfolioValues={portfolioValues!}
        onPortfolioDelete={onPortfolioDelete}
      />
      <CardList
        searchResults={searchResult}
        onPortfolioCreate={onPortfolioCreate}
      />

      {serverError && <div>Unable to connect to API</div>}
    </div>
  )
}

export default SearchPage
Portfolio Service

Download



C#React Share Tweet +1