Skip to content

Carson Wolber summer 2024 Meta University Capstone Project

Notifications You must be signed in to change notification settings

CWMetaUCapstone/EconMetrics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

194 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EconMetrics

Carson Wolber summer 2024 Meta University Capstone Project

Project Requirements

Your app provides multiple opportunities for you to solve technical challenges

Technical Challenge #1:

  • EconMetrics offers the ability to set and track goals. As those goals are met / missed, users are reminded of their goals.

    • goal setting is handled in the GoalPage folder, the ability to tracking new goals specifically is in this program flow:
    • notifications are done using flask_mail on the backend and they're set to run once a week along with pulling new transaction data
      """
      helper to send users notifications that update on the state of the goals they're currently tracking
      following a sync to grab users most recent transaction data from plaid
      """
      def send_goal_update_msg(userId):
      user = User.query.get(userId)
      message_body = 'Here\'s your breakdown on goal progress since last week: \n' + format_updates(userId)
      msg = Message('Your EconMetrics Goal Progress',
      sender='econmetrics.goalupdates@gmail.com',
      recipients=[user.email],
      body= message_body
      )
      try:
      mail.send(msg)
      return jsonify({'message': 'email sent!'})
      except Exception as e:
      print(f"error at send_goal_update_msg: {str(e)}")
      return jsonify({'failed to send email': str(e)})
      """
      helper to format the list of updates for each goal a user is tracking when tranactions update and message is sent.
      User's receive emails that for each goal they're tracking, show how their spending on that category has changed between
      the past two most recent transaction syncs with the context of the goal amount
      """
      def format_updates(userId):
      update_msg = ''
      user = User.query.get(userId)
      goals = user.goals
      new_transaction = Transactions.query.filter_by(userId=user.id).order_by(Transactions.time.desc()).first()
      # this gets last weeks transaction (the one before newest / second row) by simply skipping first row and getting the next row
      last_transaction = Transactions.query.filter_by(userId=user.id).order_by(Transactions.time.desc()).offset(1).limit(1).first()
      for goal in goals:
      category = goal.category
      update_msg += 'category: ' + category + ' targeted change in expenditure: ' + str(goal.target) + ' this weeks percent: '+ str(getattr(new_transaction, category)) + ' last week\'s percent: ' + str(getattr(last_transaction, category)) + '\n'
      return update_msg
  • After goals are set, EconMetrics provides users with personalized plans to meet their goals based on user preferences, other users who have accomplished similar goals, and market conditions.

