Major League Baguette logo
Published on

IndiesReadIt Development Diary - Week 7

Authors
Table of Contents

IndiesReadIt Development Diary - Week 7

15/01 - 2h - Refactored useBooks hook and added features.

  • I once again moved logic to useBooks custom hook, type toggle this time.
  • I made Copilot work for me most of the time, it moved the logic almost flawlessly
  • After I tested it out, I asked him to simplify the entire hook to make it more readable, it broke all the messy code in useEffect into functions
  • Final (for now) useBooks hook looks like this :
'use client';

import { Category } from '@prisma/client';
import { useQuery } from '@tanstack/react-query';
import React, { createContext, useState, useContext, useEffect } from 'react';
import { ExtendedBooks } from 'types';
import useDebounce from './useDebounce';

export const BooksContext = createContext<
  | {
      books: ExtendedBooks[];
      initialBooks: ExtendedBooks[];
      setBooks: React.Dispatch<React.SetStateAction<ExtendedBooks[]>>;
      category: Category | null;
      categorySearch: string | null;
      setCategorySearch: React.Dispatch<React.SetStateAction<string | null>>;
      isLoading: boolean;
      searchTerm: string;
      setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
      selectedBookTypes: string[];
      setSelectedBookTypes: React.Dispatch<React.SetStateAction<string[]>>;
    }
  | undefined
>(undefined);

export const BooksProvider: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const { isLoading, data: initialBooks } = useQuery({
    queryKey: ['books'],
    queryFn: async () => {
      const res = await fetch('/api/books');
      return res.json();
    },
  });
  const { data: initialCategory } = useQuery({
    queryKey: ['categories'],
    queryFn: async () => {
      const res = await fetch('/api/categories');
      return res.json();
    },
  });
  const [books, setBooks] = useState([] as ExtendedBooks[]);
  const [categorySearch, setCategorySearch] = useState<string | null>(null);
  const [category, setCategory] = useState<Category | null>(null);
  const [searchTerm, setSearchTerm] = useState('');
  const debouncedSearchTerm = useDebounce(searchTerm, 300);
  const [selectedBookTypes, setSelectedBookTypes] = useState<string[]>([]);

  const filterByBookType = (books: ExtendedBooks[]) => {
    if (!selectedBookTypes || selectedBookTypes.length === 0) return books;

    return books.filter((book) =>
      selectedBookTypes.every((type) => {
        if (type === 'physical-book') return book.isPhysical;
        if (type === 'audio-book') return book.isAudio;
        if (type === 'ebook') return book.isEbook;
        return false;
      }),
    );
  };

  const filterByCategory = (books: ExtendedBooks[]) => {
    if (!categorySearch) return books;

    return books.filter((book) => book.category.slug === categorySearch);
  };

  const filterBySearchTerm = (books: ExtendedBooks[]) => {
    if (!debouncedSearchTerm) return books;

    const lowercasedSearchTerm = debouncedSearchTerm.toLowerCase();

    return books.filter(
      (book) =>
        book.title.toLowerCase().includes(lowercasedSearchTerm) ||
        book.author.toLowerCase().includes(lowercasedSearchTerm),
    );
  };

  useEffect(() => {
    if (initialBooks) {
      let filteredBooks = initialBooks;

      filteredBooks = filterByBookType(filteredBooks);
      filteredBooks = filterByCategory(filteredBooks);
      filteredBooks = filterBySearchTerm(filteredBooks);

      setBooks(filteredBooks);
      setCategory(
        initialCategory?.find((cat: Category) => cat.slug === categorySearch) ||
          null,
      );
    }
  }, [
    initialBooks,
    categorySearch,
    initialCategory,
    debouncedSearchTerm,
    selectedBookTypes,
  ]);

  return (
    <BooksContext.Provider
      value={{
        books,
        initialBooks,
        setBooks,
        category,
        categorySearch,
        setCategorySearch,
        isLoading,
        searchTerm,
        setSearchTerm,
        selectedBookTypes,
        setSelectedBookTypes,
      }}
    >
      {children}
    </BooksContext.Provider>
  );
};

export const useBooks = () => {
  const context = useContext(BooksContext);
  if (context === undefined) {
    throw new Error('useBooks must be used within a BooksProvider');
  }
  return context;
};
  • I improved the filters bars UX by setting a very light background to highlight if toggles are on or not
  • Another cool effect of the hook is that the filter presets are saved when switching categories
  • I had to change shadcn/ui’s ToggleGroupItems to Toggle in order to use
defaultPressed={selectedBookTypes.includes("...")}
  • Finally sorted books by rating, like the subtitle said. Once again moved logic to hook, calculated the average rating there and added a new value to the ExtendedBook (averageRating)
const calculateBookRatings = (books: ExtendedBooks[]) => {
  return books.map((book) => {
    let averageRating = 0;
    if (book.rating.length > 0) {
      averageRating = getBookRatingAverage(book);
      averageRating = Number(averageRating.toFixed(2));
    }
    return { ...book, averageRating };
  });
};

16/01 - 1h30 - Polished cards, scrollbars, and animations.

  • Next major step is either “monetization” or PH launch, I still have to work for my 9-5 so I decided to work on details
  • I improved cards back by making the description scrollable (was truncated) and made it more readable.
  • Scroll bars was ugly and way too large, I decided to llok at what @d4m1n made for Shipixen and adapted it to use Tailwind (was plain CSS)
/** Scrollbar **/
  ::-webkit-scrollbar {
    @apply h-3 w-3;
  }

  ::-webkit-scrollbar-track {
    @apply bg-background;
  }

  ::-webkit-scrollbar-thumb {
    @apply rounded-2xl border-4 border-solid border-transparent bg-primary/25 bg-clip-content;
  }

  ::-webkit-scrollbar-thumb:hover {
    @apply bg-primary/50;
  }

  ::-webkit-scrollbar-corner {
    @apply bg-background;
  }
  • I shamelessly partially reused his ScrollTop component (I will specifically thank him anyway in a future about/misc page)
  • I implement autoAnimate to the BookCard parent component so cards are automatically animated when filtered or sorted.
  • It’s black magic, go try it, it’s totally adapted to fast shipping Indies
  • And it cames with a hidden bonus as you have to use the right HTML elements to make it works the way you think it should. My cards are in a grid that why the animation is perfect for this. This wouldn’t be the case if I used flex or weird structure.
  • Updated the sitemap.ts to make it generate and update all routes for each categories each time a new book is added.
  • Again Copilot saved me minutes and as a bonus learn me a smart way to do it ("take" query option from Prisma)

17/01 - 13h - Added terms, privacy pages, and details.

  • I finally added the infamous but mandatory terms & privacy pages to the app
  • I reused the data from depikt, itself reused from my wife’s agency which I trust. It may be overkill but almost everything is covered.
  • It simply a markdown file for which I set a minimal page calling the .md with markdown-to-jsx (minimal markdown for React, perfect for this case)
  • Do not forget to declare .md files in a markdwon.d.ts file
declare module '*.md' {
  const content: string;
  export default content;
}
  • I then updated the footer to add the terms and privacy pages and made the old “A project by Dany”/BuyMeACoffee duo a simpler double icon (X and BuyMe)
  • Real nomad indiehacker shit here, full work from outside on laptop with Galaxy Buds Pro (active noise suppression is life changing)
  • Updated all the fcking “buy book” links with Amazon Affiliates ones.. took me more than hour
  • Still don’t have a proper book Dashboard, I updated everything from Supabase, it’s enough for now
  • Speaking of Buy Buttons, I refactored it to shared comp with check if it’s affiliate link
const checkIfAffiliate = (link: string) => {
  return link.includes("amzn.to");
};
  • Inspired from Uneed I made a real footer with SEO oriented stuff

  • Added a proper app description for added text

  • Links to all categories because there wasn’t actual links due to the combobox (important for Google crawler)

  • Added the best rated books for keywords

  • Added another Submit book button

  • Layout was a bit challenging but went mainly for grid as I think it’s the most adapted. I used to be a flex maxi but I gain wisdom and use grid when I should

    IndiesReadIt Footer

19/01 - 2h - Preparing for MVP and ProductHunt launch.

  • The website is now close to the MVP I had in mind, I switched my focus to “marketing”
  • I set the ProductHunt page following a really nice guide from Luca Restagno
  • Nothing fancy, just fill everything you can, make simple yet appealing visuals and force yourself to make a demo video.
  • Try to not activate your video recording app on the only computer that have no webcam…
  • Here some visuals: IndiesReadIt PH Visual 2IndiesReadIt PH Visual 1IndiesReadIt PH Visual 3

Week 7 Summary

This week for IndiesReadIt focused on refining the website for an upcoming ProductHunt launch. Key highlights include optimizing for Amazon Affiliates, adding terms and privacy pages, and streamlining search functionalities. The useBooks hook underwent significant improvements, enhancing readability. Visual enhancements and strategic considerations for marketing rounded out the week.


Submit your favorite indie books to IndiesReadIt and help build this collaborative directory.

Stay tuned for more updates!