Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ This project prefers a highly componentized React codebase that avoids duplicate
- **Styling**: Use Tailwind CSS utility classes. Share common styling through base components rather than repeating class strings.
- **Git**: Do not commit .png files
- **Formatting**: Code is formatted with Prettier using tabs. Run `npx prettier --write` before committing. Don't add comments to code unless absolutely necessary.
- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass.
- **Type Safety**: You are not allowed to use `any` or `unknown`.
- **Redux**: Compose selectors and helpers rather than copy/pasting logic.
- **Redux**: Compose selectors and helpers rather than copy/pasting logic. UI components should avoid data manipulation—use Redux selectors to transform and format data instead of doing it in components.
- **Error Handling**: Keep error handling reasonable but not excessive. This is a small app - simple null checks and basic 404s are fine. Don't over-engineer with detailed error types for every edge case.
- **Unit Tests**: Write unit tests for important service functions, especially those involving business logic or data transformations.
- **Testing**: After changes, run `yarn test:codex` from the repository root to ensure all tests pass.

Follow these guidelines to keep the codebase clean and maintainable.
5 changes: 5 additions & 0 deletions apps/react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useDarkMode } from './utils/useDarkMode';
import { DeckStatsScreen } from './screens/DeckStatsScreen/DeckStatsScreen';
import { AccountScreen } from './screens/AccountScreen/AccountScreen';
import { AttemptHistoryScreen } from './screens/AttemptHistoryScreen/AttemptHistoryScreen';
import { StreakActivityScreen } from './screens/StreakActivityScreen/StreakActivityScreen';
import { useAppDispatch } from 'MemoryFlashCore/src/redux/store';
import { refreshUser } from 'MemoryFlashCore/src/redux/actions/refresh-user-action';

Expand Down Expand Up @@ -81,6 +82,10 @@ export default function App() {
path="/study/:deckId/stats"
element={<AuthenticatedRoute screen={<DeckStatsScreen />} />}
/>
<Route
path="/streak"
element={<AuthenticatedRoute screen={<StreakActivityScreen />} />}
/>
<Route
path="/study/:deckId/attempts"
element={<AuthenticatedRoute screen={<AttemptHistoryScreen />} />}
Expand Down
7 changes: 5 additions & 2 deletions apps/react/src/components/StreakChip.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import clsx from 'clsx';
import { useAppSelector } from 'MemoryFlashCore/src/redux/store';

Expand All @@ -25,17 +26,19 @@ export const StreakChip: React.FC<StreakChipProps> = ({ className }) => {
}, [count]);