Technical Challenge #2:

  • Users of EconMetrics want to be able to visualize their financial information and their information compared to other users. This goal is mostly encapsulated in the components in the Graph folder here: https://github.com/CWMetaUCapstone/EconMetrics/tree/e87f96ff40c9694587a0d7d77ee2a357a1ff9b0e/frontend/src/ProfilePage/Graphs There is also a pie plot PNG showing off a users expenditure in a different visual:
    """
    helper function to generate a pie chart representation of user's transaction data at a sub-category level
    plots are saved in the frontend/public folder for use by react. Each file name uses userId so each user
    has a stored unique chart
    """
    def create_pie_plot(transaction_data, userId, transactionId):
    try:
    # extract the 'name' and 'percent' fields by normailizing the the user_transaction_data JSON and and create a nx2 data frame for these fields
    rows = []
    for category in transaction_data.values():
    temp_df = pd.json_normalize(category, record_path='details')
    rows.append(temp_df)
    data = pd.concat(rows, ignore_index=True)
    labels = data['name'].tolist()
    percents = data['percent'].tolist()
    fig, ax = plt.subplots()
    ax.pie(percents, labels=labels, autopct='%.2f%%')
    root = '../frontend/public'
    filename = f'pie_chart_{userId}_{transactionId}.png'
    directory = os.path.join(root, filename)
    plt.savefig(directory, transparent=True)
    plt.close()
    except Exception as e:
    print(f"error occurred at create_pie_plot: {str(e)}")
  • They also want to be able to customize/filter what information is pooled into the data visualized.
    • there are instances of this goal in both folders. for the TimeChart component this is done by regulating the data prop using a react-select prop
    • in CompBoxPlot this done both directly in the graph by clicking on different parts of the graph:
      • // if a circle is clicked, the corresponding userId is propagated onto profile page to show details about that user
        svg.selectAll("circle.point")
        .on("click", (event, d) => {
        const userId = d.id
        OnClickedUserId(userId)
        })
        // users can reset the click state by clicking on the graph itself (not a circle or box)
        svg.on("click", (event) => {
        const target = event.target
        if(target.className.baseVal !== 'point'){
        OnClickedUserId(0)
        }
        if(target.className.baseVal !== 'box-plot'){
        OnBoxClick({})
        }
        })
      • // if the user hovers over a dot, all dots corresponding to the user associated with that dot are highlighted
        svg.on("mouseover", (event) => {
        const target = event.target
        if(target.className.baseVal === 'point'){
        const userId = target.getAttribute("data-user-id");
        svg.selectAll("circle.point")
        .attr("opacity", function(){
        return this.getAttribute("data-user-id") === userId ? 1 : 0.2
        })
        }
        }).on("mouseout", () => {
        svg.selectAll("circle.point")
        .attr("opacity", 1)
        });
    • as well as giving the user some external control over data visualization using two different Checkbox components
      • // conditionally append box plots based on user input through the corresponding checkbox
        if(showBoxPlots){
        // the placement of this if statement in the program allows dots to be placed on top of the box so their mouse effects can still be reached
        const boxWidth = chartConfig['scatter-range'] - chartConfig['scatter-range'] / 2 + margin.left;
        const boxOffset = (bandwidth - boxWidth) / 2;
        const boxPlots = categoryGroups.selectAll("rect.box-plot")
        .data(d => [d])
        .join("g")
        .attr("transform", `translate(${boxOffset}, 0)`)
        boxPlots.append("rect")
        .attr("class", "box-plot")
        .attr("y", d => yAxis(d.quartiles[2]))
        .attr("height", d => yAxis(d.quartiles[0]) - yAxis(d.quartiles[2]))
        .attr("width", boxWidth)
        .attr("fill", "#ddd")
        .attr("opacity", 0.6)
        .on("click", (event, d) => {
        // if user data is being shown all other inputs are blocked
        if(!showUserData){
        OnBoxClick(d)
        }
        })
        boxPlots.append("line")
        .attr("class", "whisker")
        .attr("y1", d => yAxis(d.range[1]))
        .attr("y2", d => yAxis(d.range[0]))
        .attr("x1", (bandwidth - (bandwidth * chartConfig['box-width-factor']))/2 + chartConfig['scatter-range']/2)
        .attr("x2", (bandwidth - (bandwidth * chartConfig['box-width-factor']))/2 + chartConfig['scatter-range']/2)
        .attr("stroke", "black")
        boxPlots.append("line")
        .attr("class", "median")
        .attr("y1", d => yAxis(d.quartiles[1]))
        .attr("y2", d => yAxis(d.quartiles[1]))
        .attr("x1", 0)
        .attr("x2", boxWidth)
        .attr("stroke", "black")
        boxPlots.append("line")
        .attr("class", "cap")
        .attr("y1", d => yAxis(d.range[1]))
        .attr("y2", d => yAxis(d.range[1]))
        .attr("x1", boxWidth * .2)
        .attr("x2", boxWidth * .8)
        .attr("stroke", "black")
        boxPlots.append("line")
        .attr("class", "cap")
        .attr("y1", d => yAxis(d.range[0]))
        .attr("y2", d => yAxis(d.range[0]))
        .attr("x1", boxWidth * .2)
        .attr("x2", boxWidth * .8)
        .attr("stroke", "black")
        }
        const similarDots = categoryGroups.selectAll("circle.point")
        .data(d => d.entries)
        .join("circle")
        .attr("class", "point")
        // the x-axis value for dots are randomized within the range of the parent category to give more of a scattered look to improve clarity
        .attr("cx", () => Math.random() * chartConfig['scatter-range'] - chartConfig['scatter-range'] / 2 + margin.left)
        .attr("cy", d => yAxis(d.value))
        .attr("fill", d => colorRules(d.id))
        .attr("r", chartConfig.dot_radius * 1.5)
        .attr("data-category", d => d.category)
        .attr("data-user-id", d => d.id)
        /*
        if [showUserData] is enabled, plot the data for user whose profile we're on,
        highlight these points by graying out other dots and disabling their pointer effects
        */
        if(showUserData){
        categoryGroups.selectAll("circle.user-point")
        /*
        because userData field in [categoryValues] is passed as the direct userData object,
        categories are not seperated in the same way as in entries
        so we need to map out each category to get the category name and associated percent
        */
        .data(d => {
        return d.userData.map(user => ({
        total_percent: user[d.category].total_percent,
        category: d.category
        }));
        })
        .join("circle")
        .attr("class", "user-point")
        .attr("cx", () => Math.random() * chartConfig['scatter-range'] - chartConfig['scatter-range'] / 2 + margin.left)
        .attr("cy", d => yAxis(d.total_percent))
        .attr("fill", "steelblue")
        .attr("r", chartConfig.dot_radius * 2)
        .attr("data-category", d => d.category)
        similarDots
        .attr("fill", "#ddd")
        .style("pointer-events", "none")
        }
      • <div className='Checkboxes'>
        <FormControlLabel
        control={
        <Checkbox
        color='success'
        checked={yourStatsCheckbox}
        onChange={handleYourStatsBoxChange}
        />
        }
        label="Show Your Stats"
        />
        <FormControlLabel
        control={
        <Checkbox
        color='success'
        checked={boxPlotCheckbox}
        onClick={handleBoxPlotChange}
        />
        }
        label="Show Box Plots"
        />
        </div>
  • In order for this visualization to be performant and responsive, we will have to implement a caching system
    • both the svg images themselve and the state of the external checkbox and react select components are cached using localStorage
      • /* when a new graph selection is made / deleted, set select options to match
        the value of selectedOptions, tracked through the [options] parameter.
        The [selectedOptions] item in localStorage is also set to match this change
        */
        const handleSelectChange = (options) => {
        setSelectedOptions(options);
        localStorage.setItem('selectedOptions', JSON.stringify(options));
        };
        const saveOverTimeSvgToLocalStorage = (svgElement) => {
        // svg is saved in local storage as a src url
        const overTimeSVGData = new XMLSerializer().serializeToString(svgElement);
        localStorage.setItem('overTimeSVG', overTimeSVGData);
        setOverTimeChartSVG(overTimeSVGData);
        };
        const saveBoxPlotSvgToLocalStorage = (svgElement) => {
        const boxSVGData = new XMLSerializer().serializeToString(svgElement)
        localStorage.setItem('boxPlotSVG', boxSVGData)
        setBoxPlotSVG(boxSVGData)
        };
      • useEffect(() => {
        const boxDataString = boxPlotCheckbox.toString();
        localStorage.setItem('boxPlotCheckbox', boxDataString);
        }, [boxPlotCheckbox]);
        useEffect(() => {
        const yourStatsDataString = yourStatsCheckbox.toString();
        localStorage.setItem('yourStatsCheckbox', yourStatsDataString);
        }, [yourStatsCheckbox])
      • useEffect(() => {
        // retrieve and set svg src and selected options from localStorage if there's data saved
        const savedOptions = localStorage.getItem('selectedOptions');
        const savedOverTimeSVG = localStorage.getItem('overTimeSVG');
        const savedBoxPlotSVG = localStorage.getItem('boxPlotSVG')
        const storedYourStatsCheckbox = localStorage.getItem('yourStatsCheckbox');
        const storedBoxPlotCheckbox = localStorage.getItem('boxPlotCheckbox');
        if (storedBoxPlotCheckbox) {
        // boolean data is stored as a string in local storage so we convert back to bool
        setBoxPlotCheckbox(storedBoxPlotCheckbox === 'true');
        }
        if (storedYourStatsCheckbox) {
        setYourStatsCheckbox(storedYourStatsCheckbox === 'true')
        }
        if (savedOptions) {
        setSelectedOptions(JSON.parse(savedOptions));
        }
        if (savedBoxPlotSVG) {
        const imgSrc = `data:image/svg+xml;base64,${btoa((encodeURIComponent(savedBoxPlotSVG)))}`;
        setBoxPlotSVG(imgSrc);
        }
        if (savedOverTimeSVG) {
        const imgSrc = `data:image/svg+xml;base64,${btoa((encodeURIComponent(savedOverTimeSVG)))}`;
        setOverTimeChartSVG(imgSrc);
        }
        }, []);
        Additionally the matplotlib pie plot is cached as a file in the project
      • """
        helper function to generate a pie chart representation of user's transaction data at a sub-category level
        plots are saved in the frontend/public folder for use by react. Each file name uses userId so each user
        has a stored unique chart
        """
        def create_pie_plot(transaction_data, userId, transactionId):
        try:
        # extract the 'name' and 'percent' fields by normailizing the the user_transaction_data JSON and and create a nx2 data frame for these fields
        rows = []
        for category in transaction_data.values():
        temp_df = pd.json_normalize(category, record_path='details')
        rows.append(temp_df)
        data = pd.concat(rows, ignore_index=True)
        labels = data['name'].tolist()
        percents = data['percent'].tolist()
        fig, ax = plt.subplots()
        ax.pie(percents, labels=labels, autopct='%.2f%%')
        root = '../frontend/public'
        filename = f'pie_chart_{userId}_{transactionId}.png'
        directory = os.path.join(root, filename)
        plt.savefig(directory, transparent=True)
        plt.close()
        except Exception as e:
        print(f"error occurred at create_pie_plot: {str(e)}")
      • useEffect(() => {
        if(mostRecentTransId != 0){
        setPieSrc(`../../public/pie_chart_${userId}_${mostRecentTransId}.png`);
        }
        }, [mostRecentTransId]);
      • https://github.com/CWMetaUCapstone/EconMetrics/blob/e87f96ff40c9694587a0d7d77ee2a357a1ff9b0e/frontend/src/ProfilePage/Profile.jsx#L332C20-L334

