From d36ed4efe26958268cde7ed5978e4c08a86ed1bd Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 24 Jan 2025 21:44:41 -0300 Subject: [PATCH 01/16] feat: update User entity to track lastSeen and manage login streak logic --- server/src/user/entity/user.entity.ts | 5 +---- server/src/user/user.service.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/server/src/user/entity/user.entity.ts b/server/src/user/entity/user.entity.ts index d450dc19..c191659a 100644 --- a/server/src/user/entity/user.entity.ts +++ b/server/src/user/entity/user.entity.ts @@ -19,14 +19,11 @@ export class User { lastEdited: Date; @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) - lastLogin: Date; + lastSeen: Date; @Prop({ type: Number, required: true, default: 0 }) loginStreak: number; - @Prop({ type: Number, required: true, default: 0 }) - loginCount: number; - @Prop({ type: Number, required: true, default: 0 }) playCount: number; diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index c0f39540..dedc21c3 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -95,6 +95,27 @@ export class UserService { if (!usedData) throw new HttpException('user not found', HttpStatus.NOT_FOUND); + const today = new Date(); + today.setHours(0, 0, 0, 0); // Set the time to the start of the day + + const lastSeenDate = new Date(usedData.lastSeen); + lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day + + if (lastSeenDate < today) { + usedData.lastSeen = new Date(); + usedData.lastSeen = new Date(); + + // if the last seen date is not yesterday, reset the login streak + // if the last seen date is not yesterday, reset the login streak + const yesterday = new Date(today); + yesterday.setDate(today.getDate() - 1); + + if (lastSeenDate < yesterday) usedData.loginStreak = 1; + else usedData.loginStreak += 1; + + usedData.save(); // no need to await this, we already have the data to sent back + } // if equal or greater, do nothing about the login streak + return usedData; } From be6fc428e17764a483a5e1b866c61ce708d3d6df Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 24 Jan 2025 21:44:44 -0300 Subject: [PATCH 02/16] feat: enhance getSelfUserData to update lastSeen and manage login streak logic --- server/src/user/user.service.spec.ts | 44 +++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/server/src/user/user.service.spec.ts b/server/src/user/user.service.spec.ts index 0855811d..5f9cc421 100644 --- a/server/src/user/user.service.spec.ts +++ b/server/src/user/user.service.spec.ts @@ -198,7 +198,7 @@ describe('UserService', () => { describe('getSelfUserData', () => { it('should return self user data', async () => { const user = { _id: 'test-id' } as UserDocument; - const userData = { ...user } as UserDocument; + const userData = { ...user, lastSeen: new Date() } as UserDocument; jest.spyOn(service, 'findByID').mockResolvedValue(userData); @@ -217,6 +217,48 @@ describe('UserService', () => { new HttpException('user not found', HttpStatus.NOT_FOUND), ); }); + + it('should update lastSeen and increment loginStreak if lastSeen is before today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + + const userData = { + ...user, + lastSeen: yesterday, + loginStreak: 1, + save: jest.fn().mockResolvedValue(true), + } as unknown as UserDocument; + + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); + + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginStreak).toBe(2); + expect(userData.save).toHaveBeenCalled(); + }); + + it('should not update lastSeen or increment loginStreak if lastSeen is today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const userData = { + ...user, + lastSeen: today, + loginStreak: 1, + save: jest.fn().mockResolvedValue(true), + } as unknown as UserDocument; + + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); + + expect(result.lastSeen).toEqual(today); + expect(result.loginStreak).toBe(1); + expect(userData.save).not.toHaveBeenCalled(); + }); }); describe('usernameExists', () => { From c35cf8120b79bcf61ea502493c731b6f0669eca8 Mon Sep 17 00:00:00 2001 From: tomast1337 Date: Fri, 24 Jan 2025 21:45:05 -0300 Subject: [PATCH 03/16] test: add unit test to reset login streak if lastSeen is not yesterday --- server/src/user/user.service.spec.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/server/src/user/user.service.spec.ts b/server/src/user/user.service.spec.ts index 5f9cc421..e93c59f7 100644 --- a/server/src/user/user.service.spec.ts +++ b/server/src/user/user.service.spec.ts @@ -222,6 +222,7 @@ describe('UserService', () => { const user = { _id: 'test-id' } as UserDocument; const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); const userData = { ...user, @@ -259,6 +260,28 @@ describe('UserService', () => { expect(result.loginStreak).toBe(1); expect(userData.save).not.toHaveBeenCalled(); }); + + it('should reset loginStreak if lastSeen is not yesterday', async () => { + const user = { _id: 'test-id' } as UserDocument; + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + twoDaysAgo.setHours(0, 0, 0, 0); + + const userData = { + ...user, + lastSeen: twoDaysAgo, + loginStreak: 5, + save: jest.fn().mockResolvedValue(true), + } as unknown as UserDocument; + + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); + + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginStreak).toBe(1); + expect(userData.save).toHaveBeenCalled(); + }); }); describe('usernameExists', () => { From 5c0a160c348dd556becfcca5d5c532fd6f812d82 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 11 Feb 2025 02:03:14 -0300 Subject: [PATCH 04/16] fix: tweak ad layout for song card ad; fix side rail ad not collapsing fully --- web/src/modules/shared/components/client/ads/AdSlots.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/modules/shared/components/client/ads/AdSlots.tsx b/web/src/modules/shared/components/client/ads/AdSlots.tsx index d12e0771..cd6b0cf2 100644 --- a/web/src/modules/shared/components/client/ads/AdSlots.tsx +++ b/web/src/modules/shared/components/client/ads/AdSlots.tsx @@ -122,7 +122,7 @@ export const SideRailAdSlot = ({ className }: { className?: string }) => { // height with this class: "max-h-[calc(100vh-9rem)]", but then the container doesn't fit to // the ad content height, always occupying the full viewport height instead. So we use 'max-w-fit' // to cap the max height to that of the ad. - 'flex-0 sticky mb-8 top-24 p-2 min-h-96 max-h-fit hidden xl:block w-36 min-w-36 bg-zinc-800/50 rounded-xl', + 'flex-0 sticky mb-8 top-24 min-h-96 max-h-fit hidden xl:block w-36 min-w-36 bg-zinc-800/50 rounded-xl', className, )} adSlot='4995642586' @@ -171,7 +171,7 @@ export const SongCardAdSlot = ({ className }: { className?: string }) => { )} adSlot='1737918264' adFormat='fluid' - adLayoutKey='-7o+ez-1j-38+bu' + adLayoutKey='-6o+ez-1j-38+bu' showCloseButton={false} /> ); From 0df8b0576261ec96208c7d03f7b51004ee94151e Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 11 Feb 2025 02:18:47 -0300 Subject: [PATCH 05/16] fix: hide unfilled ad slots --- web/src/app/globals.css | 9 +++++++++ web/src/modules/shared/components/client/ads/AdSlots.tsx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 88470965..d23e7f6e 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -150,3 +150,12 @@ body { transform: translatex(100%) skewy(-45deg); top: -4px; } + + +/************** Google AdSense **************/ + +/* Hide unfilled ads */ +/* https://support.google.com/adsense/answer/10762946?hl=en */ +ins.adsbygoogle[data-ad-status="unfilled"] { + display: none !important; +} diff --git a/web/src/modules/shared/components/client/ads/AdSlots.tsx b/web/src/modules/shared/components/client/ads/AdSlots.tsx index cd6b0cf2..11f3c8e1 100644 --- a/web/src/modules/shared/components/client/ads/AdSlots.tsx +++ b/web/src/modules/shared/components/client/ads/AdSlots.tsx @@ -152,7 +152,7 @@ export const MultiplexAdSlot = ({ className }: { className?: string }) => { return ( Date: Tue, 11 Feb 2025 02:37:12 -0300 Subject: [PATCH 06/16] fix: side rail ad not collapsing fully (again) --- web/src/modules/shared/components/client/ads/AdSlots.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/modules/shared/components/client/ads/AdSlots.tsx b/web/src/modules/shared/components/client/ads/AdSlots.tsx index 11f3c8e1..a2c1e0c6 100644 --- a/web/src/modules/shared/components/client/ads/AdSlots.tsx +++ b/web/src/modules/shared/components/client/ads/AdSlots.tsx @@ -122,7 +122,7 @@ export const SideRailAdSlot = ({ className }: { className?: string }) => { // height with this class: "max-h-[calc(100vh-9rem)]", but then the container doesn't fit to // the ad content height, always occupying the full viewport height instead. So we use 'max-w-fit' // to cap the max height to that of the ad. - 'flex-0 sticky mb-8 top-24 min-h-96 max-h-fit hidden xl:block w-36 min-w-36 bg-zinc-800/50 rounded-xl', + 'flex-0 sticky mb-8 top-24 max-h-fit hidden xl:block w-36 min-w-36 bg-zinc-800/50 rounded-xl', className, )} adSlot='4995642586' From 28c96e89d653e0f29ef13f2bcbfa97f9af80e706 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 11 Feb 2025 11:32:14 -0300 Subject: [PATCH 07/16] chore: add alternate ad publisher code --- web/public/ads.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/web/public/ads.txt b/web/public/ads.txt index 07bfb1fb..192ecbfe 100644 --- a/web/public/ads.txt +++ b/web/public/ads.txt @@ -1 +1,2 @@ google.com, pub-2486912467787383, DIRECT, f08c47fec0942fa0 +google.com, pub-6165475566660433, DIRECT, f08c47fec0942fa0 \ No newline at end of file From 4156de7b75a173d1deda06e040a1dab45c2958f2 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 11 Feb 2025 13:43:02 -0300 Subject: [PATCH 08/16] fix: height of song card ad making entire row taller --- web/src/modules/shared/components/client/ads/AdSlots.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/modules/shared/components/client/ads/AdSlots.tsx b/web/src/modules/shared/components/client/ads/AdSlots.tsx index a2c1e0c6..c6d9c73f 100644 --- a/web/src/modules/shared/components/client/ads/AdSlots.tsx +++ b/web/src/modules/shared/components/client/ads/AdSlots.tsx @@ -166,7 +166,7 @@ export const SongCardAdSlot = ({ className }: { className?: string }) => { return ( Date: Tue, 11 Feb 2025 14:35:36 -0300 Subject: [PATCH 09/16] fix: height of song card incorrect again --- web/src/modules/browse/components/SongCard.tsx | 2 +- web/src/modules/shared/components/client/ads/AdSlots.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/modules/browse/components/SongCard.tsx b/web/src/modules/browse/components/SongCard.tsx index d9a4328d..247a6b56 100644 --- a/web/src/modules/browse/components/SongCard.tsx +++ b/web/src/modules/browse/components/SongCard.tsx @@ -73,7 +73,7 @@ const SongCard = ({ song }: { song: SongPreviewDtoType | null }) => { return !song ? ( ) : ( - +
{ return ( Date: Tue, 11 Feb 2025 16:10:28 -0300 Subject: [PATCH 10/16] fix: song card height not adjusting to content --- web/src/modules/browse/components/SongCard.tsx | 2 +- web/src/modules/shared/components/client/ads/AdSlots.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/modules/browse/components/SongCard.tsx b/web/src/modules/browse/components/SongCard.tsx index 247a6b56..ef5d4305 100644 --- a/web/src/modules/browse/components/SongCard.tsx +++ b/web/src/modules/browse/components/SongCard.tsx @@ -73,7 +73,7 @@ const SongCard = ({ song }: { song: SongPreviewDtoType | null }) => { return !song ? ( ) : ( - +
{ return ( Date: Tue, 18 Feb 2025 18:42:10 -0300 Subject: [PATCH 11/16] fix: incorrect help page link --- web/posts/help/5_custom-instruments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/posts/help/5_custom-instruments.md b/web/posts/help/5_custom-instruments.md index e90da21d..bb26b867 100644 --- a/web/posts/help/5_custom-instruments.md +++ b/web/posts/help/5_custom-instruments.md @@ -54,7 +54,7 @@ Note Block World lets you use any sound in the latest Minecraft version in your When you upload a song that uses custom instruments to Note Block World, you'll need to manually select the sound files for each custom instrument you used. This ensures that your song sounds the way you intended it to sound, even if the listener doesn't have the custom instruments installed. -Uploading a song with custom instruments follows the same process as [uploading a regular song](/help/1-creating-song), with the addition of manually selecting the sound files for your custom instruments. Here's how you can do it: +Uploading a song with custom instruments follows the same process as [uploading a regular song](/help/creating-song), with the addition of manually selecting the sound files for your custom instruments. Here's how you can do it: 1. After you've filled in the song's title, description, and other metadata, you'll see a section labeled _Custom instruments_. From f5c4a987b304b9a64f3a7470764014c4362becd0 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 18 Feb 2025 18:53:04 -0300 Subject: [PATCH 12/16] fix: make 'back' button in articles return to main blog/help page --- web/src/app/(content)/(info)/blog/[id]/page.tsx | 11 +++++++---- web/src/app/(content)/(info)/help/[id]/page.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/web/src/app/(content)/(info)/blog/[id]/page.tsx b/web/src/app/(content)/(info)/blog/[id]/page.tsx index 66c04353..264dae1f 100644 --- a/web/src/app/(content)/(info)/blog/[id]/page.tsx +++ b/web/src/app/(content)/(info)/blog/[id]/page.tsx @@ -1,9 +1,9 @@ import type { Metadata } from 'next'; import Image from 'next/image'; +import Link from 'next/link'; import { notFound } from 'next/navigation'; import { PostType, getPostData } from '@web/src/lib/posts'; -import BackButton from '@web/src/modules/shared/components/client/BackButton'; import { CustomMarkdown } from '@web/src/modules/shared/components/CustomMarkdown'; type BlogPageProps = { @@ -45,9 +45,12 @@ const BlogPost = ({ params }: BlogPageProps) => { return ( <>
- - {'< Back to Help'} - + + {'< Back to Blog'} +

{post.title}

{/* Author */} diff --git a/web/src/app/(content)/(info)/help/[id]/page.tsx b/web/src/app/(content)/(info)/help/[id]/page.tsx index 3e100ef7..a47ec2a9 100644 --- a/web/src/app/(content)/(info)/help/[id]/page.tsx +++ b/web/src/app/(content)/(info)/help/[id]/page.tsx @@ -4,7 +4,6 @@ import Link from 'next/link'; import { notFound } from 'next/navigation'; import { PostType, getPostData } from '@web/src/lib/posts'; -import BackButton from '@web/src/modules/shared/components/client/BackButton'; import { CustomMarkdown } from '@web/src/modules/shared/components/CustomMarkdown'; type HelpPageProps = { @@ -46,9 +45,12 @@ const HelpPost = ({ params }: HelpPageProps) => { return ( <>
- + {'< Back to Help'} - +

{post.title}

{/* Author */} From c3348f7e5e1c4c017e59137ed76dafd9e23ff25a Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 18 Feb 2025 19:55:54 -0300 Subject: [PATCH 13/16] chore: add ignored search folders in VSCode workspace file --- NoteBlockWorld.code-workspace | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/NoteBlockWorld.code-workspace b/NoteBlockWorld.code-workspace index 25a7ce54..cb438c31 100644 --- a/NoteBlockWorld.code-workspace +++ b/NoteBlockWorld.code-workspace @@ -26,6 +26,11 @@ "editor.codeActionsOnSave": { "source.fixAll": "explicit" }, - "jest.disabledWorkspaceFolders": ["Root", "Frontend"] + "jest.disabledWorkspaceFolders": ["Root", "Frontend"], + "search.exclude": { + "**/.git": true, + "**/node_modules": true, + "**/dist": true, + } } } From 9b907423ddfac404248da965ab97d6f89e288656 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 18 Feb 2025 20:05:21 -0300 Subject: [PATCH 14/16] feat: add login count to user data again --- server/src/user/entity/user.entity.ts | 3 +++ server/src/user/user.service.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/server/src/user/entity/user.entity.ts b/server/src/user/entity/user.entity.ts index c191659a..eec64637 100644 --- a/server/src/user/entity/user.entity.ts +++ b/server/src/user/entity/user.entity.ts @@ -21,6 +21,9 @@ export class User { @Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now }) lastSeen: Date; + @Prop({ type: Number, required: true, default: 0 }) + loginCount: number; + @Prop({ type: Number, required: true, default: 0 }) loginStreak: number; diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index e0a031e0..e8424092 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -114,6 +114,8 @@ export class UserService { if (lastSeenDate < yesterday) usedData.loginStreak = 1; else usedData.loginStreak += 1; + usedData.loginCount++; + usedData.save(); // no need to await this, we already have the data to sent back } // if equal or greater, do nothing about the login streak From 2a44fca2f0a56f1094c0596531f4bec0e8581839 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 18 Feb 2025 20:12:22 -0300 Subject: [PATCH 15/16] style: remove duplicate comment, rename variable --- server/src/user/user.service.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/user/user.service.ts b/server/src/user/user.service.ts index e8424092..27001c2b 100644 --- a/server/src/user/user.service.ts +++ b/server/src/user/user.service.ts @@ -92,34 +92,32 @@ export class UserService { } public async getSelfUserData(user: UserDocument) { - const usedData = await this.findByID(user._id.toString()); - if (!usedData) + const userData = await this.findByID(user._id.toString()); + if (!userData) throw new HttpException('user not found', HttpStatus.NOT_FOUND); const today = new Date(); today.setHours(0, 0, 0, 0); // Set the time to the start of the day - const lastSeenDate = new Date(usedData.lastSeen); + const lastSeenDate = new Date(userData.lastSeen); lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day if (lastSeenDate < today) { - usedData.lastSeen = new Date(); - usedData.lastSeen = new Date(); + userData.lastSeen = new Date(); - // if the last seen date is not yesterday, reset the login streak // if the last seen date is not yesterday, reset the login streak const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); - if (lastSeenDate < yesterday) usedData.loginStreak = 1; - else usedData.loginStreak += 1; + if (lastSeenDate < yesterday) userData.loginStreak = 1; + else userData.loginStreak += 1; - usedData.loginCount++; + userData.loginCount++; - usedData.save(); // no need to await this, we already have the data to sent back + userData.save(); // no need to await this, we already have the data to send back } // if equal or greater, do nothing about the login streak - return usedData; + return userData; } public async usernameExists(username: string) { From 1a25ca53d80581209f13d102378f6e0f7bfb1330 Mon Sep 17 00:00:00 2001 From: Bernardo Costa Date: Tue, 18 Feb 2025 20:19:53 -0300 Subject: [PATCH 16/16] test: add test cases for login count incrementation --- server/src/user/user.service.spec.ts | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/server/src/user/user.service.spec.ts b/server/src/user/user.service.spec.ts index be42a053..882a2278 100644 --- a/server/src/user/user.service.spec.ts +++ b/server/src/user/user.service.spec.ts @@ -282,6 +282,50 @@ describe('UserService', () => { expect(result.loginStreak).toBe(1); expect(userData.save).toHaveBeenCalled(); }); + + it('should increment loginCount if lastSeen is not today', async () => { + const user = { _id: 'test-id' } as UserDocument; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setHours(0, 0, 0, 0); + + const userData = { + ...user, + lastSeen: yesterday, + loginCount: 5, + save: jest.fn().mockResolvedValue(true), + } as unknown as UserDocument; + + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); + + expect(result.lastSeen).toBeInstanceOf(Date); + expect(result.loginCount).toBe(6); + expect(userData.save).toHaveBeenCalled(); + }); + + it('should not increment loginCount if lastSeen is today', async () => { + const user = { _id: 'test-id' } as UserDocument; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const userData = { + ...user, + lastSeen: today, + loginCount: 5, + save: jest.fn().mockResolvedValue(true), + } as unknown as UserDocument; + + jest.spyOn(service, 'findByID').mockResolvedValue(userData); + + const result = await service.getSelfUserData(user); + + expect(result.lastSeen).toEqual(today); + expect(result.loginCount).toBe(5); + expect(userData.save).not.toHaveBeenCalled(); + }); }); describe('usernameExists', () => {