diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 98103878..39d50581 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/apps/backend/package.json b/apps/backend/package.json index 4c07695e..6f7094ac 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -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", diff --git a/apps/backend/src/song/song.controller.spec.ts b/apps/backend/src/song/song.controller.spec.ts index b44b4162..362be7d7 100644 --- a/apps/backend/src/song/song.controller.spec.ts +++ b/apps/backend/src/song/song.controller.spec.ts @@ -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'; @@ -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, diff --git a/apps/backend/src/song/song.controller.ts b/apps/backend/src/song/song.controller.ts index a6e8410d..737456fb 100644 --- a/apps/backend/src/song/song.controller.ts +++ b/apps/backend/src/song/song.controller.ts @@ -105,11 +105,6 @@ export class SongController { ): Promise> { // 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, diff --git a/apps/frontend/posts/blog/2025-12-31_song-search.md b/apps/frontend/posts/blog/2025-12-31_song-search.md index 9b940319..7d3928d6 100644 --- a/apps/frontend/posts/blog/2025-12-31_song-search.md +++ b/apps/frontend/posts/blog/2025-12-31_song-search.md @@ -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! 🎆👋 diff --git a/apps/frontend/src/app/(content)/(info)/about/about.mdx b/apps/frontend/src/app/(content)/(info)/about/about.mdx index ac55f36c..663702cb 100644 --- a/apps/frontend/src/app/(content)/(info)/about/about.mdx +++ b/apps/frontend/src/app/(content)/(info)/about/about.mdx @@ -30,16 +30,16 @@ Note Block World is made possible by our amazing community of note block enthusi Backers Sponsors diff --git a/apps/frontend/src/app/(content)/(info)/blog/page.tsx b/apps/frontend/src/app/(content)/(info)/blog/page.tsx index a9855f94..d5d90ce7 100644 --- a/apps/frontend/src/app/(content)/(info)/blog/page.tsx +++ b/apps/frontend/src/app/(content)/(info)/blog/page.tsx @@ -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' > -
+
>('/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', }, diff --git a/apps/frontend/src/modules/browse/WelcomeBanner.tsx b/apps/frontend/src/modules/browse/WelcomeBanner.tsx index 63f3e264..3eb557c4 100644 --- a/apps/frontend/src/modules/browse/WelcomeBanner.tsx +++ b/apps/frontend/src/modules/browse/WelcomeBanner.tsx @@ -71,7 +71,7 @@ export const WelcomeBanner = () => { {' • '} Donate diff --git a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx index 0f9d3ccf..50996e86 100644 --- a/apps/frontend/src/modules/browse/components/HomePageComponent.tsx +++ b/apps/frontend/src/modules/browse/components/HomePageComponent.tsx @@ -87,7 +87,7 @@ export const HomePageComponent = () => {
- {(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 ? ( diff --git a/apps/frontend/src/modules/browse/components/SongCard.tsx b/apps/frontend/src/modules/browse/components/SongCard.tsx index 5910abbc..a47c41c0 100644 --- a/apps/frontend/src/modules/browse/components/SongCard.tsx +++ b/apps/frontend/src/modules/browse/components/SongCard.tsx @@ -41,7 +41,7 @@ const SongDataDisplay = ({ song }: { song: SongPreviewDtoType | null }) => {
{/* Song author */} -

+

{!song ? ( ) : ( diff --git a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx index 8e5efd6d..60a10135 100644 --- a/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx +++ b/apps/frontend/src/modules/browse/components/client/context/RecentSongs.context.tsx @@ -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; @@ -28,6 +28,20 @@ type RecentSongsStore = RecentSongsState & RecentSongsActions; const adCount = 1; const pageSize = 12; +const fetchCount = pageSize - adCount; + +function injectAdSlots( + songs: SongPreviewDtoType[], +): Array { + const songsWithAds: Array = [...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((set, get) => ({ // Initial state @@ -37,16 +51,13 @@ export const useRecentSongsStore = create((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: '', }); @@ -68,8 +79,6 @@ export const useRecentSongsStore = create((set, get) => ({ set({ isLoading: true }); try { - const fetchCount = pageSize - adCount; - const params: Record = { page, limit: fetchCount, // TODO: fix constants @@ -86,20 +95,15 @@ export const useRecentSongsStore = create((set, get) => ({ { params }, ); - const newSongs: Array = - 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) { @@ -116,7 +120,7 @@ export const useRecentSongsStore = create((set, get) => ({ set({ selectedCategory: category, page: 1, - recentSongs: Array(12).fill(null), + recentSongs: Array(pageSize).fill(null), hasMore: true, }); }, @@ -129,7 +133,7 @@ export const useRecentSongsStore = create((set, get) => ({ } set({ - recentSongs: [...recentSongs, ...Array(12).fill(null)], + recentSongs: [...recentSongs, ...Array(pageSize).fill(null)], page: get().page + 1, }); }, @@ -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]); }; diff --git a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx index 9333cb9b..a1993338 100644 --- a/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/SongRow.tsx @@ -58,7 +58,7 @@ export const SongRow = ({ song }: { song?: SongPreviewDtoType | null }) => { > diff --git a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx index 2778ae70..a2db02d6 100644 --- a/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx +++ b/apps/frontend/src/modules/my-songs/components/client/context/MySongs.context.tsx @@ -51,7 +51,7 @@ export const useMySongsStore = create((set, get) => ({ page: null, totalSongs: 0, totalPages: 0, - currentPage: 0, + currentPage: 1, pageSize: MY_SONGS.PAGE_SIZE, isLoading: true, error: null, @@ -203,10 +203,15 @@ export const useMySongsStore = create((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 @@ -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); diff --git a/apps/frontend/src/modules/shared/components/GoogleAdSense.tsx b/apps/frontend/src/modules/shared/components/GoogleAdSense.tsx index 284fcae8..065ca205 100644 --- a/apps/frontend/src/modules/shared/components/GoogleAdSense.tsx +++ b/apps/frontend/src/modules/shared/components/GoogleAdSense.tsx @@ -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.