Your app interacts with a database (e.g. Parse)

  • My app uses Postgres with Prisma to track four different tables as well as having an intermediary table to allow for a many to many relationship because User and Goals.
  • # Database Models
    class User(db.Model):
    __tablename__ = 'User'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(120), unique=True, nullable=False)
    password = db.Column(db.String(120), nullable=False)
    city = db.Column(db.String(120), nullable=True)
    postal = db.Column(db.String(120), nullable=True)
    state = db.Column(db.String(120), nullable=True)
    salary = db.Column(db.String(120), nullable=True)
    roommates = db.Column(db.Integer, nullable=True)
    children = db.Column(db.Integer, nullable=True)
    job = db.Column(db.String(120), nullable=True)
    token = db.Column(db.String(120), nullable=True, unique=True)
    transactions = relationship("Transactions", back_populates="user")
    goals = relationship("Goals", secondary='user_goals', back_populates="users")
    similarusers = relationship("SimilarUsers", back_populates="user")
    class Transactions(db.Model):
    __tablename__ = 'Transactions'
    id = db.Column(db.Integer, primary_key=True)
    userId = db.Column(db.Integer, ForeignKey('User.id'))
    user = relationship("User", back_populates="transactions")
    rent = db.Column(db.Numeric(13, 2), nullable=True)
    utilities = db.Column(db.Numeric(13, 2), nullable=True)
    housing = db.Column(db.Numeric(13, 2), nullable=True)
    loans = db.Column(db.Numeric(13, 2), nullable=True)
    student_loans = db.Column(db.Numeric(13, 2), nullable=True)
    car_loans_and_lease = db.Column(db.Numeric(13, 2), nullable=True)
    credit_card_payments = db.Column(db.Numeric(13, 2), nullable=True)
    other_loans = db.Column(db.Numeric(13, 2), nullable=True)
    entertainment = db.Column(db.Numeric(13, 2), nullable=True)
    streaming_services = db.Column(db.Numeric(13, 2), nullable=True)
    other_entertainment = db.Column(db.Numeric(13, 2), nullable=True)
    food = db.Column(db.Numeric(13, 2), nullable=True)
    restaurants = db.Column(db.Numeric(13, 2), nullable=True)
    groceries = db.Column(db.Numeric(13, 2), nullable=True)
    medical_care = db.Column(db.Numeric(13, 2), nullable=True)
    transportation = db.Column(db.Numeric(13, 2), nullable=True)
    gas = db.Column(db.Numeric(13, 2), nullable=True)
    parking = db.Column(db.Numeric(13, 2), nullable=True)
    ride_share = db.Column(db.Numeric(13, 2), nullable=True)
    public_transit = db.Column(db.Numeric(13, 2), nullable=True)
    other_transportation = db.Column(db.Numeric(13, 2), nullable=True)
    merchandise = db.Column(db.Numeric(13, 2), nullable=True)
    retail = db.Column(db.Numeric(13, 2), nullable=True)
    apparel = db.Column(db.Numeric(13, 2), nullable=True)
    e_commerce = db.Column(db.Numeric(13, 2), nullable=True)
    electronics = db.Column(db.Numeric(13, 2), nullable=True)
    pet_supplies = db.Column(db.Numeric(13, 2), nullable=True)
    super_stores = db.Column(db.Numeric(13, 2), nullable=True)
    other_merchandise = db.Column(db.Numeric(13, 2), nullable=True)
    other_expenses = db.Column(db.Numeric(13, 2), nullable=True)
    gym_membership = db.Column(db.Numeric(13, 2), nullable=True)
    financial_planning = db.Column(db.Numeric(13, 2), nullable=True)
    legal_services = db.Column(db.Numeric(13, 2), nullable=True)
    insurance = db.Column(db.Numeric(13, 2), nullable=True)
    tax_payments = db.Column(db.Numeric(13, 2), nullable=True)
    travel = db.Column(db.Numeric(13, 2), nullable=True)
    investment_and_saving = db.Column(db.Numeric(13, 2), nullable=True)
    investment = db.Column(db.Numeric(13, 2), nullable=True)
    savings_account = db.Column(db.Numeric(13, 2), nullable=True)
    time = db.Column(db.DateTime, server_default=func.now())
    transaction_date = db.Column(db.Integer, nullable=True)
    class Goals(db.Model):
    __tablename__ = 'Goals'
    id = db.Column(db.Integer, primary_key=True)
    users = relationship("User", secondary='user_goals', back_populates="goals")
    category = db.Column(db.String)
    target = db.Column(db.Integer)
    createdAt = db.Column(db.DateTime, server_default=func.now())
    class SimilarUsers(db.Model):
    __tablename__ = 'SimilarUsers'
    id = db.Column(db.Integer, primary_key=True)
    user = relationship("User", back_populates="similarusers")
    userId = db.Column(db.Integer, ForeignKey('User.id'))
    similarId = db.Column(db.Integer, unique=True)
    rent = db.Column(db.Numeric(13, 2), nullable=True)
    utilities = db.Column(db.Numeric(13, 2), nullable=True)
    housing = db.Column(db.Numeric(13, 2), nullable=True)
    loans = db.Column(db.Numeric(13, 2), nullable=True)
    student_loans = db.Column(db.Numeric(13, 2), nullable=True)
    car_loans_and_lease = db.Column(db.Numeric(13, 2), nullable=True)
    credit_card_payments = db.Column(db.Numeric(13, 2), nullable=True)
    other_loans = db.Column(db.Numeric(13, 2), nullable=True)
    entertainment = db.Column(db.Numeric(13, 2), nullable=True)
    streaming_services = db.Column(db.Numeric(13, 2), nullable=True)
    other_entertainment = db.Column(db.Numeric(13, 2), nullable=True)
    food = db.Column(db.Numeric(13, 2), nullable=True)
    restaurants = db.Column(db.Numeric(13, 2), nullable=True)
    groceries = db.Column(db.Numeric(13, 2), nullable=True)
    medical_care = db.Column(db.Numeric(13, 2), nullable=True)
    transportation = db.Column(db.Numeric(13, 2), nullable=True)
    gas = db.Column(db.Numeric(13, 2), nullable=True)
    parking = db.Column(db.Numeric(13, 2), nullable=True)
    ride_share = db.Column(db.Numeric(13, 2), nullable=True)
    public_transit = db.Column(db.Numeric(13, 2), nullable=True)
    other_transportation = db.Column(db.Numeric(13, 2), nullable=True)
    merchandise = db.Column(db.Numeric(13, 2), nullable=True)
    retail = db.Column(db.Numeric(13, 2), nullable=True)
    apparel = db.Column(db.Numeric(13, 2), nullable=True)
    e_commerce = db.Column(db.Numeric(13, 2), nullable=True)
    electronics = db.Column(db.Numeric(13, 2), nullable=True)
    pet_supplies = db.Column(db.Numeric(13, 2), nullable=True)
    super_stores = db.Column(db.Numeric(13, 2), nullable=True)
    other_merchandise = db.Column(db.Numeric(13, 2), nullable=True)
    other_expenses = db.Column(db.Numeric(13, 2), nullable=True)
    gym_membership = db.Column(db.Numeric(13, 2), nullable=True)
    financial_planning = db.Column(db.Numeric(13, 2), nullable=True)
    legal_services = db.Column(db.Numeric(13, 2), nullable=True)
    insurance = db.Column(db.Numeric(13, 2), nullable=True)
    tax_payments = db.Column(db.Numeric(13, 2), nullable=True)
    travel = db.Column(db.Numeric(13, 2), nullable=True)
    investment_and_saving = db.Column(db.Numeric(13, 2), nullable=True)
    investment = db.Column(db.Numeric(13, 2), nullable=True)
    savings_account = db.Column(db.Numeric(13, 2), nullable=True)
    # the user_goals table serves as an intermediary between users and goals to facilitate the many-to-many relationship
    user_goals = db.Table('user_goals',
    db.Column('id', db.Integer, primary_key=True),
    db.Column('user_id', db.Integer, db.ForeignKey('User.id')),
    db.Column('goal_id', db.Integer, db.ForeignKey('Goals.id')),
    )

