DEV Community

Victor Del Carpio
Victor Del Carpio

Posted on

Building a Spotify Journal

Introduction

For my Phase 2 final project at the Flatiron School's Software Engineering program, I developed an online journal that integrated a user's Spotify history so they could write about the music they listen to.

Background

In this project, I used Vite and React to create the online journal. The key functionalities were implemented using axios for making asynchronous HTTP requests to Spotify's API endpoints, as well as the json-server. I used Material UI for the styling and used Yup as the form provider.

Implementation

For the sake of code organization, I made a file that handled all of the API calls and would make each of the GET requests easy to run. This file would specifically make calls to Spotify API endpoints and to the json-server. The code looked like the following:

import axios from "axios";

export async function getUserId(token) {
    const endpoint = `https://api.spotify.com/v1/me`;
    const response = await axios.get(endpoint, {
        headers: {
            Authorization: `Bearer ${token}`,
        }
    });
    return response.data;
}

export async function getUserFavorites(type, time_range, limit = 20, token) {
    const endpoint = `https://api.spotify.com/v1/me/top/${type}`;
    const response = await axios.get(endpoint, {
        headers: {
            Authorization: `Bearer ${token}`,
        },
        params: {
            time_range: time_range,
            limit: limit,
        },
    });
    return response.data.items;
}

export async function getRecentTracks(token) {
    const endpoint = "https://api.spotify.com/v1/me/player/recently-played";
    const response = await axios.get(endpoint, {
        headers: {
            Authorization: `Bearer ${token}`,
        },
        params: {
            limit: 50,
        },
    });
    const filtered = response.data.items.map((track) => track.track);
    return filtered;
}

export async function getTrack(token, id) {
    const endpoint = `https://api.spotify.com/v1/tracks/${id}`;
    const response = await axios.get(endpoint, {
        headers: {
            Authorization: `Bearer ${token}`
        }
    });
    return response.data;
}

export async function getJournalEntries(id) {
    const endpoint = `http://localhost:4000/entries?user_id=${id}`;
    const response = await axios.get(endpoint);
    return response.data;
}

Enter fullscreen mode Exit fullscreen mode

Implementing User Authentication

To enable user login without relying on an Express server, I implemented a straightforward authentication process. Users accessed a designated URL for authentication, which then provided an access token upon successful login. This access token was centrally stored in a context to ensure universal accessibility across all application pages and components.

Designing the Web Application

The web application's architecture revolved around React's Router, enabling seamless navigation between its four distinct pages. Leveraging useRoutes, certain pages shared a cohesive layout, enhancing user experience and maintaining visual consistency.

The application's components are methodically organized within designated folders, ensuring clarity and maintainability within the project structure.

Crafting the Journal Entry Form

One of the project's highlights was the intricate design of the Journal Entry form. Powered by tools like Material UI and Yup for styling and form validation, respectively, the form offered a user-friendly interface for inputting journal entries. Notably, the form included:

  • Text input fields with character limits for titles and descriptions.
  • A date picker component facilitated by RHFDatePicker for selecting precise dates.
import PropTypes from 'prop-types';
import { FormProvider as Form } from 'react-hook-form';
import React from 'react';

FormProvider.propTypes = {
    children: PropTypes.node,
    methods: PropTypes.object,
    onSubmit: PropTypes.func,
};

export default function FormProvider({ children, onSubmit, methods }) {
    return (
        <Form {...methods}>
            <form onSubmit={onSubmit}>{children}</form>
        </Form>
    );   
}
Enter fullscreen mode Exit fullscreen mode

From here on out the code generally looks like the following:

<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
    <Stack spacing={3} sx={{ my: 2, width: "50vw" }}>
        {/* Display error alert if submission fails */}
        {!!errors.afterSubmit && (
            <Alert severity="error">{errors.afterSubmit.message}</Alert>
        )}

        {/* Display track information if available */}

        {/* Text input for title with character limit */}
        <RHFTextField name="title" label="Title" charLimit={100} />

        {/* Multiline input for description with character limit */}
        <RHFTextField name="description" label="Description" multiline rows={5} charLimit={3000} />

        {/* Date picker for selecting a date */}
        <LocalizationProvider dateAdapter={AdapterDayjs}>
            <RHFDatePicker name="date" label="Date" />
        </LocalizationProvider>
    </Stack>

    {/* Submit button */}
    <Button>
        Post Entry
    </Button>
