Skip to content

Feature/login stats #34

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
7 changes: 6 additions & 1 deletion NoteBlockWorld.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
6 changes: 3 additions & 3 deletions server/src/user/entity/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ 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;
loginCount: number;

@Prop({ type: Number, required: true, default: 0 })
loginCount: number;
loginStreak: number;

@Prop({ type: Number, required: true, default: 0 })
playCount: number;
Expand Down
111 changes: 110 additions & 1 deletion server/src/user/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -217,6 +217,115 @@ 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);
yesterday.setHours(0, 0, 0, 0);

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();
});

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();
});

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', () => {
Expand Down
27 changes: 24 additions & 3 deletions server/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +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);

return usedData;
const today = new Date();
today.setHours(0, 0, 0, 0); // Set the time to the start of the day

const lastSeenDate = new Date(userData.lastSeen);
lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day

if (lastSeenDate < today) {
userData.lastSeen = new Date();

// 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) userData.loginStreak = 1;
else userData.loginStreak += 1;

userData.loginCount++;

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 userData;
}

public async usernameExists(username: string) {
Expand Down
2 changes: 1 addition & 1 deletion web/posts/help/5_custom-instruments.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_.

Expand Down
1 change: 1 addition & 0 deletions web/public/ads.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
google.com, pub-2486912467787383, DIRECT, f08c47fec0942fa0
google.com, pub-6165475566660433, DIRECT, f08c47fec0942fa0
11 changes: 7 additions & 4 deletions web/src/app/(content)/(info)/blog/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -45,9 +45,12 @@ const BlogPost = ({ params }: BlogPageProps) => {
return (
<>
<article className='max-w-screen-md mx-auto mb-36'>
<BackButton className='text-zinc-500 hover:text-zinc-400 text-sm'>
{'< Back to Help'}
</BackButton>
<Link
href='/help'
className='text-zinc-500 hover:text-zinc-400 text-sm'
>
{'< Back to Blog'}
</Link>
<h1 className='text-4xl font-bold mt-16 mb-8'>{post.title}</h1>

{/* Author */}
Expand Down
8 changes: 5 additions & 3 deletions web/src/app/(content)/(info)/help/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -46,9 +45,12 @@ const HelpPost = ({ params }: HelpPageProps) => {
return (
<>
<article className='max-w-screen-md mx-auto mb-36'>
<BackButton className='text-zinc-500 hover:text-zinc-400 text-sm'>
<Link
href='/blog'
className='text-zinc-500 hover:text-zinc-400 text-sm'
>
{'< Back to Help'}
</BackButton>
</Link>
<h1 className='text-4xl font-bold mt-16 mb-8'>{post.title}</h1>

{/* Author */}
Expand Down
9 changes: 9 additions & 0 deletions web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion web/src/modules/browse/components/SongCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const SongCard = ({ song }: { song: SongPreviewDtoType | null }) => {
return !song ? (
<SongDataDisplay song={song} />
) : (
<Link href={`/song/${song.publicId}`} className='h-full'>
<Link href={`/song/${song.publicId}`} className='h-full max-h-fit'>
<div
className='bg-zinc-800 hover:scale-105 hover:bg-zinc-700 rounded-lg cursor-pointer w-full h-full transition-all duration-200'
style={{ backfaceVisibility: 'hidden' }}
Expand Down
8 changes: 4 additions & 4 deletions web/src/modules/shared/components/client/ads/AdSlots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 max-h-fit hidden xl:block w-36 min-w-36 bg-zinc-800/50 rounded-xl',
className,
)}
adSlot='4995642586'
Expand Down Expand Up @@ -152,7 +152,7 @@ export const MultiplexAdSlot = ({ className }: { className?: string }) => {
return (
<AdTemplate
className={cn(
'relative rounded-xl bg-zinc-800/50 p-2 my-8 h-auto min-h-32 w-full min-w-64 text-sm text-zinc-400',
'relative rounded-xl bg-zinc-800/50 my-8 h-auto min-h-32 w-full min-w-64 text-sm text-zinc-400',
className,
)}
adSlot='6673081563'
Expand All @@ -166,12 +166,12 @@ export const SongCardAdSlot = ({ className }: { className?: string }) => {
return (
<AdTemplate
className={cn(
'relative rounded-xl bg-zinc-800 p-2 my-8 h-full w-full min-w-64 text-sm text-zinc-400',
'relative rounded-xl bg-zinc-800 p-2 h-full w-full min-w-64 text-sm text-zinc-400',
className,
)}
adSlot='1737918264'
adFormat='fluid'
adLayoutKey='-7o+ez-1j-38+bu'
adLayoutKey='-6o+ez-1j-38+bu'
showCloseButton={false}
/>
);
Expand Down