Skip to content

Commit 3abf71e

Browse files
committed
refactor: update component aliases and enhance UI components
- Changed component aliases in components.json to reflect new module structure. - Updated layout.tsx to include a 'dark' class for improved styling. - Added a new utility function oklchToRgb for color conversion in utils.ts. - Introduced a Slider component in the shared UI module for better user interaction. - Refactored SongForm and SongThumbnailInput components to utilize new module paths and the Slider component for enhanced functionality.
1 parent 23569c4 commit 3abf71e

File tree

8 files changed

+167
-70
lines changed

8 files changed

+167
-70
lines changed

apps/frontend/components.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
},
1313
"iconLibrary": "lucide",
1414
"aliases": {
15-
"components": "@web/components",
15+
"components": "@web/modules/shared/components",
1616
"utils": "@web/lib/utils",
17-
"ui": "@web/components/ui",
18-
"lib": "@web/lib",
19-
"hooks": "@web/hooks"
17+
"ui": "@web/modules/shared/components/ui",
18+
"lib": "@web/modules/shared/lib",
19+
"hooks": "@web/modules/shared/hooks"
2020
},
2121
"registries": {}
2222
}

apps/frontend/src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export default function RootLayout({
8888
/>
8989
<meta name='theme-color' content='#3295ff' />
9090
</head>
91-
<body className={lato.className + ' bg-zinc-900 text-white h-full'}>
91+
<body className={lato.className + 'dark bg-zinc-900 text-white h-full'}>
9292
<NextTopLoader
9393
showSpinner={false}
9494
crawlSpeed={700}

apps/frontend/src/lib/utils.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,59 @@ import { twMerge } from 'tailwind-merge';
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs));
66
}
7+
8+
/**
9+
* Converts OKLCH color format to RGB hex code
10+
* @param oklchString - OKLCH color string (e.g., "oklch(38% 0.189 293.745)")
11+
* @returns RGB hex code (e.g., "#1a1a1a")
12+
*/
13+
export function oklchToRgb(oklchString: string): string {
14+
// Parse OKLCH values from string like "oklch(38% 0.189 293.745)"
15+
const match = oklchString.match(/oklch\(([^)]+)\)/);
16+
if (!match) {
17+
throw new Error(`Invalid OKLCH format: ${oklchString}`);
18+
}
19+
20+
const parts = match[1].trim().split(/\s+/);
21+
const l = parseFloat(parts[0].replace('%', '')) / 100; // Convert percentage to 0-1
22+
const c = parseFloat(parts[1]);
23+
const h = parseFloat(parts[2]) * (Math.PI / 180); // Convert degrees to radians
24+
25+
// Convert OKLCH to OKLab
26+
const a = c * Math.cos(h);
27+
const bLab = c * Math.sin(h);
28+
29+
// Convert OKLab to linear RGB
30+
// OKLab to linear sRGB matrix (inverse of sRGB to OKLab)
31+
const l_ = l + 0.3963377774 * a + 0.2158037573 * bLab;
32+
const m_ = l - 0.1055613458 * a - 0.0638541728 * bLab;
33+
const s_ = l - 0.0894841775 * a - 1.291485548 * bLab;
34+
35+
const l3 = l_ * l_ * l_;
36+
const m3 = m_ * m_ * m_;
37+
const s3 = s_ * s_ * s_;
38+
39+
const r_ = 4.0767416621 * l3 - 3.3077115913 * m3 + 0.2309699292 * s3;
40+
const g_ = -1.2684380046 * l3 + 2.6097574011 * m3 - 0.3413193965 * s3;
41+
const b_ = -0.0041960863 * l3 - 0.7034186147 * m3 + 1.707614701 * s3;
42+
43+
// Convert linear RGB to sRGB (gamma correction)
44+
const gamma = (c: number) => {
45+
if (c <= 0.0031308) {
46+
return 12.92 * c;
47+
}
48+
return 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
49+
};
50+
51+
const r = Math.max(0, Math.min(1, gamma(r_)));
52+
const g = Math.max(0, Math.min(1, gamma(g_)));
53+
const b = Math.max(0, Math.min(1, gamma(b_)));
54+
55+
// Convert to 0-255 range and then to hex
56+
const toHex = (n: number) => {
57+
const hex = Math.round(n * 255).toString(16);
58+
return hex.length === 1 ? '0' + hex : hex;
59+
};
60+
61+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
62+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use client';
2+
3+
import * as React from 'react';
4+
import * as SliderPrimitive from '@radix-ui/react-slider';
5+
6+
import { cn } from '@web/lib/utils';
7+
8+
const Slider = React.forwardRef<
9+
React.ElementRef<typeof SliderPrimitive.Root>,
10+
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
11+
>(({ className, ...props }, ref) => (
12+
<SliderPrimitive.Root
13+
ref={ref}
14+
className={cn(
15+
'relative flex w-full touch-none select-none items-center',
16+
className,
17+
)}
18+
{...props}
19+
>
20+
<SliderPrimitive.Track className='relative h-1.5 w-full grow overflow-hidden rounded-full bg-zinc-700'>
21+
<SliderPrimitive.Range className='absolute h-full bg-zinc-500' />
22+
</SliderPrimitive.Track>
23+
<SliderPrimitive.Thumb className='block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' />
24+
</SliderPrimitive.Root>
25+
));
26+
Slider.displayName = SliderPrimitive.Root.displayName;
27+
28+
export { Slider };

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,7 @@ export const UploadSongProvider = ({
236236
let parsedSong: SongFileType;
237237

238238
try {
239-
parsedSong = (await parseSongFromBuffer(
240-
await file.arrayBuffer(),
241-
)) as unknown as SongFileType; // TODO: Investigate this weird type error
239+
parsedSong = await parseSongFromBuffer(await file.arrayBuffer());
242240
} catch (e) {
243241
console.error('Error parsing song file', e);
244242
toast.error('Invalid song file! Please try again with a different song.');
@@ -298,8 +296,6 @@ export const UploadSongProvider = ({
298296
);
299297

300298
formMethods.setValue('allowDownload', true);
301-
302-
// disable allowDownload
303299
}
304300
}, [song, formMethods]);
305301

@@ -342,8 +338,6 @@ export const UploadSongProvider = ({
342338
);
343339
};
344340

345-
// Hook that combines Zustand store with React Hook Form from context
346-
// This maintains backward compatibility with the old Context API
347341
export const useUploadSongProvider = (): useUploadSongProviderType => {
348342
const store = useUploadSongStore();
349343
const formContext = useContext(UploadSongFormContext);

apps/frontend/src/modules/song/components/client/SongForm.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,24 @@ import {
2020
Select,
2121
TextArea,
2222
UploadButton,
23-
} from '../../../shared/components/client/FormElements';
24-
import { useSongProvider } from '../../../song/components/client/context/Song.context';
23+
} from '@web/modules/shared/components/client/FormElements';
24+
import { useSongProvider } from '@web/modules/song/components/client/context/Song.context';
2525

2626
import InstrumentPicker from './InstrumentPicker';
2727
import { SongThumbnailInput } from './SongThumbnailInput';
28+
import React from 'react';
2829

2930
type SongFormProps = {
3031
type: 'upload' | 'edit';
3132
isLoading?: boolean;
3233
isLocked?: boolean;
3334
};
3435

35-
export const SongForm = ({
36+
export const SongForm: React.FC<SongFormProps> = ({
3637
type,
3738
isLoading = false,
3839
isLocked = false,
39-
}: SongFormProps) => {
40+
}) => {
4041
const useSongProviderData = useSongProvider(type);
4142

4243
const { sendError, errors, submitSong, isSubmitting, formMethods, register } =

apps/frontend/src/modules/song/components/client/SongThumbnailInput.tsx

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { useMemo } from 'react';
22
import { UseFormReturn } from 'react-hook-form';
33

44
import { BG_COLORS, THUMBNAIL_CONSTANTS } from '@nbw/config';
5-
import { NoteQuadTree } from '@nbw/song';
6-
import { cn } from '@web/lib/utils';
5+
import { cn, oklchToRgb } from '@web/lib/utils';
76
import {
87
Tooltip,
98
TooltipContent,
@@ -13,24 +12,27 @@ import {
1312
import { useSongProvider } from './context/Song.context';
1413
import { EditSongForm, UploadSongForm } from './SongForm.zod';
1514
import { ThumbnailRendererCanvas } from './ThumbnailRenderer';
15+
import { Slider } from '@web/modules/shared/components/ui/slider';
1616

1717
const formatZoomLevel = (zoomLevel: number) => {
1818
const percentage = 100 * Math.pow(2, zoomLevel - 3);
1919
return `${percentage}%`;
2020
};
2121

22-
function ThumbnailSliders({
23-
formMethods,
24-
isLocked,
25-
maxTick,
26-
maxLayer,
27-
}: {
22+
type ThumbnailSlidersProps = {
2823
formMethods: UseFormReturn<UploadSongForm> & UseFormReturn<EditSongForm>;
2924
isLocked: boolean;
3025
maxTick: number;
3126
maxLayer: number;
32-
}) {
33-
const { register } = formMethods;
27+
};
28+
29+
const ThumbnailSliders: React.FC<ThumbnailSlidersProps> = ({
30+
formMethods,
31+
isLocked,
32+
maxTick,
33+
maxLayer,
34+
}) => {
35+
const { setValue } = formMethods;
3436

3537
const [zoomLevel, startTick, startLayer] = formMethods.watch([
3638
'thumbnailData.zoomLevel',
@@ -44,13 +46,15 @@ function ThumbnailSliders({
4446
<label htmlFor='zoom-level'>Zoom Level</label>
4547
</div>
4648
<div>
47-
<input
48-
type='range'
49+
<Slider
4950
id='zoom-level'
50-
className='w-full disabled:cursor-not-allowed'
51-
{...register('thumbnailData.zoomLevel', {
52-
valueAsNumber: true,
53-
})}
51+
value={[zoomLevel]}
52+
onValueChange={(value) => {
53+
setValue('thumbnailData.zoomLevel', value[0], {
54+
shouldValidate: true,
55+
});
56+
}}
57+
className='w-full'
5458
disabled={isLocked}
5559
min={THUMBNAIL_CONSTANTS.zoomLevel.min}
5660
max={THUMBNAIL_CONSTANTS.zoomLevel.max}
@@ -61,14 +65,15 @@ function ThumbnailSliders({
6165
<label htmlFor='start-tick'>Start Tick</label>
6266
</div>
6367
<div className='w-full'>
64-
<input
65-
type='range'
68+
<Slider
6669
id='start-tick'
67-
className='w-full disabled:cursor-not-allowed'
68-
{...register('thumbnailData.startTick', {
69-
valueAsNumber: true,
70-
max: maxTick,
71-
})}
70+
value={[startTick]}
71+
onValueChange={(value) => {
72+
setValue('thumbnailData.startTick', value[0], {
73+
shouldValidate: true,
74+
});
75+
}}
76+
className='w-full'
7277
disabled={isLocked}
7378
min={THUMBNAIL_CONSTANTS.startTick.default}
7479
max={maxTick}
@@ -79,14 +84,15 @@ function ThumbnailSliders({
7984
<label htmlFor='start-layer'>Start Layer</label>
8085
</div>
8186
<div className='w-full'>
82-
<input
83-
type='range'
87+
<Slider
8488
id='start-layer'
85-
className='w-full disabled:cursor-not-allowed'
86-
{...register('thumbnailData.startLayer', {
87-
valueAsNumber: true,
88-
max: maxLayer,
89-
})}
89+
value={[startLayer]}
90+
onValueChange={(value) => {
91+
setValue('thumbnailData.startLayer', value[0], {
92+
shouldValidate: true,
93+
});
94+
}}
95+
className='w-full'
9096
disabled={isLocked}
9197
min={THUMBNAIL_CONSTANTS.startLayer.default}
9298
max={maxLayer}
@@ -95,21 +101,22 @@ function ThumbnailSliders({
95101
<div>{startLayer}</div>
96102
</div>
97103
);
98-
}
104+
};
99105

100-
const ColorButton = ({
101-
color,
102-
tooltip,
103-
active,
104-
onClick,
105-
disabled,
106-
}: {
106+
type ColorButtonProps = {
107107
color: string;
108108
tooltip: string;
109109
active: boolean;
110-
111110
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
112111
disabled: boolean;
112+
};
113+
114+
const ColorButton: React.FC<ColorButtonProps> = ({
115+
color,
116+
tooltip,
117+
active,
118+
onClick,
119+
disabled,
113120
}) => (
114121
<Tooltip>
115122
<TooltipTrigger asChild>
@@ -128,12 +135,14 @@ const ColorButton = ({
128135
</Tooltip>
129136
);
130137

131-
export const SongThumbnailInput = ({
132-
type,
133-
isLocked,
134-
}: {
138+
type SongThumbnailInputProps = {
135139
type: 'upload' | 'edit';
136140
isLocked: boolean;
141+
};
142+
143+
export const SongThumbnailInput: React.FC<SongThumbnailInputProps> = ({
144+
type,
145+
isLocked,
137146
}) => {
138147
const { song, formMethods } = useSongProvider(type);
139148

@@ -156,10 +165,7 @@ export const SongThumbnailInput = ({
156165
/>
157166

158167
{song && notes && (
159-
<ThumbnailRendererCanvas
160-
notes={notes as unknown as NoteQuadTree} //TODO: fix this bizarre type cast
161-
formMethods={formMethods}
162-
/>
168+
<ThumbnailRendererCanvas notes={notes} formMethods={formMethods} />
163169
)}
164170

165171
{/* Background Color */}
@@ -177,8 +183,10 @@ export const SongThumbnailInput = ({
177183
}
178184
onClick={(e: React.MouseEvent<HTMLButtonElement>) => {
179185
e.preventDefault();
180-
181-
formMethods.setValue('thumbnailData.backgroundColor', dark);
186+
formMethods.setValue(
187+
'thumbnailData.backgroundColor',
188+
oklchToRgb(dark),
189+
);
182190
}}
183191
/>
184192
))}

0 commit comments

Comments
 (0)