return (
<div
<Link
to="/streak"
className={clsx(
'inline-flex items-center gap-1.5 rounded-full px-3 py-1.5 text-sm font-medium',
'bg-orange-50 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300',
'hover:bg-orange-100 dark:hover:bg-orange-900/50 transition-colors',
animate ? 'animate-bounce' : '',
className,
)}
aria-label={`Current streak ${displayCount} days`}
>
<span>🔥</span>
<span>{displayCount}</span>
</div>
</Link>
);
};
3 changes: 2 additions & 1 deletion apps/react/src/components/feedback/NetworkStateWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ export const NetworkStateWrapper: React.FC<NetworkStateWrapperProps> = ({
const showSpinner =
isLoading &&
(showSpinnerWhen === 'always' || (showSpinnerWhen === 'no-children' && !hasData));
const showChildren = !isLoading || hasData;

return (
<>
<Spinner show={showSpinner} />
<BasicErrorCard error={error} />
{children}
{showChildren && children}
</>
);
};
2 changes: 1 addition & 1 deletion apps/react/src/components/inputs/BaseTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const BaseTextArea = React.forwardRef<HTMLTextAreaElement, BaseTextAreaPr
({ className = '', ...props }, ref) => (
<textarea
ref={ref}
className={`block w-full rounded-md border-0 py-1.5 bg-white dark:bg-gray-800 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 resize-y min-h-[80px] ${className}`}
className={`block w-full rounded-md border border-default py-2 px-3 bg-surface text-fg placeholder:text-lm-muted dark:placeholder:text-dm-muted focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent transition-all duration-150 sm:text-sm resize-y min-h-[80px] ${className}`}
{...props}
/>
),
Expand Down
2 changes: 1 addition & 1 deletion apps/react/src/components/inputs/TextAreaField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const TextAreaField: React.FC<TextAreaFieldProps> = ({
...props
}) => (
<div>
<label htmlFor={id} className="block text-sm font-medium leading-6 text-gray-900">
<label htmlFor={id} className="block text-sm font-medium leading-6 text-fg">
{label}
</label>
<div className="mt-2 relative">
Expand Down
22 changes: 0 additions & 22 deletions apps/react/src/components/notation/BarsSetting.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions apps/react/src/components/notation/ChordProgressionInput.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { InputField } from '../inputs';
import { InputField, TextAreaField } from '../inputs';
import { ChordToneDisplay } from './ChordToneDisplay';
import { ChordMemorySettings } from './defaultSettings';
import { parseChordProgression } from './parseChordProgression';
Expand Down Expand Up @@ -35,7 +35,7 @@ export const ChordProgressionInput: React.FC<ChordProgressionInputProps> = ({
value={chordMemory.progression}
onChange={(e) => handleProgressionChange(e.target.value)}
/>
<InputField
<TextAreaField
id="chord-text-prompt"
label="Text Prompt (optional)"
placeholder="e.g., Autumn Leaves - Verse"
Expand Down
14 changes: 10 additions & 4 deletions apps/react/src/components/notation/NotationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import React from 'react';
import { TranspositionSelector } from '../TranspositionSelector';
import { majorKeys } from 'MemoryFlashCore/src/lib/notes';
import { NotationSettingsState } from './defaultSettings';
import { NoteSettings } from './NoteSettings';
import { SheetMusicSettings } from './SheetMusicSettings';
import { SettingsSection } from './SettingsSection';
import { RangeSettings } from './RangeSettings';
import { CardTypeOptions } from './CardTypeOptions';
import { BarsSetting } from './BarsSetting';

interface NotationSettingsProps {
settings: NotationSettingsState;
Expand All @@ -28,11 +27,18 @@ export const NotationSettings: React.FC<NotationSettingsProps> = ({ settings, on
(selected, i) => selected && i !== currentKeyIdx,
);

const isChordMemory = settings.cardType === 'Chord Memory';

return (
<div className="space-y-4">
<NoteSettings keySig={settings.keySig} onChange={update} />
<CardTypeOptions settings={settings} onChange={update} />
<BarsSetting bars={settings.bars} setBars={(n) => update({ bars: n })} />
{!isChordMemory && (
<SheetMusicSettings
keySig={settings.keySig}
bars={settings.bars}
onChange={update}
/>
)}
<SettingsSection
title="Transpositions"
collapsible={true}
Expand Down
26 changes: 0 additions & 26 deletions apps/react/src/components/notation/NoteSettings.tsx

This file was deleted.

107 changes: 67 additions & 40 deletions apps/react/src/components/notation/ScoreToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import React from 'react';
import clsx from 'clsx';
import { BaseDuration, Duration } from 'MemoryFlashCore/src/lib/measure';
import { StaffEnum } from 'MemoryFlashCore/src/types/Cards';
import { useScoreEditor } from './ScoreEditor';

const baseDurations: BaseDuration[] = ['w', 'h', 'q', '8', '16'];
const tieOptions: Duration[] = ['w', 'h', 'q', '8', '16', '32', '64'];

const btn = 'border rounded flex items-center justify-center text-lg';
const btnBase =
'border border-default rounded flex items-center justify-center text-sm font-medium transition-colors';
const btnActive = 'bg-gray-200 dark:bg-dm-elevated';
const btnInactive = 'bg-lm-surface dark:bg-dm-surface hover:bg-gray-100 dark:hover:bg-dm-elevated';

interface ToolbarButtonProps {
active?: boolean;
onClick: () => void;
children: React.ReactNode;
className?: string;
}

const ToolbarButton: React.FC<ToolbarButtonProps> = ({
active = false,
onClick,
children,
className,
}) => (
<button
className={clsx(btnBase, active ? btnActive : btnInactive, className)}
onClick={onClick}
>
{children}
</button>
);

export const ScoreToolbar: React.FC = () => {
const {
Expand All @@ -23,62 +48,64 @@ export const ScoreToolbar: React.FC = () => {
} = useScoreEditor();
const tiedDurations = durations.slice(1);
return (
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted w-14">Duration</span>
{baseDurations.map((d) => (
<button
<ToolbarButton
key={d}
className={`${btn} w-10 h-10 ${dur === d ? 'bg-gray-200' : ''}`}
active={dur === d}
onClick={() => setDur(d)}
className="w-9 h-9"
>
{d}
</button>
</ToolbarButton>
))}
<button
className={`${btn} w-10 h-10 ${dotted ? 'bg-gray-200' : ''}`}
onClick={toggleDot}
>
<ToolbarButton active={dotted} onClick={toggleDot} className="w-9 h-9">
.
</button>
<button className={`${btn} w-10 h-10`} onClick={addRest}>
rest
</button>
<button
className={`${btn} px-3 h-10 ${staff === StaffEnum.Treble ? 'bg-gray-200' : ''}`}
onClick={() => setStaff(StaffEnum.Treble)}
>
Treble
</button>
<button
className={`${btn} px-3 h-10 ${staff === StaffEnum.Bass ? 'bg-gray-200' : ''}`}
onClick={() => setStaff(StaffEnum.Bass)}
>
Bass
</button>
</ToolbarButton>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted">ties</span>
<span className="text-sm text-muted w-14">Tie</span>
{tiedDurations.length === 0 && <span className="text-sm text-muted">none</span>}
{tiedDurations.map((duration, index) => (
<button
<ToolbarButton
key={`tie-${index}-${duration}`}
className={`${btn} h-8 px-3 text-base`}
onClick={() => removeTieDuration(index)}
className="h-8 px-2"
>
{duration} ×
</button>
</ToolbarButton>
))}
<div className="flex gap-1">
{tieOptions.map((option) => (
<button
key={`tie-option-${option}`}
className={`${btn} h-8 px-2 text-base`}
onClick={() => addTieDuration(option)}
>
+{option}
</button>
))}
</div>
{tieOptions.map((option) => (
<ToolbarButton
key={`tie-option-${option}`}
onClick={() => addTieDuration(option)}
className="h-8 px-2"
>
+{option}
</ToolbarButton>
))}
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-muted w-14">Staff</span>
<ToolbarButton
active={staff === StaffEnum.Treble}
onClick={() => setStaff(StaffEnum.Treble)}
className="px-3 h-9"
>
Treble
</ToolbarButton>
<ToolbarButton
active={staff === StaffEnum.Bass}
onClick={() => setStaff(StaffEnum.Bass)}
className="px-3 h-9"
>
Bass
</ToolbarButton>
<ToolbarButton onClick={addRest} className="px-3 h-9">
Rest
</ToolbarButton>
</div>
</div>
);
Expand Down
42 changes: 42 additions & 0 deletions apps/react/src/components/notation/SheetMusicSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import { Select, NumberInput } from '../inputs';
import { majorKeys } from 'MemoryFlashCore/src/lib/notes';
import { SettingsSection } from './SettingsSection';
import { ScoreToolbar } from './ScoreToolbar';

interface SheetMusicSettingsProps {
keySig: string;
bars: number;
onChange: (changes: Partial<{ keySig: string; bars: number }>) => void;
}

export const SheetMusicSettings: React.FC<SheetMusicSettingsProps> = ({
keySig,
bars,
onChange,
}) => (
<SettingsSection title="Sheet Music Settings">
<div className="space-y-4">
<div className="flex gap-4">
<label className="flex items-center gap-2">
Key
<Select value={keySig} onChange={(e) => onChange({ keySig: e.target.value })}>
{majorKeys.map((k) => (
<option key={k}>{k}</option>
))}
</Select>
</label>
<label className="flex items-center gap-2">
Bars
<NumberInput
className="w-16"
min={1}
value={bars}
onChange={(e) => onChange({ bars: parseInt(e.target.value, 10) || 1 })}
/>
</label>
</div>
<ScoreToolbar />
</div>
</SettingsSection>
);
3 changes: 1 addition & 2 deletions apps/react/src/components/notation/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
export * from './NotationSettings';
export * from './NoteSettings';
export * from './SheetMusicSettings';
export * from './RangeSettings';
export * from './CardTypeOptions';
export * from './SettingsSection';
export * from './NotationPreviewList';
export * from './defaultSettings';
export * from './BarsSetting';
2 changes: 1 addition & 1 deletion apps/react/src/components/ui/buttonStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const buttonBaseClasses =

export const variantEnabledStyles: Record<ButtonVariant, string> = {
primary:
'bg-accent text-white hover:bg-accent-hover dark:bg-[#e8e8ea] dark:text-[#1a1a1a] dark:hover:bg-[#d4d4d6] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
'bg-accent text-white hover:bg-accent-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent',
secondary:
'bg-gray-100 text-lm-fg hover:bg-gray-200 dark:bg-dm-elevated dark:text-dm-fg dark:hover:bg-white/15',
outline:
Expand Down
Loading