Logo

dev-resources.site

for different kinds of informations.

Remix, React, the Discogs API & filtermusikk.no

Published at
9/23/2024
Categories
react
remix
javascript
webdev
Author
mannuelf
Categories
4 categories in total
react
open
remix
open
javascript
open
webdev
open
Author
8 person written this
mannuelf
open
Remix, React, the Discogs API & filtermusikk.no

I'd like to share and hopefully inspire some of you to do more side projects.

This is how I thought about building a vinyl list web app, the process of building it, including "some" not all technical details.

First some context.

I had a "need" and an itch to get some game time with Remix.

Best way to learn is to build something, preferably something with/around something that you are interested in/passionate about.

It is no secret that I am a huge fan of music. I have been collecting and playing records over the years, on and off... and right now very much on.

I found a local record store that I like Filter Musikk, I have been buying records from them for the last little while now.

They have a Discogs store online. Discogs is marketplace for music (vinyl, cd's, cassettes) it's like eBay for music, but better.

Anyways, lets talk about code the Discogs API and how I have been playing around with it.

I thought it would be cool to build a little app that would allow me to see what they have in stock.

🎹 Live demo here
Image description

The data

I like to save my endpoints in collections in Postman, so I can easily refer to them later.

Here is the Discogs API endpoint I used to get the inventory of a user.

https://api.discogs.com/users/username/inventory
?status=status
&sort=sort
&sort_order=sort_order
&per_page=per_page
&page=page
Enter fullscreen mode Exit fullscreen mode

Fairly simple.

Discogs API collection

The plan and design

Keep it clean simple design. Focus on the music, the records and the artwork.

Discogs App design

The stack

Remix for the server side rendering. I was only concerned with learning how things work in Remix with loaders, routes and actions. So I did not build anything from scratch.

I used Tailwind CSS for the styling because it's fast and easy to use, after a second thought I decided to use shadcn/ui so I did not have to build the components from scratch either.

The code

Make use of the Remix resources for building the app, no sense reinventing the wheel.

I used the server side pagination by Jacob Paris.

Type definitions

I like to get accustomed to the data I am working with, so I define the types I will be working with. Quicktype.io is good for this job.

export interface Pagination {
  items: number;
  page: number;
  pages: number;
  per_page: number;
  urls: Record<
    string,
    {
      last: string;
      next: string;
    }
  >;
}

export interface Price {
  currency: string;
  value: number;
}

interface OriginalPrice {
  curr_abbr: string;
  curr_id: number;
  formatted: string;
  value: number;
}

interface SellerStats {
  rating: string;
  stars: number;
  total: number;
}

interface Seller {
  id: number;
  username: string;
  avatar_url: string;
  stats: SellerStats;
  min_order_total: number;
  html_url: string;
  uid: number;
  url: string;
  payment: string;
  shipping: string;
  resource_url: string;
}

interface Image {
  type: string;
  uri: string;
  resource_url: string;
  uri150: string;
  width: number;
  height: number;
}

interface ReleaseStatsCommunity {
  in_wantlist: number;
  in_collection: number;
}

interface ReleaseStats {
  community: ReleaseStatsCommunity;
}

interface Release {
  thumbnail: string;
  description: string;
  images: Image[];
  artist: string;
  format: string;
  resource_url: string;
  title: string;
  year: number;
  id: number;
  label: string;
  catalog_number: string;
  stats: ReleaseStats;
}

export interface Listing {
  id: number;
  resource_url: string;
  uri: string;
  status: string;
  condition: string;
  sleeve_condition: string;
  comments: string;
  ships_from: string;
  posted: string;
  allow_offers: boolean;
  offer_submitted: boolean;
  audio: boolean;
  price: Price;
  original_price: OriginalPrice;
  shipping_price: Record<string, unknown>;
  original_shipping_price: Record<string, unknown>;
  seller: Seller;
  release: Release;
}

export interface InventoryFetchResponse {
  pagination: Pagination;
  listings: Listing[];
}
Enter fullscreen mode Exit fullscreen mode

Inventory server component

import type { LoaderFunction, MetaFunction } from "@remix-run/node";
import { json, useLoaderData } from "@remix-run/react";
import { Footer } from "~/components/footer";
import { PaginationBar } from "~/components/paginationBar";
import { StatusAlert } from "~/components/StatusAlert";
import { fetchUserInventory } from "~/inventory";
import { Inventory } from "~/inventory/inventory";

export const loader: LoaderFunction = async ({ request }) => {
  const url = new URL(request.url);
  const searchParams = new URLSearchParams(url.search);
  const pageNumber = searchParams.get("page") || "1";

  try {
    const data = await fetchUserInventory(
      pageNumber,
      "12",
      process.env.SELLER_USERNAME,
      "for sale",
      "listed",
      "desc",
    );

    return json({ inventory: data, ENV: { sellerUsername: process.env.SELLER_USERNAME } });
  } catch (error) {
    console.log("Error fetching inventory:", error);
    return json({ error: "Failed to load inventory. Please try again later or" }, { status: 500 });
  }
};

export const Meta: MetaFunction = () => {
  const { ENV } = useLoaderData<typeof loader>();
  return [
    {
      title: `Shop ${ENV.sellerUsername} records`,
    },
    {
      name: "description",
      content: `Buy some vinyl records from ${ENV.sellerUsername}`,
    },
  ];
};

export default function Index() {
  const { inventory, error } = useLoaderData<typeof loader>();

  if (error) {
    <StatusAlert {...error} />;
  }

  if (!inventory || !inventory.pagination) {
    return <div>No inventory data available.</div>;
  }

  return (
    <>
      <Inventory {...inventory} />
      <PaginationBar total={inventory.pagination.pages} />
      <Footer />
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

The Inventory component takes the inventory data and renders it, it is the entry point for the app index page, no routes planned.

I show an alert if there is an error fetching the data, with a link to Discogs api status page.

Remix works like this:

  • you have a loader function that fetches the data server side and returns it to the component wrapped by json() function
  • the default Index function then renders the data
  • meta function is used to set the title and description of the page.

fetchUserInventory function

export const loader: LoaderFunction = async ({ request }) => {
  const url = new URL(request.url);
  const searchParams = new URLSearchParams(url.search);
  const pageNumber = searchParams.get("page") || "1";

  try {
    const data = await fetchUserInventory(
      pageNumber,
      "12",
      process.env.SELLER_USERNAME,
      "for sale",
      "listed",
      "desc",
    );

    return json({ inventory: data, ENV: { sellerUsername: process.env.SELLER_USERNAME } });
  } catch (error) {
    console.log("Error fetching inventory:", error);
    return json({ error: "Failed to load inventory. Please try again later or" }, { status: 500 });
  }
};
Enter fullscreen mode Exit fullscreen mode
  • this function is used to fetch the data from the Discogs API,
  • it takes the page number, number of items per page, seller username, status, condition, sort order as arguments.
  • it returns the data and the seller username to the loader function.
  • returns an error if there is an error fetching the data.

Inventory client component

The card component is your typical React card component, it has a header, content and footer.

I build string of the artist and song title and pass it to the link, which when clicked launches YouTube Music, its not perfect. Some times it does not find the song, but it works most of the time.

import React from "react";
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "~/components/ui/card";
import { cn } from "~/lib/utils";
import type { InventoryFetchResponse, Listing } from "./inventory.types";

export const Inventory = (data: InventoryFetchResponse): React.ReactElement => {
  return (
    <section className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 2xl:grid-cols-6 gap-4 justify-items-start pb-7">
      {data.listings.map((listing: Listing) => (
        <article
          id={listing.release.title}
          key={listing.release.title}
          className="flex justify-start w-full"
        >
          <Card className={cn("p-0 shadow-none w-full overflow-hidden")}>
            <div className="justify-items-start">
              <CardHeader
                className="flex-1 h-40 sm:h-60 md:h-90 lg:h-100 p-0 relative"
                title={listing.release.title}
                style={{
                  backgroundImage: listing.release.images[0]
                    ? `url(${listing.release.images[0].uri})`
                    : "",
                  backgroundSize: "cover",
                  backgroundPosition: "center",
                }}
              >
                {listing.condition ?? listing.condition}
              </CardHeader>
              <CardContent className="flex-1 pt-4">
                <CardTitle className="text-sm">
                  {listing.release.title}
                </CardTitle>
                <CardDescription className="leading-6 text-black">
                  <strong>Artist:</strong> {listing.release.artist}
                  <br />
                  <strong>Label:</strong> {listing.release.label} -{" "}
                  {listing.release.catalog_number}
                  <br />
                  <strong>Released:</strong> {listing.release.year}
                  <br />
                  <strong>Price:</strong> {listing.original_price.formatted}
                </CardDescription>
                <CardDescription className="leading-6">
                  <span className="grid grid-cols-1 md:grid-cols-2 gap-1">
                    <a
                      href={listing.uri}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="text-xs text-white border-0 border-black-600 bg-black hover:text-color-black rounded-md p-2 mt-2 "
                    >
                      View on Discogs
                    </a>
                    <a
                      href={`https://music.youtube.com/search?q=${
                        encodeURIComponent(
                          listing.release.title,
                        )
                      } ${encodeURIComponent(listing.release.artist)}`}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="flex justify-start text-xs text-white border-0 border-black-600 bg-black hover:text-color-black rounded-md p-2 mt-2"
                    >
                      <span className="w-[24px] mt-[2px]">
                        <img
                          src={"./icon-youtube.svg"}
                          alt="YouTube"
                          width={16}
                          height={16}
                        />
                      </span>
                      <span>Listen</span>
                    </a>
                  </span>
                </CardDescription>
              </CardContent>
            </div>
            <CardFooter className="p-0"></CardFooter>
          </Card>
        </article>
      ))}
    </section>
  );
};
Enter fullscreen mode Exit fullscreen mode

The result

Live demo here

Discogs App result

Conclusion

I enjoy the plug and play nature of Remix and the freedom to do as I please.

The documentation is good and easy to follow, it's easy to get started and build something quickly.

The data flow concept is easy to understand makes doing SSR much more approachable than it was in the past.

Data fetching with the loader and hooks to get the data out its concise, did not get the chance yet to use an action as I will need to build form (coming up next a site search).

Remix focus on performance and Web standards, which I like. Did not reach for Axios or anything like that (TanStack query) because I did not need to, the fetch API is good enough for simple tasks like this.

When it Remix merges into React Router 7 I will continue to use it, I will take this on a case by case basis, depending on the project and types of problems we trying to solve.

Looking else where/alternatives would be to use NextJS or maybe look at react server components next.

Ask me again in the future when React 19 is out.

Get the code on GitHub.

remix Article's
30 articles in total
Favicon
How to disable a link in React Router 7 / Remix
Favicon
Verifying Your Shopify App Embed is Actually Enabled with Remix: A Step-by-Step Guide
Favicon
Headless e-commerce structure
Favicon
πŸš€ OpenAI's Transition from Next.js to Remix: A Strategic Move in Web Development 🌐
Favicon
# Key New Features in React Router 7: Embracing the Remix Future
Favicon
React Router V7: A Crash Course
Favicon
Stop Running to Next.js β€” Remix is the Future of React, and Here’s Why You’re Missing Out
Favicon
Introducing React Page Tracker: Simplify Navigation Tracking
Favicon
Create an Admin Panel for your project in 5Β minutes
Favicon
Remix Drizzle Auth Template
Favicon
I used GitHub as a CMS
Favicon
Remix vs. Next.js: Why Choose Remix?
Favicon
Choosing Remix as a Server-Side Rendering (SSR) Framework
Favicon
Next.js vs Remix: Which Framework is Better?
Favicon
Using PostHog in Remix Loaders and Actions on Cloudflare Pages
Favicon
Creating a marketplace with Stripe Connect: The onboarding process
Favicon
In-Depth Analysis of Next.js, Gatsby, and Remix
Favicon
Implementing RSS feed with Remix
Favicon
Cloudflare + Remix + PostgreSQL with Prisma Accelerate's Self Hosting
Favicon
Membangun Aplikasi Verifikasi Kode Autentikasi dengan Twilio Menggunakan Go dan Remix
Favicon
Google Analytics (GA4) implementation with React - Remix example
Favicon
Day 3 of Brylnt: Next.js vs Remix
Favicon
𝐌𝐚𝐧𝐭𝐒𝐧𝐞 𝐁𝐨𝐚𝐫𝐝𝐬 πŸš€
Favicon
How to Integrate Mixpanel with Remix?
Favicon
Remix, React, the Discogs API & filtermusikk.no
Favicon
How to navigate between Shopify app
Favicon
Framer Motion useAnimation()
Favicon
An Introduction to Remix
Favicon
Remix vs Next.js: Which Framework Should You Choose?
Favicon
Remix vs Next.js: Which Framework Should You Choose?

Featured ones: