🐍

Comment fonctionne Focused Shares ?

On va voir ensemble comment j'ai créé mon application Focused Shares avec Next.js. L'application permet de regrouper des musiques lofi, jazz, etc.

Comment fonctionne Focused Shares ?

On va voir ensemble comment j’ai créé mon application Focused Shares avec Next.js. L’application permet de regrouper des musiques lofi, jazz, etc., de YouTube et de les assembler sur une page profil, comme ici : mon profile. C’est un peu comme Linktree, mais cette fois pour des musiques lofi, jazz, et autres.

Table of contents

Ouvrir la table des matières

La stack technique utilisée

  • Next.js 14
  • Tailwind CSS
  • Radix UI
  • Lucia (une librairie d’authentification)
  • ImageKit (pour la gestion des images de profil)

Next.js 14

J’ai choisi Next.js pour pouvoir développer rapidement mon application, en seulement 6 jours. Cette techno est top pour ça, surtout avec les server actions et les forms actions qui simplifient la gestion des formulaires sans avoir besoin de taper dans une API.

Tailwind CSS

Pour donner un style au design de l’appli et bien gérer la mise en page, Tailwind CSS est super pratique. Ça aide à rendre tout ça flexible et rapide à ajuster.

Radix UI

Pour cette première version de l’application, Radix UI était parfait pour ajouter rapidement des composants. Mais attention, si tu veux un design system super précis, ça pourrait être un peu limitant.

Lucia

J’ai testé Lucia parce que la version 3 venait de sortir et franchement, c’est une belle découverte. Ça se pose là comme une bonne alternative à next-auth.

ImageKit

Pour les images de profil, rien de mieux qu’ImageKit. Avec leur CDN, je peux stocker et gérer les images facilement via leur API simple d’utilisation.

Fonctionnement

Au début du projet, il fallait prioriser certaines fonctionnalités clés pour l’application :

  • Authentification : Essentielle pour permettre l’ajout de musique dans l’appli.
  • Mise en place de l’API Youtube Data Api v3
  • Possibilité d’ajouter de la musique : Pour enrichir le contenu disponible aux utilisateurs.

(1) Authentification : Pour garder les choses simples, j’ai choisi d’utiliser OAuth 2.0 de Google. Ça évite les tracas de gestion d’email et de mot de passe. S’authentifier via Google, c’est pratique et ça allège le processus dans n’importe quelle appli, en éliminant la fatigue de devoir toujours entrer un email et un mot de passe.

(2) YouTube Data API v3 : J’utilise cette API pour extraire les données des vidéos YouTube que je souhaite intégrer dans l’application. Voici comment je procède pour récupérer les informations nécessaires des vidéos :

/**
 * Get the song data from youtube api
 * @param videoId the video id
 * @returns the song data
 */
export const songData = async (videoId: string) => {
  const response = await fetch(
    `https://www.googleapis.com/youtube/v3/videos?part=snippet,contentDetails,statistics&id=${videoId}&key=${env.GOOGLE_API_KEY}`,
    {
      method: "GET",
      headers: {
        "Content-Type": "application/json",
      },
    }
  );

  return response.json();
};

(3) Avant d’ajouter la musique dans la base de données, je procède à plusieurs petites vérifications :

  • Vérifier si l’utilisateur est bien connecté : pour être sûr que la personne qui ajoute la musique est bien celle qui a le droit de le faire.
  • Extraire l’ID de la vidéo YouTube à partir de l’URL : c’est nécessaire pour savoir quelle vidéo l’utilisateur veut ajouter.
  • Vérifier si la vidéo existe déjà dans la base de données avec l’ID de la vidéo YouTube que j’ai extrait de l’URL : pour éviter de dupliquer les vidéos déjà présentes.
  • Si la vidéo existe déjà, je renvoie un message d’erreur car la vidéo est déjà présente dans la base de données.
  • Si la vidéo n’existe pas, alors je l’ajoute dans la base de données avec l’ID de la vidéo YouTube que j’ai extrait de l’URL et je renvoie un message de succès.

Upload d’image côté front-end

Après avoir implémenté les fonctionnalités principales de l’application, j’ai décidé de rajouter une gestion des photos de profil.