Your app integrates with at least one API (that you didn’t learn about in CodePath) – free APIs only

  • my app uses three APIs, Plaid for getting a users bank account info, Back4APP for a dataset of BLS recognized occupations, and Mapbox for their address autocomplete feature.
  • # Plaid Client Initialization
    def create_plaid_client():
    configuration = Configuration()
    configuration.host = "https://sandbox.plaid.com"
    configuration.api_key['clientId'] = os.getenv('PLAID_CLIENT_ID')
    configuration.api_key['secret'] = os.getenv('PLAID_SECRET')
    configuration.ssl_ca_cert = certifi.where()
    api_client = ApiClient(configuration)
    return plaid_api.PlaidApi(api_client)
  • @app.route('/api/exchange_public_token/<userId>', methods=['POST'])
    def exchange_public_token(userId):
    public_token = request.json['public_token']
    try:
    exchange_request = ItemPublicTokenExchangeRequest(
    public_token=public_token
    )
    exchange_response = plaid_client.item_public_token_exchange(exchange_request)
    access_token = exchange_response['access_token'].encode('utf-8')
    item_id = exchange_response['item_id']
    user = User.query.get(userId)
    # store the user's access token in user table, an access token provides repeated access to user's plaid-data
    # this token is encrypted for security
    # Encrypt the token
    key = os.getenv('ENCRYPTION_KEY')
    cipher = Fernet(key)
    encrypted_token = cipher.encrypt(access_token)
    user.token = encrypted_token.decode('utf-8')
    db.session.commit()
    return jsonify({'message': 'Access token exchanged successfully'})
    except Exception as e:
    print(f"error at exchange_public_token: {str(e)}")
    return jsonify({'error at exchange_public_token': str(e)}), 500
  • """
    this function handles the transaction sync endpoint for updating user transactions
    this is designed as a helper so it can be used both by the flask route and in schedular cron job
    """
    def transactions_sync(userId):
    try:
    user = User.query.get(userId)
    # decrypt the access token from the database to use it
    encrypted_token = user.token
    key = os.getenv('ENCRYPTION_KEY')
    cipher = Fernet(key)
    decrypted_token = cipher.decrypt(encrypted_token.encode('utf-8')).decode('utf-8')
    request = TransactionsSyncRequest(access_token=decrypted_token)
    transactions = plaid_client.transactions_sync(request)
    # transaction object is by default non-serialable hence why we transform it to a dictionary
    transactions_data = {"transactions": []}
    for transaction in transactions.added:
    transactions_data["transactions"].append(transaction.to_dict())
    clean_data = clean_transaction_data(transactions_data)
    user_transaction_data = aggregate_user_data(clean_data)
    db_status = save_transaction(userId, user_transaction_data)
    return jsonify({'message': db_status, 'data': user_transaction_data})
    except Exception as e:
    print(f"error at transactions_sync: {str(e)}")
    return jsonify({'error': 'error at transactions_sync'}), 500
  • https://github.com/CWMetaUCapstone/EconMetrics/blob/e87f96ff40c9694587a0d7d77ee2a357a1ff9b0e/frontend/src/ProfilePage/FormPages/PlaidLink.jsx
  • <AddressAutofill accessToken={import.meta.env.VITE_MAPBOX_KEY} onSelect={handleAddressSelect}>
    <input type="text" placeholder='Address' autoComplete="street-address"required/>
    <label>City</label>
    <input type="text" name="city" autoComplete='address-level2' onChange={handleInputChange} readOnly />
    <label>State</label>
    <input type="text" name="state" autoComplete='address-level1' onChange={handleInputChange} readOnly />
    <label>Postal Code</label>
    <input type="text" name="postal" autoComplete='postal-code' onChange={handleInputChange} readOnly />
    </AddressAutofill>
  • /*
    helper function to fetch list of jobs matching [searchTerm] from the
    back4app BLS occupations list endpoint
    */
    export const fetchJobTitles = async (searchTerm) => {
    const where = encodeURIComponent(JSON.stringify({
    "title": {
    "$regex": searchTerm,
    "$options": "i"
    }
    }));
    const headers = {
    'X-Parse-Application-Id': import.meta.env.VITE_API_BLS_ID,
    'X-Parse-REST-API-Key': import.meta.env.VITE_API_BLS_KEY
    };
    return fetch(`https://parseapi.back4app.com/classes/Occupations_Job?limit=10&keys=title&where=${where}`, {
    headers: headers
    })
    .then(response => response.json())
    .then(data => {
    if (data.results) {
    // map the fetched options into the value-label form react-select uses
    const jobOptions = data.results.map(job => ({ value: job.title, label: job.title }));
    return(jobOptions)
    } else {
    console.error('No results found for jobOptions');
    }
    })
    .catch(error => console.error('Error fetching jobs:', error));
    }

