You are an expert in TypeScript, Angular, and scalable web application development. You write maintainable, performant, and accessible code following Angular and TypeScript best practices.
WhyTV is a web application that provides a TV-like experience for watching YouTube videos. Users create custom "channels" based on descriptions of their interests, and the system uses AI to automatically curate endless video playlists from YouTube.
- Dev Server:
ng serveornpm start- Start local development - Build:
ng buildornpm run build- Build for production - Test:
ng test- Run unit tests with Karma - Generate:
ng generate component component-name- Create component - Functions:
cd functions && npm run build- Build Firebase functionscd functions && npm run serve- Start Firebase emulatorscd functions && npm run lint- Lint Firebase functions
This project follows the Angular coding standards located in the CODING_STANDARDS directory. Please ensure all code adheres to these standards:
- Review CODING_STANDARDS/angular-cheat-sheet.md for quick reference of law codes
- Follow patterns in CODING_STANDARDS/angular_coding_standards.md
- Use state management patterns from CODING_STANDARDS/angular-state-architecture.md
IMPORTANT: Treat these standards as linting rules. Before writing or modifying any Angular code:
- Check the relevant law codes in the cheat sheet
- Ensure your implementation follows the prescribed patterns
- Document any necessary deviations in project-overrides.md
- Coding Standards: See CODING_STANDARDS directory for universal Angular/TypeScript standards
- Quick Reference: Angular Cheat Sheet with law codes
- Navigation Guide: Documentation Map for finding information
- Library References: See docs directory for external library documentation
- Project Overrides: Project-specific deviations from standards
- View Encapsulation: Always use
ViewEncapsulation.Noneto avoid ng-deep anti-pattern - Component Styles: Wrap all styles in component's selector (e.g.,
app-my-component { }) - PrimeNG Overrides: Use nested selectors within component wrapper
- Never Use: Avoid
:hostand::ng-deepselectors
- Video Controls: No custom controls UI (progress bar, volume slider, playback speed)
- Channel Rail: Referenced but component doesn't exist
- Comments: Button exists but no implementation
- Share: Button exists but no implementation
- SpeedDial Menu: Implemented but commented out in template
- State Fragmentation: Three different stores (channels, videoPlayer, userActivity) with overlapping concerns
- Inconsistent Data Flow: Some components update state directly, others use services
- Duplicate Code: HomePage has duplicate handlers that exist in SideActionsComponent
- Performance: All channel videos loaded upfront, no lazy loading
- Remove console.log statements throughout codebase
- Remove duplicate event handlers in HomePage
- Consolidate channel creation to use only CreateChannelDialogComponent
- Fix unprotected state stores (currently using
protectedState: false)
ALWAYS follow this pattern to prevent circular updates:
User Action → Component → Service Method → Store Update → Component (via signals)
// GOOD: Read from store signals
export class MyComponent {
private channelsState = inject(ChannelsState);
// Use signals directly in template
currentChannel = this.channelsState.currentChannel;
// Or create computed signals
hasVideos = computed(() =>
this.channelsState.currentChannel()?.videos?.length > 0
);
}// GOOD: Update through service methods
export class MyComponent {
private channelService = inject(ChannelService);
selectChannel(channelId: string) {
// Service handles the state update
this.channelService.setCurrentChannel(channelId);
}
}// GOOD: Service updates store
export class ChannelService {
private channelsState = inject(ChannelsState);
setCurrentChannel(channelId: string) {
const channel = this.channelsState.channels()
.find(c => c.id === channelId);
if (channel) {
patchState(this.channelsState, {
currentChannel: channel,
currentVideoIndex: 0
});
}
}
}// BAD: Component directly updates store
export class MyComponent {
private channelsState = inject(ChannelsState);
selectChannel(channel: Channel) {
// NEVER do this in components!
patchState(this.channelsState, {
currentChannel: channel
});
}
}// BAD: State for same feature managed in multiple places
// This caused the circular update issues mentioned by the user
export class ComponentA {
showControls = signal(false); // Local state
}
export class ComponentB {
playerState = inject(videoPlayerState);
// Different component managing same concept
updateControls() {
patchState(this.playerState, { showControls: true });
}
}- Read: Use signals for current channel, videos, etc.
- Update: Only through ChannelService methods
- Never: Update channel/video selection directly in components
- Read: Use signals for playback state, volume, etc.
- Update: Through VideoPlayerService methods or RxJS subjects
- Special: Player component can update time/duration directly for performance
- Read: Use isActive signal and activity$ observable
- Update: Only through UserActivityService.markActive()
- Never: Manage activity state locally in components
// GOOD: Centralized event handling
export class SideActionsComponent {
private videoPlayerService = inject(VideoPlayerService);
handleMuteToggle() {
// Service handles state update
this.videoPlayerService.toggleMute();
}
}- Don't create local state for anything shared between components
- Don't subscribe to subjects in multiple places to update the same state
- Don't use both signals and RxJS for the same state concept
- Don't update state in effects or computed signals
- Don't forget to clean up subscriptions (use takeUntilDestroyed)
Firebase → FirestoreService → ChannelService → ChannelsState → Components
User Action (outside of player component) → VideoPlayerService (RxJS subjects) → WhytvPlayerComponent → videoPlayerState
User Interaction → UserActivityService → userActivity$ observable → UI visibility
Channel creation follows this status progression:
new → processing queries → queries ready → select_videos → videos_selected → created
Each function updates the status to trigger the next function in the chain.
WhyTV uses three distinct state stores with Angular signals and @ngrx/signals, each serving a specific purpose while maintaining clean separation of concerns.
Store Responsibilities:
- Channel catalog management (all channels with 'live' status)
- Current channel selection tracking
- Video collections for each channel
- Current video index tracking within active channel
- Channel metadata (name, number, description, AI model settings)
Service (ChannelService): [CS-S05]
- Channel data fetching from Firestore
- Channel creation and management
- Current channel selection
- Video navigation within channels
Key Design: Videos are embedded within channels rather than having their own store, simplifying the data model since videos always belong to a channel.
Store Responsibilities:
- Playback state (playing/paused/buffering/ended)
- Player settings (volume, muted state, playback speed)
- Current video metadata (title, duration, current time)
- UI visibility states (controls visibility, user activity)
- Liked status for current video
Service (VideoPlayerService):
- Player control event streams (play$, pause$, seek$, volume$, etc.)
- YouTube API interaction abstraction
- Player state synchronization
- Coordinating between components and player instance
Special Pattern: Uses RxJS Subjects as an event bus for player controls, allowing components to emit commands that the player component subscribes to.
State Responsibilities:
- User activity tracking (active/inactive)
- Last activity timestamp
- Inactivity timeout management
Service (UserActivityService):
- Activity detection and timeout logic
- Emitting activity state changes
- Managing the 3-second inactivity timer
- Currently using
protectedState: falseto allow direct updates (temporary) - Components inject both services and stores directly
- VideoPlayerService acts as event coordinator between components