Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4a82ba8
chore(deps): bump multer from 2.0.2 to 2.1.1 in /apps/backend
dependabot[bot] Mar 5, 2026
ba37b0c
fix(homepage): first page being fetched again on load more
Bentroen Mar 7, 2026
9f58a58
fix(homepage): duplicate recent songs row on load more
Bentroen Mar 7, 2026
f6fa5e1
fix(homepage): re-introduce ad slot on initial recent song sample
Bentroen Mar 7, 2026
b6d4401
fix(homepage): remove unnecessary type guard on recent songs iteration
Bentroen Mar 7, 2026
e7a3ef6
fix(my-songs): prevent initial fetch with page=0 causing 400 error
Bentroen Mar 7, 2026
1c1d28d
fix(navbar): remove arbitrary width and make username form responsive
Bentroen Mar 8, 2026
5dad403
fix(about): team member pics shrinking horizontally in mobile layout
Bentroen Mar 8, 2026
d1ddb59
fix(about): use column layout on team member cards on mobile
Bentroen Mar 8, 2026
246e46f
fix(about): decrease sponsors image size to avoid section width overflow
Bentroen Mar 8, 2026
de2f8b0
fix(my-songs): un-shrink play button on song thumb hover
Bentroen Mar 8, 2026
ea0829f
fix(navbar): slightly increase size of settings and upload buttons
Bentroen Mar 8, 2026
1ddad2e
fix(navbar): hide upload button on mobile layout
Bentroen Mar 8, 2026
588919d
fix: don't lead directly to pre-selected amount on OC donation links
Bentroen Mar 8, 2026
2d79c6c
fix(song): adjust line height on song card's author name overflow
Bentroen Mar 8, 2026
361ff92
feat(navbar): link Songs tab to search page with no query
Bentroen Mar 8, 2026
26d27cd
fix(search): prevent page title from refreshing when on browse mode
Bentroen Mar 8, 2026
7b13b23
fix(search): disable sorting and order on no results
Bentroen Mar 8, 2026
cd218df
fix(search): remove 10-song limit on query with random sort
Bentroen Mar 8, 2026
5782339
feat(search): introduce random sort option for search results
Bentroen Mar 8, 2026
7a62839
fix(blog): align content to bottom of card when post title wraps line
Bentroen Mar 8, 2026
a77b999
fix(blog): update song search post to reflect actual release date
Bentroen Mar 8, 2026
34d34f6
fix: increase size of avatar in 'Sign in' button
Bentroen Mar 8, 2026
53fe753
chore(ads): add comment on reverting script tag change
Bentroen Mar 8, 2026
9647182
Merge branch 'main' into develop
Bentroen Mar 8, 2026
127e8c5
chore(deps): bump multer from 2.0.2 to 2.1.1 in /apps/backend (#82)
Bentroen Mar 8, 2026
312909a
refactor(search): integrate sort button conditional into existing block
Bentroen Mar 9, 2026
5e033f5
fix(test): remove test asserting removed 10-song limit on random sort
tomast1337 Mar 10, 2026
f896e38
Merge branch 'develop' of github.com:OpenNBS/NoteBlockWorld into develop
tomast1337 Mar 10, 2026
12ab64a
chore(tests): pin bun version to 1.3.5 due to regression in 1.3.10
tomast1337 Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ jobs:
- name: Install bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
# Pin to 1.3.5: Bun 1.3.10 has a regression (descriptor.value undefined with NestJS/Swagger).
# Fix merged in PR #27527 (Feb 28) but not yet in a release. Revert when 1.3.11+ is latest.
bun-version: '1.3.5'

- name: Install dependencies
run: bun install
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"esm": "^3.2.25",
"express": "^5.2.1",
"mongoose": "^9.0.1",
"multer": "2.0.2",
"multer": "2.1.1",
"nanoid": "^5.1.6",
"passport": "^0.7.0",
"passport-github": "^1.1.0",
Expand Down
18 changes: 1 addition & 17 deletions apps/backend/src/song/song.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ import {
SongSortType,
FeaturedSongsDto,
} from '@nbw/database';
import {
BadRequestException,
HttpStatus,
UnauthorizedException,
} from '@nestjs/common';
import { HttpStatus, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Test, TestingModule } from '@nestjs/testing';
import { Response } from 'express';
Expand Down Expand Up @@ -153,18 +149,6 @@ describe('SongController', () => {
expect(songService.getRandomSongs).toHaveBeenCalledWith(5, 'electronic');
});

it('should throw error for invalid random limit', async () => {
const query: SongListQueryDTO = {
page: 1,
limit: 15,
sort: SongSortType.RANDOM,
};

await expect(songController.getSongList(query)).rejects.toThrow(
BadRequestException,
);
});

it('should handle recent sort', async () => {
const query: SongListQueryDTO = {
page: 1,
Expand Down
5 changes: 0 additions & 5 deletions apps/backend/src/song/song.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,6 @@ export class SongController {
): Promise<PageDto<SongPreviewDto>> {
// Handle random sort
if (query.sort === SongSortType.RANDOM) {
if (query.limit && (query.limit < 1 || query.limit > 10)) {
throw new BadRequestException(
'Limit must be between 1 and 10 for random sort',
);
}
const data = await this.songService.getRandomSongs(
query.limit ?? 1,
query.category,
Expand Down
40 changes: 27 additions & 13 deletions apps/frontend/posts/blog/2025-12-31_song-search.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,61 @@
---
title: 'You can now search for songs in Note Block World!'
date: '2025-12-31'
date: '2026-02-18'
author: 'Bentroen'
authorImage: 'bentroen.png'
image: '/img/blog/song-search.webp'
tags: ['updates']
---

**That's right:** the (delayed) Christmas present you have been asking for since forever has just arrived! **Song search has been released to everyone in Note Block World, starting now!**
**That's right:** the feature you ALL have been asking for since the beginning of the website has just arrived! **Song search has been released to everyone in Note Block World, starting now.** 🔍

This was simply our most requested feature, and we're glad to finally be able to unveil it to you!
This was simply our **most requested feature**, and we're glad to finally be able to unveil it to you!

---

## List of changes

- Added a search bar on the page's navbar!
- The results page includes:
- Total number of results returned
- Sorting by recent, popular, duration, play count and note count
- Sorting by **recent, **popular**, **duration**, **play count** and **note count\*\*
- Ordering (ascending, descending)
- Many more filtering options coming right next!

We'd like to thank [tomast1337](https://github.com/tomast1337) for a significant portion of the implementation, as well as our Discord community for providing valuable feedback, and even developing some alternatives (such as the **awesome** [Note Block World Finder](https://nbw.flwc.cc/)) to fill in the gap while the official solution wasn't out!
### Bugfixes and improvements

We hope you enjoy the update, and the search feature makes it ever easier to find creations you like on Note Block World!
- Added a translucent, glass-like effect on the navbar!
- Fixed icons looking huge during page load, before settling in place. We've had an... \*_ahem_\*- _alignment_ session with the icons and they will behave now.
- Fixed unformatted Markdown showing on blog post previews.
- The navbar becomes scrollable if its contents are too large to fit the screen. (This will be replaced by a proper responsive design soon!)
- Fixed song preview cards being huge when there weren't enough entries to fill an entire column.
- Mouse hover areas for the popup and the hover effect are now the same on the navbar buttons.

---

We'd like to thank [tomast1337](https://github.com/tomast1337) for a significant portion of the implementation, as well as our Discord community for providing valuable feedback — and even developing some alternatives (such as the **awesome** [Note Block World Finder](https://nbw.flwc.cc/)) to fill in the gap while the official solution wasn't out!

As always, report bugs to us in [Discord](https://discord.gg/note-block-world-608692895179997252) or in our [GitHub](https://github.com/OpenNBS/NoteBlockWorld/issues/new/choose) page. We'd like to make the website the best it can be, and we count on your support to help us achieve this goal!
As always, report bugs to us in [Discord](https://discord.gg/note-block-world-608692895179997252) or in our [GitHub](https://github.com/OpenNBS/NoteBlockWorld/issues/new/choose) page. We'd like to make the website the best it can be, and we count on **your support** to help us achieve this goal!

We hope you enjoy the update, and the search feature makes it ever easier to find creations you like on Note Block World!

## Up next

Search is just a small step in our plan to become **the world's largest public, open repository of note block creations.** Although this is a very small feature, some important refactoring is underway to pave the way for even more advanced features:
Search is just a small step in our plan to become **the world's largest public, open repository of note block creations.** Although this is a tiny feature, some important refactoring is underway to pave the way for even more advanced features:

- In-browser song playback!
- Advanced search filtering!
- User profiles!
- ...and much, much more!

You can expect these features to start rolling out in the next few months. Stay tuned — there's much more to come in 2026!
You can expect these features to start rolling out in the next few months. Stay tuned — there's much more to come this year!

## Wrapping up

This year couldn't have been more amazing. We've reached over **two thousand songs** uploaded to Note Block World, and held our **second community-wide [collaboration event](http://localhost:3000/blog/maestro's-musical-masterpieces)** with the creators of [M.A.E.S.T.R.O.](https://noteblock.world/blog/maestro), which had over **30 submissions** spanning **over 90 minutes of music!** And there was even time for a bonus feature before closing off this year! 🎁
The year of 2025 couldn't have been more amazing. Thanks to **your** engagement and support, we've reached over **two thousand songs** uploaded to Note Block World, and held our **second community-wide [collaboration event](/blog/maestro's-musical-masterpieces)** with the creators of [M.A.E.S.T.R.O.](/blog/maestro), which got over **30 submissions** spanning **over 90 minutes of music!**

Thank you to everyone who visited the website, shared music with their friends, submitted their own creations, or even [donated](https://opencollective.com/opennbs/donate/profile) to help us keep the website running. **Our community is at the core of everything we do, and the reason why we're excited to keep working on cool things everyday!**
Thank you to everyone who visited the website, shared music with their friends, submitted their own creations, or even [donated](https://opencollective.com/opennbs/donate) to help us keep the website running. **Our community is at the core of everything we do, and the reason why we're excited to keep working on cool things everyday!**

We wish everyone an **incredible 2026**, full of achievements and amazing moments. We'll certainly work hard to make sure it is a wonderful year for Note Block World — especially as it will mark **Note Block Studio's 15th Anniversary!** Stay tuned for the many cool things we're planning to celebrate this remarkable moment in the history of note blocks.
We wish everyone an **incredible 2026**, full of achievements and amazing moments. We'll certainly work hard to make sure it is a wonderful year for Note Block World — especially as **Note Block Studio will turn 15 years old!** Stay tuned for the many cool things we're planning to celebrate this remarkable moment in the history of note blocks.

**Happy New Year!** See y'all in the next one! :fireworks: :wave_tone1:
See y'all in the next one! 🎆👋
12 changes: 6 additions & 6 deletions apps/frontend/src/app/(content)/(info)/about/about.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ Note Block World is made possible by our amazing community of note block enthusi

<Image
unoptimized
src='https://opencollective.com/opennbs/backers.svg?width=900'
src='https://opencollective.com/opennbs/backers.svg?width=800'
alt='Backers'
width={900}
height={900}
width={800}
height={800}
/>

<Image
unoptimized
src='https://opencollective.com/opennbs/sponsors.svg?width=900'
src='https://opencollective.com/opennbs/sponsors.svg?width=800'
alt='Sponsors'
width={900}
height={900}
width={800}
height={800}
/>
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(content)/(info)/blog/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const BlogPageComponent = ({ posts }: { posts: PostType[] }) => {
href={`/blog/${post.id}`}
className='w-full h-full p-4 rounded-md bg-zinc-800/50 hover:bg-zinc-700/80 transition-all duration-200'
>
<article key={i} className='flex flex-col'>
<article key={i} className='flex flex-col h-full'>
<Image
src={post.image || '/img/post.png'}
width={480}
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/app/(content)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function fetchRecentSongs() {
const response = await axiosInstance.get<PageDto<SongPreviewDto>>('/song', {
params: {
page: 1, // TODO: fix constants
limit: 16, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination
limit: 11, // TODO: change 'limit' parameter to 'skip' and load 12 songs initially, then load 8 more songs on each pagination
sort: 'recent',
order: 'desc',
},
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/modules/browse/WelcomeBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const WelcomeBanner = () => {
</Link>
{' • '}
<Link
href='https://opencollective.com/opennbs/donate/profile'
href='https://opencollective.com/opennbs/donate'
className='text-blue-400 hover:text-blue-300'
>
Donate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const HomePageComponent = () => {
</div>
<div className='h-6' />
<SongCardGroup data-test='recent-songs'>
{(recentSongs || []).map((song, i) =>
{recentSongs.map((song, i) =>
// TODO: currently null = skeleton, undefined = ad slot. There must be a more robust system to indicate this.
song === undefined ? (
<SongCardAdSlot key={i} />
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/modules/browse/components/SongCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const SongDataDisplay = ({ song }: { song: SongPreviewDtoType | null }) => {
</div>
<div className='flex flex-row justify-between items-center gap-4 px-4'>
{/* Song author */}
<p className='text-sm text-zinc-400 flex-1'>
<p className='text-sm text-zinc-400 flex-1 text-pretty leading-tight'>
{!song ? (
<Skeleton />
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { PageDto, SongPreviewDtoType } from '@nbw/database';
import axiosInstance from '@web/lib/axios';

interface RecentSongsState {
recentSongs: (SongPreviewDtoType | null)[];
recentSongs: (SongPreviewDtoType | null | undefined)[];
recentError: string;
isLoading: boolean;
hasMore: boolean;
Expand All @@ -28,6 +28,20 @@ type RecentSongsStore = RecentSongsState & RecentSongsActions;

const adCount = 1;
const pageSize = 12;
const fetchCount = pageSize - adCount;

function injectAdSlots(
songs: SongPreviewDtoType[],
): Array<SongPreviewDtoType | undefined> {
const songsWithAds: Array<SongPreviewDtoType | undefined> = [...songs];

for (let i = 0; i < adCount; i++) {
const adPosition = Math.floor(Math.random() * (songsWithAds.length + 1));
songsWithAds.splice(adPosition, 0, undefined);
}

return songsWithAds;
}

export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
// Initial state
Expand All @@ -37,16 +51,13 @@ export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
hasMore: true,
selectedCategory: '',
categories: {},
page: 0,
page: 1,

// Actions
initialize: (initialRecentSongs) => {
// If no initial songs, set page to 1 to trigger fetch
// Otherwise, keep page at 0 since we already have the first page of data
const initialPage = initialRecentSongs.length === 0 ? 1 : 0;
set({
recentSongs: initialRecentSongs,
page: initialPage,
recentSongs: injectAdSlots(initialRecentSongs),
page: 1,
hasMore: true,
recentError: '',
});
Expand All @@ -68,8 +79,6 @@ export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
set({ isLoading: true });

try {
const fetchCount = pageSize - adCount;

const params: Record<string, any> = {
page,
limit: fetchCount, // TODO: fix constants
Expand All @@ -86,20 +95,15 @@ export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
{ params },
);

const newSongs: Array<SongPreviewDtoType | undefined> =
response.data.content;

for (let i = 0; i < adCount; i++) {
const adPosition = Math.floor(Math.random() * newSongs.length) + 1;
newSongs.splice(adPosition, 0, undefined);
}
const fetchedSongs = response.data.content;
const newSongs = injectAdSlots(fetchedSongs);

set((state) => ({
recentSongs: [
...state.recentSongs.filter((song) => song !== null),
...response.data.content,
...newSongs,
],
hasMore: response.data.content.length >= fetchCount,
hasMore: fetchedSongs.length >= fetchCount,
recentError: '',
}));
} catch (error) {
Expand All @@ -116,7 +120,7 @@ export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
set({
selectedCategory: category,
page: 1,
recentSongs: Array(12).fill(null),
recentSongs: Array(pageSize).fill(null),
hasMore: true,
});
},
Expand All @@ -129,7 +133,7 @@ export const useRecentSongsStore = create<RecentSongsStore>((set, get) => ({
}

set({
recentSongs: [...recentSongs, ...Array(12).fill(null)],
recentSongs: [...recentSongs, ...Array(pageSize).fill(null)],
page: get().page + 1,
});
},
Expand All @@ -146,7 +150,7 @@ export const useRecentSongsPageLoader = () => {
);

useEffect(() => {
if (page === 0) return;
if (page === 1) return; // Skip fetching page 1 as it's already loaded initially
fetchRecentSongs();
}, [page, selectedCategory, fetchRecentSongs]);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => {
>
<FontAwesomeIcon
icon={faCirclePlay}
className='text-white w-12 h-12'
className='text-white text-4xl'
/>
</Link>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const useMySongsStore = create<MySongsStore>((set, get) => ({
page: null,
totalSongs: 0,
totalPages: 0,
currentPage: 0,
currentPage: 1,
pageSize: MY_SONGS.PAGE_SIZE,
isLoading: true,
error: null,
Expand Down Expand Up @@ -203,10 +203,15 @@ export const useMySongsStore = create<MySongsStore>((set, get) => ({
export const useMySongsPageLoader = () => {
const currentPage = useMySongsStore((state) => state.currentPage);
const loadPage = useMySongsStore((state) => state.loadPage);
const loadedSongs = useMySongsStore((state) => state.loadedSongs);

useEffect(() => {
// Skip loading if the page is already loaded from initial data
if (currentPage in loadedSongs) {
return;
}
loadPage();
}, [currentPage, loadPage]);
}, [currentPage, loadPage, loadedSongs]);
};

// Legacy hook name for backward compatibility
Expand All @@ -227,7 +232,7 @@ export const MySongProvider = ({
InitialsongsFolder = {},
children,
totalPagesInit = 0,
currentPageInit = 0,
currentPageInit = 1,
pageSizeInit = MY_SONGS.PAGE_SIZE,
}: MySongProviderProps) => {
const initialize = useMySongsStore((state) => state.initialize);
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/modules/shared/components/GoogleAdSense.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const GoogleAdSense = ({ pId }: { pId?: string }) => {
}

return (
// TODO: we changed from Next's Script component to a regular script tag to fix the following error:
// "AdSense head tag doesn't support `data-nscript` attribute". Check if this can be reverted later.
<script
async
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${pId}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ export const TeamMemberCard = ({
children: string;
}) => {
return (
<div className='flex flex-row gap-8 items-center'>
<div className='flex flex-col md:flex-row gap-2 md:gap-8 items-start'>
<picture className='w-full max-w-fit'>
<Image
src={`/img/authors/${img}`}
width={96}
height={96}
className='rounded-full w-24 h-24'
className='rounded-full aspect-square'
quality={100}
alt={''}
/>
Expand All @@ -45,7 +45,7 @@ export const TeamMemberCard = ({

export const Team = ({ children }: { children: React.ReactNode }) => {
return (
<section className='flex flex-col gap-8 mt-10 mx-8 mb-16'>
<section className='flex flex-col gap-8 mt-10 mx-0 md:mx-8 mb-16'>
{children}
</section>
);
Expand Down
Loading