You can log in/log out of your app as a user

You can sign up with a new user profile

Your app has multiple views

yes my app uses a total of seven differnt routes to allows users to search if they're logged in or out and navigate the various pages of my project

  • <BrowserRouter>
    <Routes>
    <Route path="/" element={
    <Navigate replace to={`/search/${menloPark}`}/>
    }/>
    <Route path="/profile/:userId" element={<Profile />}/>
    <Route path="/signup" element={<SignUp/>}/>
    <Route path="/signin" element={<SignIn/>}/>
    <Route path="/createprofile/:userId" element={<CreateAccount/>}/>
    <Route path="/search/:searchTerm" element={<SearchResults/>}/>
    <Route path="/profile/:userId/search/:searchTerm" element={<SearchResults/>}/>
    <Route path = "/goals/:userId" element={<Goals/>}/>
    </Routes>
    </BrowserRouter>

Your app has an interesting cursor interaction (e.g. a custom tooltip on hover)

yes, I used two different tool tips to help give users more context on their transactions

  • <h3>Your Transactions Breakdown</h3>
    <div className='TransactionToolTip'>
    <h3>How do I read my transaction breakdown?</h3>
    <span className='TransactionToolTipText'>
    Your transaction breakdown is based on your weekly expenditure (starting from Monday) and is framed in terms of percentages of how much you spent. So if you
    see x.xx under "resturants", that means of the money you spent last week, x.xx% of it was spending at resturants. For other two columns "Average of Similar Users" shows you
    the median perentage of expenditure spent on that category by user's who we think you're comparable to. Lastly the Difference column shows you
    your percent minus the median of similar users so you can see how exactly your habits compare to those similar to you.
    </span>
    </div>
  • <div className='ToolTip'>
    <h3>What is a box plot?</h3>
    {/* source: https://en.wikipedia.org/wiki/Box_plot */}
    <span className='ToolTipText'>
    A box plot is a method in descriptive statistics of displaying the spread of a set of numerical values in terms
    of quantiles. The height of the box itself represents the middle 50% of values, so
    half the cumulative value of points in the plot will be within the box's area and the upper and lower quarters of
    cumulative point value lay above and below the box respectively. The horizontal line within the box represents the median value
    for the data set. The two horizontal lines outside of the box represent are the minimum and maximum, or the threshold values past which data points
    are considered outliers. These lines are found using an equation that considers the intraquartile range (the height of the box) times 1.5 +/- the 75th or 25th percentiles respectively.
    Lastly, the two vertical lines, or "whiskers" describe the spread of the 25th and 75th percentiles
    </span>
    </div>