La création du composant pour uploader la photo de profil était assez compliquée, mais au final, je m’en sors très bien avec ce composant car il répond bien à mon projet.

Upload image component

Upload d’image côté back-end

Maintenant que j’ai mon composant, j’ai besoin de le faire fonctionner. Pour commencer, je liste les types d’images que l’utilisateur peut uploader.

Je décide de les lister :

const ACCEPTED_IMAGE_TYPES: ReadonlyArray<string> = [
  "jpeg",
  "jpg",
  "png",
  "webp",
];

et par la même occasion, je fais pareil pour la taille de l’image :

const MAX_FILE_SIZE = 1024 * 1024 * 5; // 5 MB

Pour valider les entrées de mon formulaire, j’utilise la librairie zod et la méthode refine pour valider les types d’image et la taille de l’image et retourner un message d’erreur :

const schema = z
  .object({
    avatar: z
      .any()
      .refine(file => {
        return file.size <= MAX_FILE_SIZE;
      }, `Max image size is 5 MB.`)
      .refine(
        files => {
          return ACCEPTED_IMAGE_TYPES.includes(files.type.split("/")[1]);
        },
        `Only ${ACCEPTED_IMAGE_TYPES.join(", ")} are accepted.`
      ),
  })
  .required();
  • Vérifier si l’utilisateur est bien connecté
  • Récupérer l’utilisateur connecté
  • Récupérer la photo de profil de l’utilisateur connecté
  • Vérifier si la photo de profil n’est pas celle de Google, Github, ou Kakao
  • Si ce n’est pas le cas, alors je supprime sa photo de profil de la base de données
  • J’ajoute la nouvelle photo de profil dans ImageKit et je sauvegarde l’ID de la nouvelle photo de profil dans la base de données (très important)
  • J’ajoute le lien de la nouvelle photo de profil dans la base de données
// get current user
const currentUser = await db
  .select()
  .from(userTable)
  .where(eq(userTable.id, user.id))
  .limit(1);
// get current user avatar
const currentUserAvatar = currentUser[0].avatar;
// verify if current user avatar is an imagekit url
if (currentUserAvatar && isImageKitUrl(currentUserAvatar)) {
  try {
    // delete old avatar
    imageKit.deleteFile(currentUser[0].avatarFieldId);
  } catch (deleteError) {
    return {
      success: false,
      message: "Failed to delete old avatar",
    };
  }
}

await db
  .update(userTable)
  .set({ avatar: response.url, avatarFieldId: response.fileId })
  .where(eq(userTable.id, user.id));

revalidatePath("/settings");

Gestion du profil de l’utilisateur

Je continue à ajouter les autres fonctionnalités de l’application, comme la possibilité de modifier l’URL de la page de profil de l’utilisateur, de changer la description, et en bonus, de mettre à jour la biographie de l’utilisateur. Ces trois fonctionnalités ont été implémentées bien plus vite que je ne le pensais.

User info component

Suppression d’un compte

Je trouve très important de donner la possibilité de supprimer un compte aux utilisateurs. Personnellement, quand je m’inscris dans une application, je vérifie directement si j’ai la possibilité de supprimer mon compte. Si ce n’est pas le cas, j’aurais une mauvaise impression de l’application.

C’est pour cela que j’ai décidé de l’implémenter. Pour cela, rien de plus simple : dans un premier temps, je supprime son image de ImageKit, ensuite je supprime tout.

await db.delete(sessionTable).where(eq(sessionTable.userId, user.id));
await db.delete(oauthAccountTable).where(eq(oauthAccountTable.userId, user.id));
await db.delete(userTable).where(eq(userTable.id, user.id));
return redirect("/");

Sauvegarder une chançon

J’ai dû revoir la fonctionnalité de sauvegarde de chanson car, à chaque clic, je mettais à jour les données dans ma base de données. J’ai décidé de faire une vérification : si la dernière chanson sauvegardée et la nouvelle chanson sauvegardée présentent une différence de moins de 10 secondes, alors je ne mets pas à jour la base de données. Si ce n’est pas le cas, je mets à jour ma base de données.

Conclusion

Pour faire court, je peaufine l’appli au fur et à mesure, et je pense rajouter des petites features comme le light mode, etc. Je continue de jouer avec les idées, d’expérimenter, et surtout de garder les choses simples et sympas.