Skip to content

Commit 60b422a

Browse files
committed
feat: integrate @nbw/song and @nbw/thumbnail packages and enhance ThumbnailRenderer component
- Added @nbw/song and @nbw/thumbnail as workspace dependencies in bun.lock and package.json. - Updated next.config.mjs to prevent @nbw/thumbnail from being bundled on the server to avoid SSR issues. - Refactored ThumbnailRenderer component to dynamically import thumbnail functions, ensuring compatibility with client-side rendering. - Modified FormElements component to use a div instead of a paragraph for description rendering, improving semantic structure.
1 parent d0d2ddb commit 60b422a

File tree

8 files changed

+191
-67
lines changed

8 files changed

+191
-67
lines changed

apps/frontend/next.config.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import createMDX from '@next/mdx';
55
const nextConfig = {
66
pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
77
// See: https://github.com/Automattic/node-canvas/issues/867#issuecomment-1925284985
8-
webpack: (config) => {
8+
webpack: (config, { isServer }) => {
99
config.externals.push({
1010
'@napi-rs/canvas': 'commonjs @napi-rs/canvas',
1111
});
1212

13+
// Prevent @nbw/thumbnail from being bundled on the server
14+
// It uses HTMLCanvasElement which is not available in Node.js
15+
if (isServer) {
16+
config.externals.push('@nbw/thumbnail');
17+
}
18+
1319
return config;
1420
},
1521
images: {

apps/frontend/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
"@mdx-js/react": "^3.1.1",
2020
"@nbw/config": "workspace:*",
2121
"@nbw/database": "workspace:*",
22+
"@nbw/song": "workspace:*",
23+
"@nbw/thumbnail": "workspace:*",
2224
"@next/mdx": "^16.0.8",
2325
"@next/third-parties": "^16.0.8",
2426
"@radix-ui/react-dialog": "^1.1.15",

apps/frontend/src/modules/shared/components/client/FormElements.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,9 @@ export const Select = forwardRef<
186186
/>
187187
)}
188188
{description && (
189-
<p className='block text-sm text-zinc-500 leading-tight pt-1 [&_a]:text-blue-400 [&_a:hover]:text-blue-300'>
189+
<div className='block text-sm text-zinc-500 leading-tight pt-1 [&_a]:text-blue-400 [&_a:hover]:text-blue-300'>
190190
<Markdown>{description}</Markdown>
191-
</p>
191+
</div>
192192
)}
193193
<ErrorBalloon message={errorMessage} />
194194
</>

apps/frontend/src/modules/song-edit/components/client/context/EditSong.context.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ import toaster from 'react-hot-toast';
1313
import { undefined as zodUndefined } from 'zod';
1414

1515
import type { UploadSongDto } from '@nbw/database';
16-
import { parseSongFromBuffer } from '@nbw/song';
17-
import type { SongFileType } from '@nbw/song';
16+
import { parseSongFromBuffer, type SongFileType } from '@nbw/song';
1817
import axiosInstance from '@web/lib/axios';
1918
import { getTokenLocal } from '@web/lib/axios/token.utils';
20-
2119
import {
2220
EditSongForm,
2321
editSongFormSchema,
24-
} from '../../../../song/components/client/SongForm.zod';
22+
} from '@web/modules/song/components/client/SongForm.zod';
2523

2624
export type useEditSongProviderType = {
2725
formMethods: UseFormReturn<EditSongForm>;

apps/frontend/src/modules/song-upload/components/client/context/UploadSong.context.tsx

Lines changed: 119 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
'use client';
22

33
import { zodResolver } from '@hookform/resolvers/zod';
4-
import { createContext, useContext, useEffect, useState } from 'react';
4+
import { createContext, useContext, useEffect } from 'react';
5+
import { create } from 'zustand';
56
import {
67
FieldErrors,
78
UseFormRegister,
@@ -11,7 +12,7 @@ import {
1112
import { toast } from 'react-hot-toast';
1213

1314
import { BG_COLORS, THUMBNAIL_CONSTANTS } from '@nbw/config';
14-
import { parseSongFromBuffer, SongFileType } from '@nbw/song';
15+
import { parseSongFromBuffer, type SongFileType } from '@nbw/song';
1516
import axiosInstance from '@web/lib/axios';
1617
import { InvalidTokenError, getTokenLocal } from '@web/lib/axios/token.utils';
1718
import {
@@ -21,6 +22,72 @@ import {
2122

2223
import UploadCompleteModal from '../UploadCompleteModal';
2324

25+
interface UploadSongState {
26+
song: SongFileType | null;
27+
filename: string | null;
28+
instrumentSounds: string[];
29+
isSubmitting: boolean;
30+
sendError: string | null;
31+
isUploadComplete: boolean;
32+
uploadedSongId: string | null;
33+
}
34+
35+
interface UploadSongActions {
36+
setSong: (song: SongFileType | null) => void;
37+
setFilename: (filename: string | null) => void;
38+
setInstrumentSounds: (sounds: string[]) => void;
39+
setInstrumentSound: (index: number, value: string) => void;
40+
setIsSubmitting: (isSubmitting: boolean) => void;
41+
setSendError: (error: string | null) => void;
42+
setIsUploadComplete: (isComplete: boolean) => void;
43+
setUploadedSongId: (id: string | null) => void;
44+
reset: () => void;
45+
}
46+
47+
type UploadSongStore = UploadSongState & UploadSongActions;
48+
49+
const initialState: UploadSongState = {
50+
song: null,
51+
filename: null,
52+
instrumentSounds: [],
53+
isSubmitting: false,
54+
sendError: null,
55+
isUploadComplete: false,
56+
uploadedSongId: null,
57+
};
58+
59+
export const useUploadSongStore = create<UploadSongStore>((set, get) => ({
60+
...initialState,
61+
62+
setSong: (song) => set({ song }),
63+
setFilename: (filename) => set({ filename }),
64+
setInstrumentSounds: (sounds) => set({ instrumentSounds: sounds }),
65+
setInstrumentSound: (index, value) => {
66+
const newValues = [...get().instrumentSounds];
67+
newValues[index] = value;
68+
set({ instrumentSounds: newValues });
69+
},
70+
setIsSubmitting: (isSubmitting) => set({ isSubmitting }),
71+
setSendError: (error) => set({ sendError: error }),
72+
setIsUploadComplete: (isComplete) => set({ isUploadComplete: isComplete }),
73+
setUploadedSongId: (id) => set({ uploadedSongId: id }),
74+
reset: () => set(initialState),
75+
}));
76+
77+
// Context for form methods (React Hook Form needs to be initialized in a component)
78+
interface UploadSongFormContextType {
79+
formMethods: UseFormReturn<UploadSongForm>;
80+
register: UseFormRegister<UploadSongForm>;
81+
errors: FieldErrors<UploadSongForm>;
82+
setFile: (file: File | null) => Promise<void>;
83+
setInstrumentSound: (index: number, value: string) => void;
84+
submitSong: () => Promise<void>;
85+
}
86+
87+
const UploadSongFormContext = createContext<UploadSongFormContextType | null>(
88+
null,
89+
);
90+
2491
export type useUploadSongProviderType = {
2592
song: SongFileType | null;
2693
filename: string | null;
@@ -37,27 +104,29 @@ export type useUploadSongProviderType = {
37104
uploadedSongId: string | null;
38105
};
39106

40-
export const UploadSongContext = createContext<useUploadSongProviderType>(
41-
null as unknown as useUploadSongProviderType,
42-
);
43-
44107
export const UploadSongProvider = ({
45108
children,
46109
}: {
47110
children: React.ReactNode;
48111
}) => {
49-
const [song, setSong] = useState<SongFileType | null>(null);
50-
const [filename, setFilename] = useState<string | null>(null);
51-
const [instrumentSounds, setInstrumentSounds] = useState<string[]>([]);
52-
const [isSubmitting, setIsSubmitting] = useState(false);
53-
54-
{
55-
/* TODO: React Hook Form has an isSubmitting attribute. Can we leverage it? https://react-hook-form.com/docs/useformstate */
56-
}
57-
58-
const [sendError, setSendError] = useState<string | null>(null);
59-
const [isUploadComplete, setIsUploadComplete] = useState(false);
60-
const [uploadedSongId, setUploadedSongId] = useState<string | null>(null);
112+
const store = useUploadSongStore();
113+
const {
114+
song,
115+
filename,
116+
instrumentSounds,
117+
isSubmitting,
118+
sendError,
119+
isUploadComplete,
120+
uploadedSongId,
121+
setSong,
122+
setFilename,
123+
setInstrumentSounds,
124+
setInstrumentSound: setInstrumentSoundStore,
125+
setIsSubmitting,
126+
setSendError,
127+
setIsUploadComplete,
128+
setUploadedSongId,
129+
} = store;
61130

62131
const formMethods = useForm<UploadSongForm>({
63132
resolver: zodResolver(uploadSongFormSchema),
@@ -164,30 +233,29 @@ export const UploadSongProvider = ({
164233
const setFileHandler = async (file: File | null) => {
165234
if (!file) return;
166235

167-
let song: SongFileType;
236+
let parsedSong: SongFileType;
168237

169238
try {
170-
song = (await parseSongFromBuffer(
239+
parsedSong = (await parseSongFromBuffer(
171240
await file.arrayBuffer(),
172241
)) as unknown as SongFileType; // TODO: Investigate this weird type error
173242
} catch (e) {
174243
console.error('Error parsing song file', e);
175244
toast.error('Invalid song file! Please try again with a different song.');
176245
setSong(null);
177-
178246
return;
179247
}
180248

181-
setSong(song);
249+
setSong(parsedSong);
182250
setFilename(file.name);
183251

184-
const { title, description, originalAuthor } = song;
252+
const { title, description, originalAuthor } = parsedSong;
185253
const formTitle = title || file.name.replace('.nbs', '');
186254
formMethods.setValue('title', formTitle);
187255
formMethods.setValue('description', description);
188256
formMethods.setValue('originalAuthor', originalAuthor);
189257

190-
const instrumentList = song.instruments.map(
258+
const instrumentList = parsedSong.instruments.map(
191259
(instrument) => instrument.file,
192260
);
193261

@@ -196,9 +264,9 @@ export const UploadSongProvider = ({
196264
};
197265

198266
const setInstrumentSound = (index: number, value: string) => {
267+
setInstrumentSoundStore(index, value);
199268
const newValues = [...instrumentSounds];
200269
newValues[index] = value;
201-
setInstrumentSounds(newValues);
202270
formMethods.setValue('customInstruments', newValues);
203271
};
204272

@@ -252,35 +320,42 @@ export const UploadSongProvider = ({
252320
};
253321
}, [formMethods.formState.isDirty, isUploadComplete]);
254322

323+
const formContextValue: UploadSongFormContextType = {
324+
formMethods,
325+
register,
326+
errors,
327+
setFile: setFileHandler,
328+
setInstrumentSound,
329+
submitSong,
330+
};
331+
255332
return (
256-
<UploadSongContext.Provider
257-
value={{
258-
sendError,
259-
formMethods,
260-
register,
261-
errors,
262-
submitSong,
263-
song,
264-
filename,
265-
instrumentSounds,
266-
setInstrumentSound,
267-
setFile: setFileHandler,
268-
isSubmitting,
269-
isUploadComplete,
270-
uploadedSongId,
271-
}}
272-
>
333+
<UploadSongFormContext.Provider value={formContextValue}>
273334
{uploadedSongId && (
274335
<UploadCompleteModal
275336
isOpen={isUploadComplete}
276337
songId={uploadedSongId}
277338
/>
278339
)}
279340
{children}
280-
</UploadSongContext.Provider>
341+
</UploadSongFormContext.Provider>
281342
);
282343
};
283344

345+
// Hook that combines Zustand store with React Hook Form from context
346+
// This maintains backward compatibility with the old Context API
284347
export const useUploadSongProvider = (): useUploadSongProviderType => {
285-
return useContext(UploadSongContext);
348+
const store = useUploadSongStore();
349+
const formContext = useContext(UploadSongFormContext);
350+
351+
if (!formContext) {
352+
throw new Error(
353+
'useUploadSongProvider must be used within UploadSongProvider',
354+
);
355+
}
356+
357+
return {
358+
...store,
359+
...formContext,
360+
};
286361
};

0 commit comments

Comments
 (0)