Your app demonstrates at least one component with complex visual styling (done by you, from scratch)

  • yes, I think the chart components I made for my second TC are a glaring example, but another non TC realted component whose styling I'm very proud of is the Search bar with the animated dropdown for seeing results with an emoji pattern matched to the goal category
  • <div className='searchContainer'>
    <div className='searchbar'>
    <div className='icon'>
    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" fill="#F7F9FB" className="bi bi-search" viewBox="0 0 16 16">
    <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
    </svg>
    </div>
    <input type="text" value={query} placeholder='Search by City, Job Title, or Salary' className='input'
    onFocus={() => setShowResults(true)}
    onBlur={() => setTimeout(() => {
    setShowResults(false);
    setSearchResults(defaultSearch);
    }, 150)}
    onChange={(e) => setQuery(e.target.value)}
    />
    </div>
    <div className={`results ${showResults ? 'show' : ''}`}>
    <ul>
    {searchResults.map((result, index) => (
    <li key={index} onClick={(e) => {
    e.stopPropagation();
    searchRouterHelper(result)}}>
    <div className='listContent'>
    {result.category === 'salary' && <p>💲</p>}
    {result.category === 'city' && <p>🏢</p>}
    {result.category === 'job' && <p>📄</p>}
    {result.label}
    </div>
    </li>
    ))}
    </ul>
    </div>
  • .results {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background-color: #F7F9FB;
    border: 1px solid #ccc;
    z-index: 1;
    visibility: hidden;
    opacity: 0;
    transition: visibility 0s, opacity 0.3s linear;
    display: flex;
    align-items: center;
    font-family: "Inter Tight", sans-serif;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }
    .results.show {
    visibility: visible;
    opacity: 1;
    animation: fadeInFromLeft 0.3s ease-out forwards;
    }
    .results ul {
    list-style-type: none;
    padding: 0;
    margin: 0;
    align-items: center;
    }
    li {
    padding: 8px;
    height: 4vh;
    width: 302px;
    border-radius: 5px;
    }
    .results li:hover {
    cursor: pointer;
    background-color: #e1e1e1;
    }
    .results li:first-child {
    margin-top: 2%;
    }
    .results li:last-child {
    margin-bottom: 2%;
    }
    .results p {
    font-size: 15pt;
    display: inline;
    margin-right: 6%;
    margin-left: 2%;
    }
    @keyframes fadeInFromLeft {
    from {
    opacity: 0;
    transform: translateX(-20px);
    }
    to {
    opacity: 1;
    transform: translateX(0);
    }
    }

