← Back to the project on the home page

RapCongolais242
A platform to discover and listen to Congolese rap: artists, tracks and music videos, with a Premium subscription.
- Personal project
- Sole designer and engineer

The player that never stops
RapCongolais242What is real?
Simulation built on the code of RapCongolais242, fictional data.
- The queue logic (shuffle, next, previous, end of a track, loop in three states) is extracted from the player of RapCongolais242, with its behaviour unchanged.
- The three artists and seven tracks are invented, and the sounds are synthesised in your browser: no real track, no cover and no third-party artist.
- The two subscription paths are an illustration built from the application's data model. No payment is simulated or made.

RapCongolais242
The demo loads only when you ask for it.
The problem
A music service for this audience has to solve three things at once. Listening must not stop when someone moves around the site. Paying must work for listeners abroad and for listeners at home. And media has to be stored and served without weighing on the app or on slow connections.
A player outside the pages
The player lives above the pages, with one queue for the whole site. The queue is built from the list you start: the catalogue, a playlist, an artist's discography. Shuffle and loop are built in, and the loop has three states: none, the whole queue, one track.
Under the hood
Starting a list builds the queue; the end of a track moves to the next one, or loops.
const playQueue = useCallback((tracks: PlayerTrack[], startIndex = 0) => { const trackVise = tracks[startIndex]; if (currentTrack?.id === trackVise?.id) { const audio = audioRef.current; if (!audio) return; if (audio.paused) audio.play(); else audio.pause(); return; } setQueueOriginale(tracks); setQueue(aleatoire ? melanger(tracks) : tracks); setIndexActuel(startIndex); }, [aleatoire, currentTrack]); const suivant = useCallback(() => { setIndexActuel((i) => (i + 1 < queue.length ? i + 1 : 0)); }, [queue.length]); const precedent = useCallback(() => { setIndexActuel((i) => (i > 0 ? i - 1 : queue.length - 1)); }, [queue.length]); const avancerAutomatiquement = useCallback(() => { setIndexActuel((i) => { if (i + 1 < queue.length) return i + 1; return boucle === "file" ? 0 : i; }); }, [queue.length, boucle]);Two payment paths
PayPal serves the international audience. For the local market, Mobile Money and Wave payments are validated by hand: the listener declares a reference, the request waits, an administrator decides. Each path keeps its own trail.
Under the hood
The manual request: which service was used, which plan, which reference, and the decision.
model ManualPaymentRequest { id String @id @default(cuid()) userId String provider ManualPaymentProvider plan PremiumPlan phoneUsed String reference String screenshotUrl String? status ManualPaymentStatus @default(PENDING) rejectReason String? createdAt DateTime @default(now()) reviewedAt DateTime? user User @relation(fields: [userId], references: [id], onDelete: Cascade)} enum ManualPaymentProvider { MTN_CONGO AIRTEL_CONGO WAVE_SENEGAL}Media kept light
Media is stored on Cloudflare R2. Images that are fetched automatically are cropped and compressed on the server, at quality 85, before they are served. This part is not in the demo. Not in this version: a global strategy of modern image formats and systematic lazy loading.
Under the hood
A centred square crop when the image is clearly wider than tall, then a JPEG at quality 85.
export async function telechargerEtRecadrerCover( urlSource: string, dossier: "pochettes"): Promise<string | null> { try { const reponse = await fetch(urlSource); if (!reponse.ok) return null; const bufferOriginal = Buffer.from(await reponse.arrayBuffer()); const image = sharp(bufferOriginal); const metadata = await image.metadata(); if (!metadata.width || !metadata.height) return null; let imageFinale = image; const ratio = metadata.width / metadata.height; if (ratio > 1.15) { const taille = metadata.height; const decalageGauche = Math.round((metadata.width - taille) / 2); imageFinale = image.extract({ left: decalageGauche, top: 0, width: taille, height: taille, }); } const bufferFinal = await imageFinale.jpeg({ quality: 85 }).toBuffer();Try it yourself
Start a track, change page, switch between the catalogue, an artist and the playlist, turn shuffle and loop on. Then open the subscription page to follow the two payment paths and play the administrator. The sounds are synthesised in your browser: no real track, and nothing plays before your click.
What you can do here
- Start a track, then change page: the music keeps playing
- Switch between the catalogue, an artist and a playlist: the queue follows
- Use shuffle, repeat, volume and the queue panel
- Walk through two subscription paths, PayPal and Mobile Money, with no real payment
- Hear sounds generated in your browser: no real track is used
Stack
Next.js 16 · TypeScript · Tailwind CSS v4 · Prisma 7 · Neon Postgres · NextAuth v5 · Cloudflare R2 · Vercel
Context
RapCongolais242 brings Congolese rap into one place: artists, tracks and music videos, with accounts and a Premium subscription.
The problem
A music service for this audience has to solve three things at once. Listening must not stop when someone moves around the site. Paying must work for listeners abroad and for listeners at home. And media has to be stored and served without weighing on the app or on slow connections. Each of these pulled the architecture in a different direction.
Constraints
- Part of the audience listens on mobile with limited bandwidth
- Mobile Money cannot be automated simply, so local payment cannot rely on a payment API
- Content from third-party artists has to be handled properly
What I built
- A home page with an artist of the week and news
- Artist pages: biography, country, style, number of tracks, discography, social links and a star rating
- Tracks with favourites, playlists and a rights-status notice on each track
- A Clips page fed by the channel's YouTube playlist, newest first
- A "Collaborate" page for artists: promotion, marketing strategy, pitch and press, shoots and visuals
- User accounts and a Premium subscription
- A full back-office to create, edit and delete artists, tracks, albums, news and requests
Key decisions
A player that never stops
The player lives outside the pages, with one queue shared by the whole site. Music keeps playing while the visitor navigates. The queue is built from the context: the catalogue, a playlist, or an artist's discography. Shuffle and loop are built in.
A player that lives inside each page. It would have cut the music at every navigation, which is the one thing a listening site cannot do.
Two payment paths, each with its own trail
PayPal serves the international audience. For the local market, Mobile Money and Wave payments are validated by hand. Each path has its own flow and its own traceability in the admin, so a payment can always be followed from request to decision.
A single flow for everyone. One provider would have shut out either the listeners abroad or the ones at home, because Mobile Money cannot be automated simply.
Media on Cloudflare R2, images processed on the server
Media is stored on Cloudflare R2, which speaks the S3 protocol, and uploads go through signed links. Images that are fetched automatically are cropped and compressed on the server, at quality 85, before they are served.
Not in this version: a global strategy of modern image formats such as WebP or AVIF, and systematic lazy loading.