</FormProvider>

Enter fullscreen mode Exit fullscreen mode

In the context of my project, I integrated a custom form controller named RHFTextField to facilitate text input with enhanced functionality. This component, derived and adapted from a previous project, underwent slight modifications to align with the specific requirements of my journal application.

Component Overview

The RHFTextField component leverages Material UI's TextField and Controller from react-hook-form to encapsulate text input within a controlled form environment.

import React from "react";
import { InputAdornment, TextField } from "@mui/material";
import PropTypes from "prop-types";
import { Controller, useFormContext } from "react-hook-form";

RHFTextField.propTypes = {
    name: PropTypes.string,
    helperText: PropTypes.node,
    charLimit: PropTypes.number, // Character limit for text input
    // Additional props from Material UI TextField
};

export default function RHFTextField({ name, helperText, charLimit, ...other }) {
    const { control } = useFormContext();

    return (
        <Controller
            name={name}
            control={control}
            render={({ field, fieldState: { error } }) => {
                const remainingCharacters = charLimit - (field.value ? field.value.length : 0);

                return (
                    <TextField
                        {...field}
                        fullWidth
                        inputProps={{ maxLength: charLimit }}
                        value={typeof field.value === 'number' && field.value === 0 ? '' : field.value}
                        error={!!error}
                        helperText={error ? error?.message : helperText}
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end">
                                    {`${remainingCharacters}`}
                                </InputAdornment>
                            ),
                        }}
                        {...other}
                    />
                );
            }}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

Implementing the RHFTextField Form Controller

In the context of my project, I integrated a custom form controller named RHFTextField to facilitate text input with enhanced functionality. This component, derived and adapted from a previous project, underwent slight modifications to align with the specific requirements of my journal application.

Component Overview

The RHFTextField component leverages Material UI's TextField and Controller from react-hook-form to encapsulate text input within a controlled form environment.

import React from "react";
import { InputAdornment, TextField } from "@mui/material";
import PropTypes from "prop-types";
import { Controller, useFormContext } from "react-hook-form";

RHFTextField.propTypes = {
    name: PropTypes.string,
    helperText: PropTypes.node,
};

export default function RHFTextField({ name, helperText, charLimit, ...other }) {
    const { control } = useFormContext();

    return (
        <Controller
            name={name}
            control={control}
            render={({ field, fieldState: { error } }) => {
                const remainingCharacters = charLimit - (field.value ? field.value.length : 0);
                return (
                    <TextField
                        {...field}
                        fullWidth
                        inputProps={{ maxLength: charLimit }}
                        value={typeof field.value === 'number' && field.value === 0 ? '' : field.value}
                        error={!!error}
                        helperText={error ? error?.message : helperText}
                        InputProps={{
                            endAdornment: (
                                <InputAdornment position="end">
                                {`${remainingCharacters}`}
                                </InputAdornment>
                            ),
                        }}
                        {...other}
                    />
                );
            }}
        />
    );
}
Enter fullscreen mode Exit fullscreen mode

Key Features and Functionality

Props Definition

  • name: The name of the input field within the form.
  • helperText: Additional helper text to provide guidance or feedback.
  • charLimit: Maximum character limit for the text input field.
  • Other Material UI TextField Props: Pass-through props for configuring text input behavior (e.g., label, variant, onChange, etc.).

Form Control with Controller

The Controller component from react-hook-form manages the state and validation of the input field, ensuring seamless integration with the form's overall state.

  • Character Limit Handling: Limits the input length based on charLimit and displays the remaining character count dynamically.
  • Error Display: Highlights input errors and displays relevant error messages using Material UI's TextField component.
  • Input Adornment: Includes an input adornment (end adornment) showing the remaining character count for real-time feedback.

Top comments (0)