Your app uses a loading state to create visual polish

  • yes I use loading states both after connecting account to plaid and when the large amount of data based on your profile must be fetched in the profile hub component. i did these using the react-spinner library
  • const { open } = usePlaidLink(config);
    return (
    <>
    {loading ? (
    <div className="LoadOverlay">
    <Oval
    height="80"
    width="80"
    color="#4fa94d"
    />
    </div>
    ) : (
    <div>
    <button className='plaid-btn' onClick={() => token && open()}>
    Connect Bank Account &gt;
    </button>
    </div>
    )}
    </>
    );
    }
  • return (
    <>
    {loading ? (
    <div className="LoadOverlay">
    <Oval
    height="80"
    width="80"
    color="#4fa94d"
    />
    </div>
    ) : (
    <>
    <div className='TopBar'>
    <ProfileTopBar/>
    </div>
    <div className='ProfileInfo'>
    <div className='ProfileContent'>
    <h2> Profile Info </h2>
    <div className='ProfileSide'>
    <div className='ProfilePart'>
    <p>City: {profileData.city}, {profileData.state} </p>
    </div>
    <div className='ProfilePart'>
    <p>Salary Range: {profileData.salary}</p>
    </div>
    <div className='ProfilePart'>
    <p>Roommates: {profileData.roommates}</p>
    </div>
    </div>
    <div className='ProfileSide'>
    <div className='ProfilePart'>
    <p>Children: {profileData.children}</p>
    </div>
    <div className='ProfilePart'>
    <p>Job: {profileData.job}</p>
    </div>
    </div>
    <button className='EditProfileBtn' onClick={showModal}>Edit Profile</button>
    </div>
    </div>
    <div className='TransactionInfo'>
    <div className='TransactionContent'>
    <h2>Transactions Info</h2>
    <div className='TransactionsTitle'>
    <h3>Your Transactions Breakdown</h3>
    <div className='TransactionToolTip'>
    <h3>How do I read my transaction breakdown?</h3>
    <span className='TransactionToolTipText'>
    Your transaction breakdown is based on your weekly expenditure (starting from Monday) and is framed in terms of percentages of how much you spent. So if you
    see x.xx under "resturants", that means of the money you spent last week, x.xx% of it was spending at resturants. For other two columns "Average of Similar Users" shows you
    the median perentage of expenditure spent on that category by user's who we think you're comparable to. Lastly the Difference column shows you
    your percent minus the median of similar users so you can see how exactly your habits compare to those similar to you.
    </span>
    </div>
    </div>
    <div className='ag-theme-alpine' style={{ height: 400, width: 800}}>
    <AgGridReact
    rowData={rows}
    columnDefs={columnDefs}
    autoGroupColumnDef={autoGroupColumnDef}
    treeData={true}
    getDataPath={getDataPath}
    groupDefaultExpanded={0}
    />
    </div>
    <div className='TransactionsTitle'>
    <h3>Visualized Data</h3>
    </div>
    <div className='piePlotTitle'>
    <h4>Your Expenditure Breakdown</h4>
    </div>
    <div className='piePlot'>
    <img src={pieSrc}/>
    </div>
    <div className='graphSelect'>
    <Select
    placeholder="Graph…"
    closeMenuOnSelect={true}
    components={animatedComponents}
    options={selectData}
    isClearable={true}
    className='graphSelector'
    isMulti
    onChange={handleSelectChange}
    value={selectedOptions}
    />
    </div>
    <div className='TimeChart'>
    <TimeChart data={overTimeChartData} onSaveSvg={saveOverTimeSvgToLocalStorage}/>
    </div>
    <div className='ToolTip'>
    <h3>What is a box plot?</h3>
    {/* source: https://en.wikipedia.org/wiki/Box_plot */}
    <span className='ToolTipText'>
    A box plot is a method in descriptive statistics of displaying the spread of a set of numerical values in terms
    of quantiles. The height of the box itself represents the middle 50% of values, so
    half the cumulative value of points in the plot will be within the box's area and the upper and lower quarters of
    cumulative point value lay above and below the box respectively. The horizontal line within the box represents the median value
    for the data set. The two horizontal lines outside of the box represent are the minimum and maximum, or the threshold values past which data points
    are considered outliers. These lines are found using an equation that considers the intraquartile range (the height of the box) times 1.5 +/- the 75th or 25th percentiles respectively.
    Lastly, the two vertical lines, or "whiskers" describe the spread of the 25th and 75th percentiles
    </span>
    </div>
    <div className='Checkboxes'>
    <FormControlLabel
    control={
    <Checkbox
    color='success'
    checked={yourStatsCheckbox}
    onChange={handleYourStatsBoxChange}
    />
    }
    label="Show Your Stats"
    />
    <FormControlLabel
    control={
    <Checkbox
    color='success'
    checked={boxPlotCheckbox}
    onClick={handleBoxPlotChange}
    />
    }
    label="Show Box Plots"
    />
    </div>
    <div className='BoxPlot'>
    <CompBoxPlot
    userData={transactions} similarUserData={similarUsers} OnClickedUserId={setClickedUserId}
    onSaveSvg={saveBoxPlotSvgToLocalStorage} showBoxPlots={boxPlotCheckbox} showUserData={yourStatsCheckbox}
    OnBoxClick={setClickedBoxDetails}/>
    </div>
    <EditProfileModal view={showEditProfileModal} closeView={closeModal} profileData={profileData}/>
    {
    clickedUserId !== 0 ? (
    <div className='ClickedDetails'>
    <p>City: {clickedUserDetails.city}</p>
    <p>Salary: {clickedUserDetails.salary}</p>
    <p>Job: {clickedUserDetails.job}</p>
    <p>Children: {clickedUserDetails.children}</p>
    <p>Roomamtes: {clickedUserDetails.roommates}</p>
    </div>
    ) : (
    <div></div>
    )
    }
    {
    Object.keys(clickedBoxDetails).length > 0 ? (
    <div className='ClickedDetails'>
    <p>25th Percentile: {clickedBoxDetails.quartiles[0].toFixed(2)} </p>
    <p>Median: {clickedBoxDetails.quartiles[1].toFixed(2)}</p>
    <p>75th Percentile: {clickedBoxDetails.quartiles[2].toFixed(2)}</p>
    <p>Upper Outlier Range: {clickedBoxDetails.range[1].toFixed(2)}</p>
    <p>Lower Outlier Range: {clickedBoxDetails.range[0].toFixed(2)}</p>
    </div>
    ) : (
    <div></div>
    )
    }
    </div>
    </div>
    </>
    )}
    </>
    )
    }

Project Planner Doc

https://docs.google.com/document/d/1x0Bo--S4QJQHxFEerv1K9MrlYiCxGJDZhKDZ9Rg-dz0/edit?usp=sharing

Figma Wireframes for Codepath

Screenshot 2024-06-24 at 10 31 57 AM Screenshot 2024-06-24 at 10 50 03 AM

About

Carson Wolber summer 2024 Meta University Capstone Project

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages