Awesome Cursor Rules Collection

Showing 2437-2448 of 2626 matches

TypeScript
# .cursorrules

rules:
  # General TypeScript Rules
  - match: "*.ts"
    rules:
      - description: "Prefer explicit types for variables and function returns"
        regex: "(let|const) [a-zA-Z_][a-zA-Z0-9_]* ="
        replace: "$1 $2: type ="
        message: "Specify the type explicitly for better readability and type safety."

      - description: "Use camelCase for variable and function names"
        regex: "(let|const|function) [a-z0-9_]+"
        replace: "$1 camelCaseName"
        message: "Use camelCase for variable and function names to maintain consistency."

      - description: "Use PascalCase for class and interface names"
        regex: "(class|interface) [a-z][a-zA-Z0-9_]+"
        replace: "$1 PascalCaseName"
        message: "Use PascalCase for class and interface names for consistency."

  # EdgeDB-js Rules
  - match: "*.ts"
    rules:
      - description: "Prefer using edgeql-js query builder over raw EdgeQL strings"
        regex: "client\\.query\\(`"
        replace: "e.<query_method>()"
        message: "Use the edgeql-js query builder for type-safe and readable queries."

      - description: "Use generated edgeql-js types for better type safety"
        regex: "any"
        replace: "<GeneratedEdgeDBType>"
        message: "Replace 'any' with specific EdgeDB types generated by edgeql-js."

  # Hono Framework Rules
  - match: "*.ts"
    rules:
      - description: "Use Hono's request handler typing for type-safe route handlers"
        regex: "async function (req, res)"
        replace: "async (c: Context) =>"
        message: "Use Hono's context type for request handlers to ensure type safety."

  # File Structure Rules for Atomic Files
  - match: "*.ts"
    rules:
      - description: "Limit to one export per file to ensure atomic file structure"
        regex: "(export const|export function|export class) .*\\n(export const|export function|export class)"
        replace: ""
        message: "Consider breaking this file into multiple files with a single export per file."

      - description: "Ensure file names reflect their content and purpose (e.g., getUser.ts, createUserRoute.ts)"
        regex: "(queries|routes)\\/(.*)\\.(ts|js)"
        replace: "$1/$2.$3"
        message: "Follow the naming convention: queries should describe the action (e.g., getUser.ts), and routes should reflect the endpoint or handler (e.g., createUserRoute.ts)."

  # Enforce Documentation and Comments
  - match: "*.ts"
    rules:
      - description: "Ensure all functions have JSDoc comments"
        regex: "function [a-zA-Z_][a-zA-Z0-9_]*\\(.*\\)\\s*{"
        replace: "/**\n * Description of the function\n * @param {type} param - Description of the parameter\n * @returns {type} Description of the return value\n */\n$0"
        message: "Add JSDoc comments to functions to improve code readability and maintainability."

  # Optimize Import Statements
  - match: "*.ts"
    rules:
      - description: "Remove unused imports"
        regex: "import .* from '.*';\\s*"
        replace: ""
        message: "Remove unused imports to keep the codebase clean."

      - description: "Sort import statements alphabetically"
        regex: "(import .+ from '.+');\\n(import .+ from '.+');"
        replace: "$2\n$1"
        message: "Sort import statements alphabetically for better readability."

  # Error Handling Rules
  - match: "*.ts"
    rules:
      - description: "Use a consistent error handling approach in async functions"
        regex: "async function .+\$begin:math:text$.*\\$end:math:text$ \\{"
        replace: "async function <functionName>(<params>) {\n  try {\n    // your code here\n  } catch (error) {\n    // handle error\n  }\n}"
        message: "Ensure consistent error handling in async functions using try/catch."

  # Limit Function and Class Length
  - match: "*.ts"
    rules:
      - description: "Limit function length to 30 lines for better readability and maintainability"
        regex: "function [a-zA-Z_][a-zA-Z0-9_]*\$begin:math:text$.*\\$end:math:text$ {([^}]*)}"
        replace: ""
        message: "Refactor this function to reduce its length and improve readability."

      - description: "Limit class length to 200 lines for better maintainability"
        regex: "class [a-zA-Z_][a-zA-Z0-9_]* {([^}]*)}"
        replace: ""
        message: "Consider splitting this class into smaller, more focused classes."

  # Additional Best Practices
  - match: "*.ts"
    rules:
      - description: "Encourage the use of TypeScript interfaces for function parameters"
        regex: "function [a-zA-Z_][a-zA-Z0-9_]*\$begin:math:text$([^)]*)\\$end:math:text$:"
        replace: "interface FunctionParams {\n  // define parameter types here\n}\n\nfunction <functionName>(params: FunctionParams):"
        message: "Use TypeScript interfaces to define function parameters for better type safety."
css
edgeql
html
typescript
stemil23/Hono-EdgeDB-Cloudflare-starter

Used in 1 repository

Python
Only change the part where I asked
fastapi
python
vue.js
dexhunter/ai-conference-organizer

Used in 1 repository

Rust
# Project: Rust Shell Implementation (CodeCrafters)

## Code Architecture
- Implement modular design with separate components for:
  - Command parsing and lexing
  - Process management
  - Built-in commands
  - REPL (Read-Eval-Print Loop)
- Use trait-based abstractions for command execution
- Follow POSIX compliance standards for shell behavior

## Rust-Specific Standards
- Use Rust 2021 edition
- Implement proper error handling with custom Error types
- Use `Result` and `Option` types appropriately
- Prefer ownership and borrowing over raw pointers
- Use strong typing with minimal use of `unsafe` blocks

## Style Guidelines
- Follow the official Rust style guide (rustfmt defaults)
- Maximum line length: 100 characters
- Use 4 spaces for indentation
- Organize imports in groups:
  1. Standard library
  2. External crates
  3. Local modules
- Name conventions:
  - Use snake_case for functions and variables
  - Use PascalCase for types and traits
  - Use SCREAMING_SNAKE_CASE for constants

## Shell-Specific Best Practices
- Implement proper signal handling (SIGINT, SIGTERM, etc.)
- Use appropriate system calls for process management
- Handle command line parsing according to POSIX rules
- Implement proper quoting and escape character handling
- Use efficient string manipulation for command processing

## Error Handling
- Create custom error types for different failure scenarios
- Implement detailed error messages for debugging
- Use the `thiserror` crate for error definitions
- Properly propagate errors up the call stack
- Handle all Result types explicitly

## Testing
- Write unit tests for all modules
- Create integration tests for shell commands
- Test edge cases in command parsing
- Implement tests for built-in commands
- Test process management functionality
- Aim for >80% test coverage

## Documentation
- Use rustdoc comments for all public items
- Document POSIX compliance details
- Include examples in documentation
- Keep inline comments focused on "why" not "what"
- Maintain a comprehensive README.md

## Performance Considerations
- Minimize allocations in hot paths
- Use appropriate data structures for command history
- Implement efficient process spawning
- Consider memory usage in long-running sessions
- Profile code for performance bottlenecks

## Security
- Validate all user inputs
- Handle environment variables safely
- Implement proper permission checking
- Sanitize command arguments
- Handle sensitive data appropriately

## Dependencies
- Minimize external dependencies
- Prefer well-maintained crates with good security records
- Use specific versions in Cargo.toml
- Review dependency updates regularly
- Document why each dependency is needed

## Version Control
- Use meaningful commit messages
- Keep commits focused and atomic
- Use feature branches for development
- Write detailed PR descriptions
- Regular commits with clear progress

Remember to:
- Follow POSIX specifications strictly
- Consider cross-platform compatibility
- Focus on robust error handling
- Maintain clear documentation
- Prioritize security in implementation
golang
rust
shell
shitover/codecrafters-shell-rust

Used in 1 repository

C++
You are an AI assistant who specializes in Flutter.

Follow these steps to generate your response:

1. Analyze the conversation history:
- Review all previous interactions between you and the user.
- Identify recurring themes, topics, or concepts the user has asked about.
- Assess the user's current level of understanding based on their questions and responses.

2. Understand the user's prompt and level:
- Carefully examine the user's current prompt.
- Identify the main technical topic or concept they're asking about.
- Determine if there are any underlying concepts the user might be struggling with.
- Consider how this question relates to previous topics discussed in the conversation history.
- Estimate the user's current level of understanding on this specific topic.

3. Analyze the codebase:
${CODEBASE}

- Review the codebase to understand the context of the user's prompt.
- Identify any relevant files, functions, or concepts that are related to the user's prompt.
- Consider how the user's prompt fits into the overall codebase and architecture.

4. Prepare your response:
- Start with a brief introduction to the topic.
- Structure your explanation with gradually increasing complexity, tailored to the user's estimated
level.
- Include real-world examples and implementations to illustrate the concept.
- Compare and contrast with similar concepts or technologies when relevant.
- Mention tips, tricks, best practices, and common pitfalls related to the topic.
- If applicable, suggest memorization techniques for key points.

Remember:
- Always prioritize clarity and relevance in your explanations.
- Use appropriate technical terminology, but be sure to explain any complex terms.
- If you're unsure about any aspect of the user's question, ask for clarification before providing
an explanation.
- Encourage the user to ask follow-up questions if any part of your explanation is unclear.

If applicable, follow these special instructions for shell scripts:
- Use only Unix-style line endings (LF)
- Avoid special Unicode characters
- Include only ASCII-compatible quotes and brackets
- Is properly formatted for direct use in a bash script

<tool-versions>
pnpm 9.12.1
nodejs 22.9.0
flutter 3.27.1-stable
java temurin-17.0.9+9
</tool-versions>


<example-repo>
Directory Structure:
Directory Structure:

└── ./
    └── Moblin
        ├── Platforms
        │   └── Kick
        │       ├── KickChannel.swift
        │       ├── KickPusher.swift
        │       └── KickViewers.swift
        └── Various
            ├── ChatBotCommand.swift
            ├── ChatTextToSpeech.swift
            ├── UrlSession.swift
            ├── Utils.swift
            ├── WebSocetClient.swift
            └── WebSocketClient.swift



---
File: /Moblin/Platforms/Kick/KickChannel.swift
---

import Foundation

struct KickLivestream: Codable {
    // periphery:ignore
    let id: Int
    let viewers: Int
}

struct KickChatroom: Codable {
    let id: Int
}

struct KickChannel: Codable {
    // periphery:ignore
    let slug: String
    let chatroom: KickChatroom
    let livestream: KickLivestream?
}

func getKickChannelInfo(channelName: String) async throws -> KickChannel {
    guard let url = URL(string: "https://kick.com/api/v1/channels/\(channelName)") else {
        throw "Invalid URL"
    }
    let (data, response) = try await httpGet(from: url)
    if !response.isSuccessful {
        throw "Not successful"
    }
    return try JSONDecoder().decode(KickChannel.self, from: data)
}

func getKickChannelInfo(channelName: String, onComplete: @escaping (KickChannel?) -> Void) {
    guard let url = URL(string: "https://kick.com/api/v1/channels/\(channelName)") else {
        onComplete(nil)
        return
    }
    let request = URLRequest(url: url)
    URLSession.shared.dataTask(with: request) { data, response, error in
        guard error == nil, let data, response?.http?.isSuccessful == true else {
            onComplete(nil)
            return
        }
        onComplete(try? JSONDecoder().decode(KickChannel.self, from: data))
    }
    .resume()
}



---
File: /Moblin/Platforms/Kick/KickPusher.swift
---

import Foundation

private struct Badge: Decodable {
    var type: String
}

private struct Identity: Decodable {
    var color: String
    var badges: [Badge]
}

private struct Sender: Decodable {
    var username: String
    var identity: Identity
}

private struct ChatMessage: Decodable {
    var content: String
    var sender: Sender

    func isModerator() -> Bool {
        return sender.identity.badges.contains(where: { $0.type == "moderator" })
    }

    func isSubscriber() -> Bool {
        return sender.identity.badges.contains(where: { $0.type == "subscriber" })
    }
}

private func decodeEvent(message: String) throws -> (String, String) {
    if let jsonData = message.data(using: String.Encoding.utf8) {
        let data = try JSONSerialization.jsonObject(
            with: jsonData,
            options: JSONSerialization.ReadingOptions.mutableContainers
        )
        if let jsonResult: NSDictionary = data as? NSDictionary {
            if let type: String = jsonResult["event"] as? String {
                if let data: String = jsonResult["data"] as? String {
                    return (type, data)
                }
            }
        }
    }
    throw "Failed to get message event type"
}

private func decodeChatMessage(data: String) throws -> ChatMessage {
    return try JSONDecoder().decode(
        ChatMessage.self,
        from: data.data(using: String.Encoding.utf8)!
    )
}

private var url =
    URL(
        string: "wss://ws-us2.pusher.com/app/32cbd69e4b950bf97679?protocol=7&client=js&version=7.6.0&flash=false"
    )!

protocol KickOusherDelegate: AnyObject {
    func kickPusherMakeErrorToast(title: String, subTitle: String?)
    func kickPusherAppendMessage(
        user: String,
        userColor: RgbColor?,
        segments: [ChatPostSegment],
        isSubscriber: Bool,
        isModerator: Bool
    )
}

final class KickPusher: NSObject {
    private var channelName: String
    private var channelId: String
    private var webSocket: WebSocketClient
    private var emotes: Emotes
    private let settings: SettingsStreamChat
    private var gotInfo = false
    private weak var delegate: (any KickOusherDelegate)?

    init(delegate: KickOusherDelegate, channelId: String, channelName: String, settings: SettingsStreamChat) {
        self.delegate = delegate
        self.channelId = channelId
        self.channelName = channelName
        self.settings = settings.clone()
        emotes = Emotes()
        webSocket = .init(url: url)
    }

    func start() {
        logger.debug("kick: Start")
        stopInternal()
        if channelName.isEmpty {
            connect()
        } else {
            getInfoAndConnect()
        }
    }

    private func getInfoAndConnect() {
        logger.debug("kick: Get info and connect")
        getKickChannelInfo(channelName: channelName) { [weak self] channelInfo in
            guard let self else {
                return
            }
            DispatchQueue.main.async {
                guard !self.gotInfo else {
                    return
                }
                guard let channelInfo else {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
                        self.getInfoAndConnect()
                    }
                    return
                }
                self.gotInfo = true
                self.channelId = String(channelInfo.chatroom.id)
                self.connect()
            }
        }
    }

    private func connect() {
        emotes.stop()
        emotes.start(
            platform: .kick,
            channelId: channelId,
            onError: handleError,
            onOk: handleOk,
            settings: settings
        )
        webSocket = .init(url: url)
        webSocket.delegate = self
        webSocket.start()
    }

    func stop() {
        logger.debug("kick: Stop")
        stopInternal()
    }

    func stopInternal() {
        emotes.stop()
        webSocket.stop()
        gotInfo = false
    }

    func isConnected() -> Bool {
        return webSocket.isConnected()
    }

    func hasEmotes() -> Bool {
        return emotes.isReady()
    }

    private func handleError(title: String, subTitle: String) {
        DispatchQueue.main.async {
            self.delegate?.kickPusherMakeErrorToast(title: title, subTitle: subTitle)
        }
    }

    private func handleOk(title: String) {
        DispatchQueue.main.async {
            self.delegate?.kickPusherMakeErrorToast(title: title, subTitle: nil)
        }
    }

    private func handleMessage(message: String) {
        do {
            let (type, data) = try decodeEvent(message: message)
            if type == "App\\Events\\ChatMessageEvent" {
                try handleChatMessageEvent(data: data)
            } else {
                logger.debug("kick: pusher: \(channelId): Unsupported type: \(type)")
            }
        } catch {
            logger
                .error("""
                kick: pusher: \(channelId): Failed to process \
                message \"\(message)\" with error \(error)
                """)
        }
    }

    private func handleChatMessageEvent(data: String) throws {
        let message = try decodeChatMessage(data: data)
        var segments: [ChatPostSegment] = []
        var id = 0
        for var segment in createKickSegments(message: message.content, id: &id) {
            if let text = segment.text {
                segments += emotes.createSegments(text: text, id: &id)
                segment.text = nil
            }
            if segment.text != nil || segment.url != nil {
                segments.append(segment)
            }
        }
        delegate?.kickPusherAppendMessage(
            user: message.sender.username,
            userColor: RgbColor.fromHex(string: message.sender.identity.color),
            segments: segments,
            isSubscriber: message.isSubscriber(),
            isModerator: message.isModerator()
        )
    }

    private func sendMessage(message: String) {
        logger.debug("kick: pusher: \(channelId): Sending \(message)")
        webSocket.send(string: message)
    }

    private func createKickSegments(message: String, id: inout Int) -> [ChatPostSegment] {
        var segments: [ChatPostSegment] = []
        var startIndex = message.startIndex
        for match in message[startIndex...].matches(of: /\[emote:(\d+):[^\]]+\]/) {
            let emoteId = match.output.1
            let textBeforeEmote = message[startIndex ..< match.range.lowerBound]
            let url = URL(string: "https://files.kick.com/emotes/\(emoteId)/fullsize")
            segments += makeChatPostTextSegments(text: String(textBeforeEmote), id: &id)
            segments.append(ChatPostSegment(id: id, url: url))
            id += 1
            startIndex = match.range.upperBound
        }
        if startIndex != message.endIndex {
            segments += makeChatPostTextSegments(text: String(message[startIndex...]), id: &id)
        }
        return segments
    }
}

extension KickPusher: WebSocketClientDelegate {
    func webSocketClientConnected(_: WebSocketClient) {
        logger.debug("kick: Connected")
        sendMessage(
            message: """
            {\"event\":\"pusher:subscribe\",
             \"data\":{\"auth\":\"\",\"channel\":\"chatrooms.\(channelId).v2\"}}
            """
        )
    }

    func webSocketClientDisconnected(_: WebSocketClient) {
        logger.debug("kick: Disconnected")
    }

    func webSocketClientReceiveMessage(_: WebSocketClient, string: String) {
        handleMessage(message: string)
    }
}



---
File: /Moblin/Platforms/Kick/KickViewers.swift
---

import Foundation

class KickViewers {
    private var task: Task<Void, Error>?
    var numberOfViewers: Int?

    func start(channelName: String) {
        task = Task.init {
            var delay = 1
            while true {
                do {
                    try await sleep(seconds: delay)
                    let info = try await getKickChannelInfo(channelName: channelName)
                    await self.setNumberOfViewers(value: info.livestream?.viewers)
                } catch {}
                if Task.isCancelled {
                    await self.setNumberOfViewers(value: nil)
                    break
                }
                delay = 30
            }
        }
    }

    private func setNumberOfViewers(value: Int?) async {
        await MainActor.run {
            self.numberOfViewers = value
        }
    }

    func stop() {
        task?.cancel()
        task = nil
    }
}



---
File: /Moblin/Various/ChatBotCommand.swift
---

import Collections

struct ChatBotMessage {
    let platform: Platform
    let user: String?
    let isModerator: Bool
    let isSubscriber: Bool
    let userId: String?
    let segments: [ChatPostSegment]
}

class ChatBotCommand {
    let message: ChatBotMessage
    private var parts: Deque<String> = []

    init?(message: ChatBotMessage) {
        self.message = message
        guard message.segments.count > 1 else {
            return nil
        }
        for segment in message.segments.suffix(from: 1) {
            if let text = segment.text {
                parts.append(text.trim())
            }
        }
    }

    func popFirst() -> String? {
        return parts.popFirst()
    }

    func rest() -> String {
        return parts.joined(separator: " ")
    }

    func user() -> String? {
        return message.user
    }
}



---
File: /Moblin/Various/ChatTextToSpeech.swift
---

import AVFAudio
import Collections
import NaturalLanguage

private let textToSpeechDispatchQueue = DispatchQueue(label: "com.eerimoq.textToSpeech", qos: .utility)

private struct TextToSpeechMessage {
    let user: String
    let message: String
    let isRedemption: Bool
}

private let saysByLanguage = [
    "en": "says",
    "sv": "säger",
    "no": "sier",
    "es": "dice",
    "de": "sagt",
    "fr": "dit",
    "pl": "mówi",
    "vi": "nói",
    "nl": "zegt",
    "zh": "说",
    "ko": "라고",
    "ru": "говорит",
    "uk": "каже",
]

class ChatTextToSpeech: NSObject {
    private var rate: Float = 0.4
    private var volume: Float = 0.6
    private var sayUsername: Bool = false
    private var detectLanguagePerMessage: Bool = false
    private var voices: [String: String] = [:]
    private var messageQueue: Deque<TextToSpeechMessage> = .init()
    private var synthesizer = AVSpeechSynthesizer()
    private var recognizer = NLLanguageRecognizer()
    private var latestUserThatSaidSomething: String?
    private var sayLatestUserThatSaidSomethingAgain = ContinuousClock.now
    private var filterEnabled: Bool = true
    private var filterMentionsEnabled: Bool = true
    private var streamerMentions: [String] = []
    private var running = true

    private func isFilteredOut(message: String) -> Bool {
        if isFilteredOutFilter(message: message) {
            return true
        }
        if isFilteredOutFilterMentions(message: message) {
            return true
        }
        return false
    }

    private func isFilteredOutFilter(message: String) -> Bool {
        if !filterEnabled {
            return false
        }
        let probability = recognizer.languageHypotheses(withMaximum: 1).first?.value ?? 0.0
        if probability < 0.7 && message.count > 30 {
            return true
        }
        if message.hasPrefix("!") {
            return true
        }
        if message.contains("https") {
            return true
        }
        return false
    }

    private func isFilteredOutFilterMentions(message: String) -> Bool {
        if !filterMentionsEnabled {
            return false
        }
        if message.starts(with: "@") || message.contains(" @") {
            for streamerMention in streamerMentions {
                if let range = message.range(of: streamerMention) {
                    if isStreamerMention(
                        message: message,
                        mentionLowerBound: range.lowerBound,
                        mentionUpperBound: range.upperBound
                    ) {
                        return false
                    }
                }
            }
            return true
        }
        return false
    }

    private func isStreamerMention(
        message: String,
        mentionLowerBound: String.Index,
        mentionUpperBound: String.Index
    ) -> Bool {
        // There is always a space at the end of the message, so this should never happen.
        guard mentionUpperBound < message.endIndex else {
            return false
        }
        if mentionLowerBound > message.startIndex {
            if message[message.index(before: mentionLowerBound)] != " " {
                return false
            }
        }
        if message[mentionUpperBound] != " " {
            return false
        }
        return true
    }

    private func getSays(_ language: String) -> String {
        return saysByLanguage[language] ?? ""
    }

    private func getVoice(message: String) -> (AVSpeechSynthesisVoice?, String)? {
        recognizer.reset()
        recognizer.processString(message)
        guard !isFilteredOut(message: message) else {
            return nil
        }
        var language = recognizer.dominantLanguage?.rawValue
        if !detectLanguagePerMessage || language == nil {
            language = Locale.current.language.languageCode?.identifier
        }
        guard let language else {
            return nil
        }
        if let voiceIdentifier = voices[language] {
            return (AVSpeechSynthesisVoice(identifier: voiceIdentifier), getSays(language))
        } else if let voice = AVSpeechSynthesisVoice.speechVoices()
            .filter({ $0.language.starts(with: language) }).first
        {
            return (AVSpeechSynthesisVoice(identifier: voice.identifier), getSays(language))
        }
        return nil
    }

    private func trySayNextMessage() {
        guard !synthesizer.isSpeaking else {
            return
        }
        guard let message = messageQueue.popFirst() else {
            return
        }
        guard let (voice, says) = getVoice(message: message.message) else {
            return
        }
        guard let voice else {
            return
        }
        let text: String
        let now = ContinuousClock.now
        if message.isRedemption {
            text = "\(message.user) \(message.message)"
        } else if !shouldSayUser(user: message.user, now: now) || !sayUsername {
            text = message.message
        } else {
            text = String(localized: "\(message.user) \(says): \(message.message)")
        }
        let utterance = AVSpeechUtterance(string: text)
        utterance.rate = rate
        utterance.pitchMultiplier = 0.8
        utterance.preUtteranceDelay = 0.05
        utterance.volume = volume
        utterance.voice = voice
        synthesizer.speak(utterance)
        latestUserThatSaidSomething = message.user
        sayLatestUserThatSaidSomethingAgain = now.advanced(by: .seconds(30))
    }

    private func shouldSayUser(user: String, now: ContinuousClock.Instant) -> Bool {
        if user != latestUserThatSaidSomething {
            return true
        }
        if now > sayLatestUserThatSaidSomethingAgain {
            return true
        }
        return false
    }

    func say(user: String, message: String, isRedemption: Bool) {
        textToSpeechDispatchQueue.async {
            guard self.running else {
                return
            }
            self.messageQueue.append(.init(user: user, message: message, isRedemption: isRedemption))
            self.trySayNextMessage()
        }
    }

    func setRate(rate: Float) {
        textToSpeechDispatchQueue.async {
            self.rate = rate
        }
    }

    func setVolume(volume: Float) {
        textToSpeechDispatchQueue.async {
            self.volume = volume
        }
    }

    func setVoices(voices: [String: String]) {
        textToSpeechDispatchQueue.async {
            self.voices = voices
        }
    }

    func setSayUsername(value: Bool) {
        textToSpeechDispatchQueue.async {
            self.sayUsername = value
        }
    }

    func setFilter(value: Bool) {
        textToSpeechDispatchQueue.async {
            self.filterEnabled = value
        }
    }

    func setFilterMentions(value: Bool) {
        textToSpeechDispatchQueue.async {
            self.filterMentionsEnabled = value
        }
    }

    func setStreamerMentions(streamerMentions: [String]) {
        textToSpeechDispatchQueue.async {
            self.streamerMentions = streamerMentions
        }
    }

    func setDetectLanguagePerMessage(value: Bool) {
        textToSpeechDispatchQueue.async {
            self.detectLanguagePerMessage = value
        }
    }

    func reset(running: Bool) {
        textToSpeechDispatchQueue.async {
            self.running = running
            self.synthesizer.stopSpeaking(at: .word)
            self.latestUserThatSaidSomething = nil
            self.messageQueue.removeAll()
            self.synthesizer = AVSpeechSynthesizer()
            self.synthesizer.delegate = self
            self.recognizer = NLLanguageRecognizer()
        }
    }

    func skipCurrentMessage() {
        textToSpeechDispatchQueue.async {
            self.synthesizer.stopSpeaking(at: .word)
            self.trySayNextMessage()
        }
    }
}

extension ChatTextToSpeech: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_: AVSpeechSynthesizer, didFinish _: AVSpeechUtterance) {
        textToSpeechDispatchQueue.async {
            self.trySayNextMessage()
        }
    }
}



---
File: /Moblin/Various/UrlSession.swift
---

import Foundation

extension URLSession {
    static func create(httpProxy: HttpProxy?) -> URLSession {
        if let httpProxy {
            if #available(iOS 17, *) {
                let configuration = URLSessionConfiguration.default
                configuration.proxyConfigurations = [
                    .init(httpCONNECTProxy: .hostPort(
                        host: .init(httpProxy.host),
                        port: .init(integerLiteral: httpProxy.port)
                    )),
                ]
                return URLSession(configuration: configuration)
            }
        }
        return URLSession.shared
    }
}



---
File: /Moblin/Various/Utils.swift
---

import AVKit
import MapKit
import MetalPetal
import SwiftUI
import WeatherKit

extension UIImage {
    func scalePreservingAspectRatio(targetSize: CGSize) -> UIImage {
        let widthRatio = targetSize.width / size.width
        let heightRatio = targetSize.height / size.height
        let scaleFactor = min(widthRatio, heightRatio)
        let scaledImageSize = CGSize(
            width: size.width * scaleFactor,
            height: size.height * scaleFactor
        )
        let renderer = UIGraphicsImageRenderer(
            size: scaledImageSize
        )
        let scaledImage = renderer.image { _ in
            self.draw(in: CGRect(
                origin: .zero,
                size: scaledImageSize
            ))
        }
        return scaledImage
    }

    func resize(height: CGFloat) -> UIImage {
        let size = CGSize(width: size.width * (height / size.height), height: height)
        let format = UIGraphicsImageRendererFormat()
        format.scale = 1
        let image = UIGraphicsImageRenderer(size: size, format: format).image { _ in
            draw(in: CGRect(origin: .zero, size: size))
        }

        return image.withRenderingMode(renderingMode)
    }
}

func widgetImage(widget: SettingsWidget) -> String {
    switch widget.type {
    case .image:
        return "photo"
    case .videoEffect:
        return "camera.filters"
    case .browser:
        return "globe"
    case .text:
        return "textformat"
    case .crop:
        return "crop"
    case .map:
        return "map"
    case .scene:
        return "photo.on.rectangle"
    case .qrCode:
        return "qrcode"
    case .alerts:
        return "megaphone"
    case .videoSource:
        return "video"
    case .scoreboard:
        return "rectangle.split.2x1"
    }
}

extension Data {
    static func random(length: Int) -> Data {
        return Data((0 ..< length).map { _ in UInt8.random(in: UInt8.min ... UInt8.max) })
    }
}

func randomString() -> String {
    return Data.random(length: 64).base64EncodedString()
}

func randomHumanString() -> String {
    return Data.random(length: 15).base64EncodedString().replacingOccurrences(
        of: "[+/=]",
        with: "",
        options: .regularExpression
    )
}

func isGoodPassword(password: String) -> Bool {
    guard password.count >= 16 else {
        return false
    }
    var seenCharacters = ""
    for character in password {
        if seenCharacters.contains(character) {
            return false
        }
        seenCharacters.append(character)
    }
    guard password.contains(/\d/) else {
        return false
    }
    return true
}

func randomGoodPassword() -> String {
    while true {
        let password = randomHumanString()
        if isGoodPassword(password: password) {
            return password
        }
    }
}

func getOrientation() -> UIDeviceOrientation {
    let orientation = UIDevice.current.orientation
    if orientation != .unknown {
        return orientation
    }
    let interfaceOrientation = UIApplication.shared.connectedScenes
        .first(where: { $0 is UIWindowScene })
        .flatMap { $0 as? UIWindowScene }?.interfaceOrientation
    switch interfaceOrientation {
    case .landscapeLeft:
        return .landscapeRight
    case .landscapeRight:
        return .landscapeLeft
    default:
        return .unknown
    }
}

extension AVCaptureDevice {
    func getZoomFactorScale(hasUltraWideCamera: Bool) -> Float {
        if hasUltraWideCamera {
            switch deviceType {
            case .builtInTripleCamera, .builtInDualWideCamera, .builtInUltraWideCamera:
                return 0.5
            case .builtInTelephotoCamera:
                return (virtualDeviceSwitchOverVideoZoomFactors.last?.floatValue ?? 10.0) / 2
            default:
                return 1.0
            }
        } else {
            switch deviceType {
            case .builtInTelephotoCamera:
                return virtualDeviceSwitchOverVideoZoomFactors.last?.floatValue ?? 2.0
            default:
                return 1.0
            }
        }
    }

    func getUIZoomRange(hasUltraWideCamera: Bool) -> (Float, Float) {
        let factor = getZoomFactorScale(hasUltraWideCamera: hasUltraWideCamera)
        return (Float(minAvailableVideoZoomFactor) * factor, Float(maxAvailableVideoZoomFactor) * factor)
    }

    var fps: (Double, Double) {
        (1 / activeVideoMinFrameDuration.seconds, 1 / activeVideoMaxFrameDuration.seconds)
    }
}

func cameraName(device: AVCaptureDevice?) -> String {
    guard let device else {
        return ""
    }
    if ProcessInfo().isiOSAppOnMac {
        return device.localizedName
    } else {
        switch device.deviceType {
        case .builtInTripleCamera:
            return String(localized: "Triple (auto)")
        case .builtInDualCamera:
            return String(localized: "Dual (auto)")
        case .builtInDualWideCamera:
            return String(localized: "Wide dual (auto)")
        case .builtInUltraWideCamera:
            return String(localized: "Ultra wide")
        case .builtInWideAngleCamera:
            return String(localized: "Wide")
        case .builtInTelephotoCamera:
            return String(localized: "Telephoto")
        default:
            return device.localizedName
        }
    }
}

func hasUltraWideBackCamera() -> Bool {
    return AVCaptureDevice.default(.builtInUltraWideCamera, for: .video, position: .back) != nil
}

func getBestBackCameraDevice() -> AVCaptureDevice? {
    var device = AVCaptureDevice.default(.builtInTripleCamera, for: .video, position: .back)
    if device == nil {
        device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back)
    }
    if device == nil {
        device = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back)
    }
    if device == nil {
        device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back)
    }
    return device
}

func getBestFrontCameraDevice() -> AVCaptureDevice? {
    return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front)
}

func getBestBackCameraId() -> String {
    guard let device = getBestBackCameraDevice() else {
        return ""
    }
    return device.uniqueID
}

func getBestFrontCameraId() -> String {
    guard let device = getBestFrontCameraDevice() else {
        return ""
    }
    return device.uniqueID
}

func openUrl(url: String) {
    UIApplication.shared.open(URL(string: url)!)
}

extension UIDevice {
    static func vibrate() {
        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
    }
}

extension URL {
    var attributes: [FileAttributeKey: Any]? {
        do {
            return try FileManager.default.attributesOfItem(atPath: path)
        } catch {
            logger.info("file-system: Failed to get attributes for file \(self)")
        }
        return nil
    }

    var fileSize: UInt64 {
        return attributes?[.size] as? UInt64 ?? UInt64(0)
    }

    func remove() {
        do {
            try FileManager.default.removeItem(at: self)
        } catch {
            logger.info("file-system: Failed to remove file \(self)")
        }
    }
}

private var thumbnails: [URL: UIImage] = [:]

func createThumbnail(path: URL) -> UIImage? {
    if let thumbnail = thumbnails[path] {
        return thumbnail
    }
    do {
        let asset = AVURLAsset(url: path, options: nil)
        let imgGenerator = AVAssetImageGenerator(asset: asset)
        imgGenerator.appliesPreferredTrackTransform = true
        let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
        let thumbnail = UIImage(cgImage: cgImage)
        thumbnails[path] = thumbnail
        return thumbnail
    } catch {
        logger.info("Failed to create thumbnail with error \(error)")
        return nil
    }
}

extension SettingsPrivacyRegion {
    func contains(coordinate: CLLocationCoordinate2D) -> Bool {
        cos(toRadians(degrees: latitude - coordinate.latitude)) >
            cos(toRadians(degrees: latitudeDelta / 2.0)) &&
            cos(toRadians(degrees: longitude - coordinate.longitude)) >
            cos(toRadians(degrees: longitudeDelta / 2.0))
    }
}

func toRadians(degrees: Double) -> Double {
    return degrees * .pi / 180
}

func toLatitudeDeltaDegrees(meters: Double) -> Double {
    return 360 * meters / 40_075_000
}

func toLongitudeDeltaDegrees(meters: Double, latitudeDegrees: Double) -> Double {
    return 360 * meters / (40_075_000 * cos(toRadians(degrees: latitudeDegrees)))
}

extension CLLocationCoordinate2D {
    func translateMeters(x: Double, y: Double) -> CLLocationCoordinate2D {
        let latitudeDelta = toLatitudeDeltaDegrees(meters: y)
        var newLatitude = (latitude < 0 ? 360 + latitude : latitude) + latitudeDelta
        newLatitude -= Double(360 * (Int(newLatitude) / 360))
        if newLatitude > 270 {
            newLatitude -= 360
        } else if newLatitude > 90 {
            newLatitude = 180 - newLatitude
        }
        let longitudeDelta = toLongitudeDeltaDegrees(meters: x, latitudeDegrees: latitude)
        var newLongitude = (longitude < 0 ? 360 + longitude : longitude) + longitudeDelta
        newLongitude -= Double(360 * (Int(newLongitude) / 360))
        if newLongitude > 180 {
            newLongitude = newLongitude - 360
        }
        return .init(latitude: newLatitude, longitude: newLongitude)
    }
}

extension MKCoordinateRegion: @retroactive Equatable {
    public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool {
        if lhs.center.latitude != rhs.center.latitude || lhs.center.longitude != rhs.center.longitude {
            return false
        }
        if lhs.span.latitudeDelta != rhs.span.latitudeDelta || lhs.span.longitudeDelta != rhs.span
            .longitudeDelta
        {
            return false
        }
        return true
    }
}

struct WidgetCrop {
    let position: CGPoint
    let crop: CGRect
}

func hasAppleLog() -> Bool {
    if #available(iOS 17.0, *) {
        for format in getBestBackCameraDevice()?.formats ?? []
            where format.supportedColorSpaces.contains(.appleLog)
        {
            return true
        }
    }
    return false
}

func factorToIso(device: AVCaptureDevice, factor: Float) -> Float {
    var iso = device.activeFormat.minISO + (device.activeFormat.maxISO - device.activeFormat.minISO) * factor
        .clamped(to: 0 ... 1)
    if !iso.isFinite {
        iso = 0
    }
    return iso
}

func factorFromIso(device: AVCaptureDevice, iso: Float) -> Float {
    var factor = (iso - device.activeFormat.minISO) /
        (device.activeFormat.maxISO - device.activeFormat.minISO)
    if !factor.isFinite {
        factor = 0
    }
    return factor.clamped(to: 0 ... 1)
}

let minimumWhiteBalanceTemperature: Float = 2200
let maximumWhiteBalanceTemperature: Float = 10000

func factorToWhiteBalance(device: AVCaptureDevice, factor: Float) -> AVCaptureDevice.WhiteBalanceGains {
    let temperature = minimumWhiteBalanceTemperature +
        (maximumWhiteBalanceTemperature - minimumWhiteBalanceTemperature) * factor
    let temperatureAndTint = AVCaptureDevice.WhiteBalanceTemperatureAndTintValues(
        temperature: temperature,
        tint: 0
    )
    return device.deviceWhiteBalanceGains(for: temperatureAndTint)
        .clamped(maxGain: device.maxWhiteBalanceGain)
}

func factorFromWhiteBalance(device: AVCaptureDevice, gains: AVCaptureDevice.WhiteBalanceGains) -> Float {
    let temperature = device.temperatureAndTintValues(for: gains).temperature
    return (temperature - minimumWhiteBalanceTemperature) /
        (maximumWhiteBalanceTemperature - minimumWhiteBalanceTemperature)
}

extension AVCaptureDevice.WhiteBalanceGains {
    func clamped(maxGain: Float) -> AVCaptureDevice.WhiteBalanceGains {
        return .init(redGain: redGain.clamped(to: 1 ... maxGain),
                     greenGain: greenGain.clamped(to: 1 ... maxGain),
                     blueGain: blueGain.clamped(to: 1 ... maxGain))
    }
}

func makeAudioCodecString() -> String {
    return "AAC"
}

extension MTILayer {
    convenience init(content: MTIImage, position: CGPoint) {
        self.init(
            content: content,
            layoutUnit: .pixel,
            position: position,
            size: content.size,
            rotation: 0,
            opacity: 1,
            blendMode: .normal
        )
    }
}

func isValidAudioBitrate(bitrate: Int) -> Bool {
    guard bitrate >= 32000, bitrate <= 320_000 else {
        return false
    }
    guard bitrate % 32000 == 0 else {
        return false
    }
    return true
}

func currentPresentationTimeStamp() -> CMTime {
    return CMClockGetTime(CMClockGetHostTimeClock())
}

func utcTimeDeltaFromNow(to: Double) -> Double {
    return Date(timeIntervalSince1970: to).timeIntervalSinceNow
}

func emojiFlag(country: String) -> String {
    let base: UInt32 = 127_397
    var emote = ""
    for ch in country.unicodeScalars {
        emote.unicodeScalars.append(UnicodeScalar(base + ch.value)!)
    }
    return emote
}

func isPhone() -> Bool {
    return UIDevice.current.userInterfaceIdiom == .phone
}

func isPad() -> Bool {
    return UIDevice.current.userInterfaceIdiom == .pad
}

func uploadImage(
    url: URL,
    paramName: String,
    fileName: String,
    image: Data,
    message: String?,
    onCompleted: @escaping (Bool) -> Void
) {
    let boundary = UUID().uuidString
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "content-type")
    var data = Data()
    if let message {
        data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
        data.append("content-disposition: form-data; name=\"content\"\r\n\r\n".data(using: .utf8)!)
        data.append(message.data(using: .utf8)!)
    }
    data.append("\r\n--\(boundary)\r\n".data(using: .utf8)!)
    data.append(
        "content-disposition: form-data; name=\"\(paramName)\"; filename=\"\(fileName)\"\r\n"
            .data(using: .utf8)!
    )
    data.append("content-type: image/jpeg\r\n\r\n".data(using: .utf8)!)
    data.append(image)
    data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!)
    URLSession.shared.uploadTask(with: request, from: data, completionHandler: { _, response, _ in
        onCompleted(response?.http?.isSuccessful == true)
    }).resume()
}

func formatCommercialStartedDuration(seconds: Int) -> String {
    let minutes = seconds / 60
    if minutes * 60 == seconds {
        if minutes == 1 {
            return "1 minute"
        } else {
            return "\(minutes) minutes"
        }
    } else {
        return "\(seconds) seconds"
    }
}

struct HttpProxy {
    var host: String
    var port: UInt16
}

extension CGSize {
    func maximum() -> CGFloat {
        return max(height, width)
    }
}



---
File: /Moblin/Various/WebSocetClient.swift
---

//
//  WebSocetClient.swift
//  Moblin
//
//  Created by Erik Moqvist on 2024-06-05.
//

import Foundation



---
File: /Moblin/Various/WebSocketClient.swift
---

import Network
import NWWebSocket
import SwiftUI
import TwitchChat

private let shortestDelayMs = 500
private let longestDelayMs = 10000

protocol WebSocketClientDelegate: AnyObject {
    func webSocketClientConnected(_ webSocket: WebSocketClient)
    func webSocketClientDisconnected(_ webSocket: WebSocketClient)
    func webSocketClientReceiveMessage(_ webSocket: WebSocketClient, string: String)
}

final class WebSocketClient {
    private var webSocket: NWWebSocket
    private var connectTimer = SimpleTimer(queue: .main)
    private var networkInterfaceTypeSelector = NetworkInterfaceTypeSelector(queue: .main)
    private var pingTimer = SimpleTimer(queue: .main)
    private var pongReceived = true
    var delegate: (any WebSocketClientDelegate)?
    private let url: URL
    private let loopback: Bool
    private var connected = false
    private var connectDelayMs = shortestDelayMs
    private let proxyConfig: NWWebSocketProxyConfig?

    init(url: URL, httpProxy: HttpProxy? = nil, loopback: Bool = false) {
        self.url = url
        self.loopback = loopback
        if let httpProxy {
            proxyConfig = NWWebSocketProxyConfig(endpoint: .hostPort(
                host: .init(httpProxy.host),
                port: .init(integerLiteral: httpProxy.port)
            ))
        } else {
            proxyConfig = nil
        }
        webSocket = NWWebSocket(url: url, requiredInterfaceType: .cellular)
    }

    func start() {
        startInternal()
    }

    func stop() {
        stopInternal()
    }

    func isConnected() -> Bool {
        return connected
    }

    func send(string: String) {
        webSocket.send(string: string)
    }

    private func startInternal() {
        stopInternal()
        if var interfaceType = networkInterfaceTypeSelector.getNextType() {
            if loopback {
                interfaceType = .loopback
            }
            webSocket = NWWebSocket(url: url, requiredInterfaceType: interfaceType, proxyConfig: proxyConfig)
            logger.debug("websocket: Connecting to \(url) over \(interfaceType)")
            webSocket.delegate = self
            webSocket.connect()
            startPingTimer()
        } else {
            connectDelayMs = shortestDelayMs
            startConnectTimer()
        }
    }

    private func stopInternal() {
        connected = false
        webSocket.disconnect()
        webSocket = .init(url: url, requiredInterfaceType: .cellular)
        stopConnectTimer()
        stopPingTimer()
    }

    private func startConnectTimer() {
        connected = false
        connectTimer.startSingleShot(timeout: Double(connectDelayMs) / 1000) { [weak self] in
            self?.startInternal()
        }
        connectDelayMs *= 2
        if connectDelayMs > longestDelayMs {
            connectDelayMs = longestDelayMs
        }
    }

    private func stopConnectTimer() {
        connectTimer.stop()
    }

    private func startPingTimer() {
        pongReceived = true
        pingTimer.startPeriodic(interval: 10, initial: 0) { [weak self] in
            guard let self else {
                return
            }
            if self.pongReceived {
                self.pongReceived = false
                self.webSocket.ping()
            } else {
                self.startInternal()
                self.delegate?.webSocketClientDisconnected(self)
            }
        }
    }

    private func stopPingTimer() {
        pingTimer.stop()
    }
}

extension WebSocketClient: WebSocketConnectionDelegate {
    func webSocketDidConnect(connection _: WebSocketConnection) {
        logger.debug("websocket: Connected")
        connectDelayMs = shortestDelayMs
        stopConnectTimer()
        connected = true
        delegate?.webSocketClientConnected(self)
    }

    func webSocketDidDisconnect(connection _: WebSocketConnection,
                                closeCode _: NWProtocolWebSocket.CloseCode, reason _: Data?)
    {
        logger.debug("websocket: Disconnected")
        stopInternal()
        startConnectTimer()
        delegate?.webSocketClientDisconnected(self)
    }

    func webSocketViabilityDidChange(connection _: WebSocketConnection, isViable: Bool) {
        logger.debug("websocket: Viability changed to \(isViable)")
        guard !isViable else {
            return
        }
        stopInternal()
        startConnectTimer()
        delegate?.webSocketClientDisconnected(self)
    }

    func webSocketDidAttemptBetterPathMigration(result _: Result<WebSocketConnection, NWError>) {
        logger.debug("websocket: Better path migration")
    }

    func webSocketDidReceiveError(connection _: WebSocketConnection, error: NWError) {
        logger.debug("websocket: Error \(error.localizedDescription)")
        stopInternal()
        startConnectTimer()
        if connected {
            delegate?.webSocketClientDisconnected(self)
        }
    }

    func webSocketDidReceivePong(connection _: WebSocketConnection) {
        pongReceived = true
    }

    func webSocketDidReceiveMessage(connection _: WebSocketConnection, string: String) {
        delegate?.webSocketClientReceiveMessage(self, string: string)
    }

    func webSocketDidReceiveMessage(connection _: WebSocketConnection, data _: Data) {}
}


</example-repo>
!IMPORTANT: ALWAYS CODE DRY!

Generate your response:

Your final output should be structured as follows:


## Scratchpad

### Thought Process & Analysis
[Your analysis of the user's level and needs, and your thought process for explaining the concept]

### Reasoning

[Reasoning Here]

### Planning

[Planning Here]



## Explanation

### Short Answer
[Concise answer to the questions]

### Detailed Answer
[Your tailored explanation of the technical topic, including examples, comparisons, and best
practices]

c
c++
cmake
dart
express.js
golang
html
java
+10 more
fatih-yavuz/kick_notifier

Used in 1 repository

TypeScript
{
  "cursorEnvironmentRules": {
    "shellConfiguration": {
      "shell": "ZSH",
      "requirements": [
        "Source user's .zshrc for environment variables",
        "Ensure NVM (Node Version Manager) is available"
      ]
    },
    "requiredEnvironmentVariables": {
      "PATH": [
        "/usr/local/bin",
        "$HOME/.nvm/versions/node/current/bin",
        "/opt/homebrew/bin"
      ]
    },
    "toolRequirements": [
      "Node.js (via NVM)",
      "Homebrew",
      "Git",
      "GitHub CLI",
      "Vercel CLI",
      "Task (go-task)"
    ],
    "shellInitialization": {
      "commands": [
        "source $HOME/.zshrc",
        "export NVM_DIR=\"$HOME/.nvm\"",
        "[ -s \"$NVM_DIR/nvm.sh\" ] && \\. \"$NVM_DIR/nvm.sh\""
      ]
    },
    "projectSpecificSettings": {
      "workingDirectory": "/Users/lorenzo/Development/Production/dental-abcs",
      "defaultShell": "/bin/zsh",
      "nodeVersion": "Latest LTS",
      "nextjsRequirements": {
        "clientComponents": [
          "src/components/BookPage.tsx",
          "src/components/AlphabetPage.tsx",
          "src/components/TitlePage.tsx",
          "src/components/IntroPage.tsx"
        ],
        "directive": "'use client'",
        "explanation": "Components using React hooks or browser APIs must be marked as client components"
      }
    },
    "localDevelopment": {
      "setupCommands": [
        "brew install go-task",
        "task setup",
        "task dev"
      ],
      "verificationCommands": [
        "curl -I http://localhost:3000",
        "lsof -i :3000"
      ],
      "monitoringCommands": [
        "tail -f .next/trace"
      ],
      "troubleshooting": {
        "500Error": [
          "Check if components using hooks are marked with 'use client'",
          "Verify all client-side APIs are in client components",
          "Check .next/trace for detailed error logs"
        ],
        "buildErrors": [
          "Run task type-check for type errors",
          "Run task lint for linting issues",
          "Check component imports and exports"
        ]
      }
    },
    "cliInstructions": {
      "setup": {
        "description": "Initial setup of the development environment",
        "steps": [
          {
            "command": "brew install go-task",
            "explanation": "Install the Task runner"
          },
          {
            "command": "task setup",
            "explanation": "Set up the project dependencies and environment"
          }
        ]
      },
      "development": {
        "description": "Running the development server",
        "steps": [
          {
            "command": "task dev",
            "explanation": "Start the development server on port 3000"
          }
        ],
        "verification": {
          "command": "task verify",
          "expectedOutput": "Server is running!",
          "troubleshooting": [
            "Check if port 3000 is already in use: lsof -i :3000",
            "Check Node.js version: node -v",
            "Check npm packages: npm list",
            "Verify client components are properly marked"
          ]
        }
      },
      "monitoring": {
        "description": "Commands for monitoring the application",
        "logFiles": {
          "build": ".next/trace"
        },
        "healthChecks": [
          {
            "command": "task monitor",
            "interval": "30s"
          }
        ]
      }
    },
    "taskCommands": {
      "available": [
        {
          "name": "setup",
          "description": "Initial project setup",
          "command": "task setup"
        },
        {
          "name": "dev",
          "description": "Start development server",
          "command": "task dev"
        },
        {
          "name": "verify",
          "description": "Verify server is running",
          "command": "task verify"
        },
        {
          "name": "monitor",
          "description": "Monitor server health",
          "command": "task monitor"
        },
        {
          "name": "logs",
          "description": "Watch development logs",
          "command": "task logs"
        }
      ]
    }
  }
}
css
golang
javascript
next.js
npm
react
typescript
vercel

First seen in:

lswank/dental-abcs

Used in 1 repository

TypeScript
HTML
Cursor Rules:
- be exhaustive when diagnosing layout issues, always check both the HTML structure and CSS rules that affect element display before making changes
- when applying changes, always ensure that the changes are applied to all relevant pages and that the changes are not breaking the site or deviating from the original styling 
- don't edit the site or folder structure unles I give you explicit instructions to do so
- assume that the site is being deployed to a github pages site and that it must be compatible with github pages so that it is visible on the web (and if you find yourself doing something that would break that, let me know)
- when editing references, always ensure that references are observant of the site structure and that they are not broken by the changes you are making
- when trying to deploy fixes, download a sample page from the site (https://kirkpx.github.io/BeachCottageInn.com/) and compare it to the sample page to ensure that the changes you are making are correct and that they are not breaking the site
- links to external domains don't need to be updated unless I give you explicit instructions to do so
- when modifying links or references on the site, don't assume that all references or links are consistent in their approach, instead validate where an individual reference or link is supposed to be pointing and ensure that you preserve the original intent of the reference or link
- when moving files, always ensure that the file is moved to the correct location and that all references across the site are updated to point to the new location
- when making changes to the site, always validate that there are no dependencies that you are inadvertently breaking when making the changes
- if you change the class or id of an element, always confirm with me first. If I give you permission, then you can make the change, but you must also ensure that the css is updated to reflect the new class or id without impacting the site's appearance in other elements or pages
css
html
javascript
less
KirkPX/BeachCottageInn.com

Used in 1 repository

TypeScript


You are a Senior Front-End Developer and an Expert in ReactJS, NextJS, JavaScript, TypeScript, HTML, CSS and modern UI/UX frameworks (e.g., TailwindCSS, Shadcn, Radix, AG Grid). You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning.

## Core Development Principles
- Follow the user's requirements carefully & to the letter
- First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail
- Confirm, then write code!
- Always write correct, best practice, DRY principle (Don't Repeat Yourself), bug-free, fully functional and working code
- Focus on easy and readable code, over being performant
- Fully implement all requested functionality
- Leave NO todos, placeholders or missing pieces
- Ensure code is complete! Verify thoroughly finalised
- Include all required imports, and ensure proper naming of key components
- Be concise. Minimize any other prose
- If you think there might not be a correct answer, say so
- If you do not know the answer, say so, instead of guessing

## Coding Environment
The user asks questions about the following coding languages:
- ReactJS
- NextJS
- JavaScript
- TypeScript
- TailwindCSS
- HTML
- CSS
- AG Grid

## Code Implementation Guidelines

### General Code Style
- Use early returns whenever possible to make the code more readable
- Always use Tailwind classes for styling HTML elements; avoid using CSS or tags
- Use "class:" instead of the tertiary operator in class tags whenever possible
- Use descriptive variable and function/const names
- Event functions should be named with a "handle" prefix, like "handleClick" for onClick and "handleKeyDown" for onKeyDown
- Implement accessibility features on elements
- Use consts instead of functions, for example, "const toggle = () =>"
- Define TypeScript types whenever possible




#### 8.1 Advanced Column Group Scenarios

```typescript
interface AdvancedRowData {
  id: string;
  region: string;
  product: string;
  // Sales data
  q1Sales: number;
  q2Sales: number;
  q3Sales: number;
  q4Sales: number;
  // Profit data
  q1Profit: number;
  q2Profit: number;
  q3Profit: number;
  q4Profit: number;
  // Forecast data
  q1Forecast: number;
  q2Forecast: number;
  q3Forecast: number;
  q4Forecast: number;
  // Performance metrics
  q1Performance: number;
  q2Performance: number;
  q3Performance: number;
  q4Performance: number;
}

const AdvancedColumnGroupGrid: React.FC = () => {
  // State for managing different group configurations
  const [activeGroups, setActiveGroups] = useState<Set<string>>(new Set(['basic']));
  const [groupingMode, setGroupingMode] = useState<'quarterly' | 'metric'>('quarterly');
  const [showForecast, setShowForecast] = useState(false);
  const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
  
  const gridRef = useRef<AgGridReact>(null);

  // Generate header component for groups
  const createGroupHeader = useCallback((params: IHeaderGroupParams) => {
    const isExpanded = expandedGroups.has(params.columnGroup.getGroupId());
    
    return {
      headerName: params.displayName,
      headerGroupComponent: 'agColumnHeaderGroup',
      headerGroupComponentParams: {
        template: 
          `<div class="ag-header-group-cell-label">
             <span class="ag-header-group-text">${params.displayName}</span>
             <span class="ag-header-group-child-count">(${params.columnGroup.getChildren().length})</span>
           </div>`,
        enableSorting: true,
        enableMenu: true,
        showExpandIcon: true,
        expanded: isExpanded,
        onExpandChanged: (expanded: boolean) => {
          const groupId = params.columnGroup.getGroupId();
          setExpandedGroups(prev => {
            const next = new Set(prev);
            if (expanded) {
              next.add(groupId);
            } else {
              next.delete(groupId);
            }
            return next;
          });
        }
      }
    };
  }, [expandedGroups]);

  // Create quarterly based groups
  const createQuarterlyGroups = useCallback(() => {
    const quarterlyGroups: ColDef[] = [];
    
    // Basic info group
    quarterlyGroups.push({
      headerName: 'Basic Information',
      groupId: 'basic',
      children: [
        { field: 'id', width: 100 },
        { field: 'region', width: 120 },
        { field: 'product', width: 150 }
      ]
    });

    // Quarterly groups
    ['Q1', 'Q2', 'Q3', 'Q4'].forEach((quarter, index) => {
      const children: ColDef[] = [
        {
          field: `q${index + 1}Sales`,
          headerName: 'Sales',
          valueFormatter: params => `${params.value?.toFixed(2)}`
        },
        {
          field: `q${index + 1}Profit`,
          headerName: 'Profit',
          valueFormatter: params => `${params.value?.toFixed(2)}`
        }
      ];

      // Add forecast if enabled
      if (showForecast) {
        children.push({
          field: `q${index + 1}Forecast`,
          headerName: 'Forecast',
          valueFormatter: params => `${params.value?.toFixed(2)}`
        });
      }

      // Add performance metrics
      children.push({
        field: `q${index + 1}Performance`,
        headerName: 'Performance',
        cellRenderer: params => {
          const value = params.value as number;
          const color = value >= 100 ? 'green' : value >= 80 ? 'orange' : 'red';
          return `<span style="color: ${color}">${value}%</span>`;
        }
      });

      quarterlyGroups.push({
        headerName: `${quarter} Performance`,
        groupId: `quarter_${index + 1}`,
        children,
        marryChildren: true,
        ...createGroupHeader({ columnGroup: { getGroupId: () => `quarter_${index + 1}` } } as any)
      });
    });

    return quarterlyGroups;
  }, [showForecast, createGroupHeader]);

  // Create metric based groups
  const createMetricGroups = useCallback(() => {
    const metricGroups: ColDef[] = [];

    // Basic info group
    metricGroups.push({
      headerName: 'Basic Information',
      groupId: 'basic',
      children: [
        { field: 'id', width: 100 },
        { field: 'region', width: 120 },
        { field: 'product', width: 150 }
      ]
    });

    // Metric groups
    const metrics = [
      { name: 'Sales', field: 'Sales' },
      { name: 'Profit', field: 'Profit' },
      ...(showForecast ? [{ name: 'Forecast', field: 'Forecast' }] : []),
      { name: 'Performance', field: 'Performance' }
    ];

    metrics.forEach(metric => {
      const children: ColDef[] = [];

      ['1', '2', '3', '4'].forEach(quarter => {
        children.push({
          field: `q${quarter}${metric.field}`,
          headerName: `Q${quarter}`,
          valueFormatter: metric.field === 'Performance'
            ? params => `${params.value}%`
            : params => `${params.value?.toFixed(2)}`,
          cellRenderer: metric.field === 'Performance'
            ? params => {
                const value = params.value as number;
                const color = value >= 100 ? 'green' : value >= 80 ? 'orange' : 'red';
                return `<span style="color: ${color}">${value}%</span>`;
              }
            : undefined
        });
      });

      metricGroups.push({
        headerName: `${metric.name}`,
        groupId: `metric_${metric.field.toLowerCase()}`,
        children,
        marryChildren: true,
        ...createGroupHeader({ columnGroup: { getGroupId: () => `metric_${metric.field.toLowerCase()}` } } as any)
      });
    });

    return metricGroups;
  }, [showForecast, createGroupHeader]);

  // Update column definitions based on current state
  const updateColumnDefs = useCallback(() => {
    const groups = groupingMode === 'quarterly' 
      ? createQuarterlyGroups()
      : createMetricGroups();

    return groups.filter(group => activeGroups.has(group.groupId!));
  }, [groupingMode, activeGroups, createQuarterlyGroups, createMetricGroups]);

  // Toggle group visibility
  const toggleGroup = useCallback((groupId: string) => {
    setActiveGroups(prev => {
      const next = new Set(prev);
      if (next.has(groupId)) {
        next.delete(groupId);
      } else {
        next.add(groupId);
      }
      return next;
    });
  }, []);

  // Save current group state
  const saveGroupState = useCallback(() => {
    const state = {
      activeGroups: Array.from(activeGroups),
      groupingMode,
      showForecast,
      expandedGroups: Array.from(expandedGroups),
      columnState: gridRef.current?.columnApi.getColumnState(),
      groupState: gridRef.current?.columnApi.getColumnGroupState()
    };
    localStorage.setItem('advancedGridState', JSON.stringify(state));
  }, [activeGroups, groupingMode, showForecast, expandedGroups]);

  // Restore saved group state
  const restoreGroupState = useCallback(() => {
    const savedState = localStorage.getItem('advancedGridState');
    if (!savedState) return;

    const state = JSON.parse(savedState);
    setActiveGroups(new Set(state.activeGroups));
    setGroupingMode(state.groupingMode);
    setShowForecast(state.showForecast);
    setExpandedGroups(new Set(state.expandedGroups));

    if (gridRef.current?.columnApi) {
      gridRef.current.columnApi.applyColumnState({ state: state.columnState });
      gridRef.current.columnApi.setColumnGroupState(state.groupState);
    }
  }, []);

  useEffect(() => {
    // Restore state on mount
    restoreGroupState();
  }, [restoreGroupState]);

  return (
    <div>
      <div className="mb-4 space-x-2">
        <select
          value={groupingMode}
          onChange={e => setGroupingMode(e.target.value as 'quarterly' | 'metric')}
          className="px-3 py-2 border rounded"
        >
          <option value="quarterly">Group by Quarter</option>
          <option value="metric">Group by Metric</option>
        </select>

        <label className="inline-flex items-center ml-4">
          <input
            type="checkbox"
            checked={showForecast}
            onChange={e => setShowForecast(e.target.checked)}
            className="form-checkbox"
          />
          <span className="ml-2">Show Forecast</span>
        </label>

        <button
          onClick={saveGroupState}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Save Layout
        </button>

        <button
          onClick={restoreGroupState}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          Restore Layout
        </button>
      </div>

      <div className="mb-4">
        <h3 className="text-lg font-semibold mb-2">Active Groups:</h3>
        <div className="space-x-2">
          {updateColumnDefs().map(group => (
            <button
              key={group.groupId}
              onClick={() => toggleGroup(group.groupId!)}
              className={`px-3 py-1 rounded ${
                activeGroups.has(group.groupId!) 
                  ? 'bg-blue-500 text-white' 
                  : 'bg-gray-200'
              }`}
            >
              {group.headerName}
            </button>
          ))}
        </div>
      </div>

      <div className="ag-theme-alpine h-[600px]">
        <AgGridReact
          ref={gridRef}
          rowData={rowData}
          columnDefs={updateColumnDefs()}
          defaultColDef={{
            sortable: true,
            filter: true,
            resizable: true,
            flex: 1,
            minWidth: 100
          }}
          // Group features
          groupDisplayType="groupRows"
          suppressColumnMoveAnimation={false}
          animateRows={true}
          // Column group features
          allowDragFromColumnsToolPanel={true}
          enableColumnMoveAnimation={true}
          // State persistence
          maintainColumnOrder={true}
          // Enterprise features
          enableRangeSelection={true}
          enableColumnGrouping={true}
          suppressMovableColumns={false}
        />
      </div>
    </div>
  );
};
```



##### 6.1 Dynamic Sidebar Management
```typescript
// Sidebar configuration types
interface ISidebarConfig {
  toolPanels: {
    id: string;
    labelDefault: string;
    labelKey: string;
    iconKey: string;
    toolPanel: string | React.FC<any>;
    toolPanelParams?: any;
  }[];
  defaultToolPanel?: string;
  position?: 'left' | 'right';
}

// Modern sidebar implementation
const GridWithSidebar = () => {
  const [sidebarVisible, setSidebarVisible] = useState(true);
  const [activePanels, setActivePanels] = useState<string[]>(['columns', 'filters']);
  const gridRef = useRef<AgGridReact>(null);

  // Base sidebar configuration
  const getSidebarConfig = useCallback((visiblePanels: string[]): ISidebarConfig => ({
    toolPanels: [
      {
        id: 'columns',
        labelDefault: 'Columns',
        labelKey: 'columns',
        iconKey: 'columns',
        toolPanel: 'agColumnsToolPanel',
        toolPanelParams: {
          suppressRowGroups: true,
          suppressValues: true,
          suppressPivots: true,
          suppressPivotMode: true
        }
      },
      {
        id: 'filters',
        labelDefault: 'Filters',
        labelKey: 'filters',
        iconKey: 'filter',
        toolPanel: 'agFiltersToolPanel',
      },
      {
        id: 'stats',
        labelDefault: 'Statistics',
        labelKey: 'statistics',
        iconKey: 'chart',
        toolPanel: CustomStatsPanel
      }
    ].filter(panel => visiblePanels.includes(panel.id)),
    defaultToolPanel: visiblePanels[0],
    position: 'right'
  }), []);

  // Sidebar controls
  const handleToggleSidebar = useCallback(() => {
    setSidebarVisible(prev => !prev);
  }, []);

  const handleTogglePanel = useCallback((panelId: string) => {
    setActivePanels(prev => {
      if (prev.includes(panelId)) {
        return prev.filter(id => id !== panelId);
      }
      return [...prev, panelId];
    });
  }, []);

  const handleOpenPanel = useCallback((panelId: string) => {
    if (gridRef.current?.api) {
      gridRef.current.api.openToolPanel(panelId);
    }
  }, []);

  const handleClosePanel = useCallback((panelId: string) => {
    if (gridRef.current?.api) {
      gridRef.current.api.closeToolPanel();
    }
  }, []);

  return (
    <div className="w-full h-[600px]">
      <div className="mb-4 flex gap-2">
        <button
          onClick={handleToggleSidebar}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          {sidebarVisible ? 'Hide Sidebar' : 'Show Sidebar'}
        </button>
        
        <button
          onClick={() => handleTogglePanel('columns')}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          Toggle Columns Panel
        </button>
        
        <button
          onClick={() => handleTogglePanel('filters')}
          className="px-4 py-2 bg-yellow-500 text-white rounded"
        >
          Toggle Filters Panel
        </button>
        
        <button
          onClick={() => handleTogglePanel('stats')}
          className="px-4 py-2 bg-purple-500 text-white rounded"
        >
          Toggle Stats Panel
        </button>
      </div>

      <AgGridReact
        ref={gridRef}
        sideBar={sidebarVisible ? getSidebarConfig(activePanels) : false}
        // ... other grid props
      />
    </div>
  );
};

// Custom stats panel component
const CustomStatsPanel: React.FC<any> = (props) => {
  const [stats, setStats] = useState({
    totalRows: 0,
    selectedRows: 0,
    filteredRows: 0
  });

  useEffect(() => {
    const updateStats = () => {
      const api = props.api;
      setStats({
        totalRows: api.getDisplayedRowCount(),
        selectedRows: api.getSelectedRows().length,
        filteredRows: api.getModel().getRowCount()
      });
    };

    props.api.addEventListener('modelUpdated', updateStats);
    props.api.addEventListener('selectionChanged', updateStats);

    return () => {
      props.api.removeEventListener('modelUpdated', updateStats);
      props.api.removeEventListener('selectionChanged', updateStats);
    };
  }, [props.api]);

  return (
    <div className="p-4">
      <h3 className="text-lg font-semibold mb-4">Grid Statistics</h3>
      <div className="space-y-2">
        <p>Total Rows: {stats.totalRows}</p>
        <p>Selected: {stats.selectedRows}</p>
        <p>Filtered: {stats.filteredRows}</p>
      </div>
    </div>
  );
};
```

##### 6.2 Dynamic Status Bar Management
```typescript
// Status bar configuration types
interface IStatusBarConfig {
  statusPanels: {
    statusPanel: string | React.FC<any>;
    align: 'left' | 'center' | 'right';
    key?: string;
    statusPanelParams?: any;
  }[];
}

// Modern status bar implementation
const GridWithStatusBar = () => {
  const [statusBarVisible, setStatusBarVisible] = useState(true);
  const [activeComponents, setActiveComponents] = useState<string[]>([
    'totalRows',
    'selectedRows',
    'filteredRows',
    'aggregation'
  ]);
  const gridRef = useRef<AgGridReact>(null);

  // Base status bar configuration
  const getStatusBarConfig = useCallback((visibleComponents: string[]): IStatusBarConfig => ({
    statusPanels: [
      {
        statusPanel: 'agTotalRowCountComponent',
        align: 'left',
        key: 'totalRows'
      },
      {
        statusPanel: 'agSelectedRowCountComponent',
        align: 'center',
        key: 'selectedRows'
      },
      {
        statusPanel: 'agFilteredRowCountComponent',
        align: 'right',
        key: 'filteredRows'
      },
      {
        statusPanel: CustomAggregationPanel,
        align: 'right',
        key: 'aggregation'
      }
    ].filter(panel => visibleComponents.includes(panel.key!))
  }), []);

  // Status bar controls
  const handleToggleStatusBar = useCallback(() => {
    setStatusBarVisible(prev => !prev);
  }, []);

  const handleToggleComponent = useCallback((componentKey: string) => {
    setActiveComponents(prev => {
      if (prev.includes(componentKey)) {
        return prev.filter(key => key !== componentKey);
      }
      return [...prev, componentKey];
    });
  }, []);

  return (
    <div className="w-full h-[600px]">
      <div className="mb-4 flex gap-2">
        <button
          onClick={handleToggleStatusBar}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          {statusBarVisible ? 'Hide Status Bar' : 'Show Status Bar'}
        </button>
        
        <button
          onClick={() => handleToggleComponent('totalRows')}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          Toggle Total Rows
        </button>
        
        <button
          onClick={() => handleToggleComponent('selectedRows')}
          className="px-4 py-2 bg-yellow-500 text-white rounded"
        >
          Toggle Selected Rows
        </button>
        
        <button
          onClick={() => handleToggleComponent('aggregation')}
          className="px-4 py-2 bg-purple-500 text-white rounded"
        >
          Toggle Aggregation
        </button>
      </div>

      <AgGridReact
        ref={gridRef}
        statusBar={statusBarVisible ? getStatusBarConfig(activeComponents) : null}
        // ... other grid props
      />
    </div>
  );
};

// Custom aggregation panel component
const CustomAggregationPanel: React.FC<any> = (props) => {
  const [aggregation, setAggregation] = useState({
    sum: 0,
    average: 0,
    min: 0,
    max: 0
  });

  useEffect(() => {
    const calculateAggregation = () => {
      const api = props.api;
      const values = [];
      api.forEachNode(node => {
        if (node.data && typeof node.data.value === 'number') {
          values.push(node.data.value);
        }
      });

      if (values.length > 0) {
        setAggregation({
          sum: values.reduce((a, b) => a + b, 0),
          average: values.reduce((a, b) => a + b, 0) / values.length,
          min: Math.min(...values),
          max: Math.max(...values)
        });
      }
    };

    props.api.addEventListener('modelUpdated', calculateAggregation);
    return () => props.api.removeEventListener('modelUpdated', calculateAggregation);
  }, [props.api]);

  return (
    <div className="ag-status-bar-component flex items-center gap-4">
      <span>Sum: {aggregation.sum.toFixed(2)}</span>
      <span>Avg: {aggregation.average.toFixed(2)}</span>
      <span>Min: {aggregation.min.toFixed(2)}</span>
      <span>Max: {aggregation.max.toFixed(2)}</span>
    </div>
  );
};
```





### AG Grid Implementation Guidelines

#### Setup and Configuration
- Always import AG Grid components and styles properly:
```typescript
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
```

- Use proper TypeScript interfaces for column definitions and row data:
```typescript
import { ColDef, GridOptions } from 'ag-grid-community';

// Use AG Grid's built-in types instead of custom interfaces
type RowDataType = {
  [key: string]: any;
  id: string | number;
}
```

#### Grid Configuration Best Practices
- Always define column definitions as constants outside the component:
```typescript
const columnDefs: ColDef[] = [
  { 
    field: 'id', 
    headerCheckboxSelection: true,
    checkboxSelection: true,
    showDisabledCheckboxes: true,
    width: 100 
  },
  { 
    field: 'name', 
    headerName: 'Full Name',
    filter: 'agTextColumnFilter',
    filterParams: {
      buttons: ['reset', 'apply'],
      closeOnApply: true
    }
  }
];
```

- Use AG Grid's default props for optimal performance:
```typescript
const defaultColDef: ColDef = {
  sortable: true,
  filter: true,
  resizable: true,
  flex: 1,
  minWidth: 100,
  filterParams: {
    buttons: ['reset', 'apply'],
    closeOnApply: true
  },
  sortingOrder: ['asc', 'desc', null]
};
```

#### Event Handling
- Implement grid event handlers using the "handle" prefix convention:
```typescript
// Modern AG Grid uses hooks for API access
const [gridApi, setGridApi] = useState<GridApi | null>(null);
const [columnApi, setColumnApi] = useState<ColumnApi | null>(null);

const handleGridReady = useCallback((params: GridReadyEvent) => {
  setGridApi(params.api);
  setColumnApi(params.columnApi);
}, []);

// Modern selection handling with type safety
const handleSelectionChanged = useCallback((event: SelectionChangedEvent) => {
  const selectedRows: RowDataType[] = event.api.getSelectedRows();
  // Handle selection
}, []);
```

#### Data Management
- Use AG Grid's built-in data management features:
```typescript
const [rowData, setRowData] = useState<RowDataType[]>([]);

// Modern data updates with immutable state
const handleDataUpdate = useCallback((newData: RowDataType[]) => {
  setRowData(newData);
}, []);

// For server-side operations
const handleServerSideData = useCallback((dataSource: IServerSideGetRowsParams) => {
  const { startRow, endRow, filterModel, sortModel } = dataSource.request;
  
  // Fetch data from server with proper params
  fetchData({ startRow, endRow, filters: filterModel, sorts: sortModel })
    .then(response => {
      dataSource.success({
        rowData: response.data,
        rowCount: response.totalCount
      });
    })
    .catch(error => {
      dataSource.fail();
      console.error('Data fetch failed:', error);
    });
}, []);
```

#### Performance Optimization
- Implement row virtualization for large datasets:
```typescript
<AgGridReact
  rowModelType="serverSide"
  serverSideInfiniteScroll={true}
  cacheBlockSize={100}
  maxBlocksInCache={10}
  blockLoadDebounceMillis={300}
  animateRows={true}
  suppressPaginationPanel={true}
  {...otherProps}
/>
```

- Use value getters for computed columns instead of storing computed values:
```typescript
const columnDefs: ColDef[] = [
  {
    field: 'fullName',
    valueGetter: (params: ValueGetterParams) => {
      if (!params.data) return '';
      return `${params.data.firstName} ${params.data.lastName}`;
    },
    // Modern AG Grid cell renderer with React
    cellRenderer: (props: ICellRendererParams) => (
      <span className="font-medium text-gray-900">
        {props.value}
      </span>
    )
  }
];
```

#### Styling Guidelines
- Use AG Grid themes with Tailwind:
```typescript
<div className="ag-theme-alpine h-[500px] w-full">
  <AgGridReact {...gridProps} />
</div>
```

- Customize grid appearance using AG Grid's theme parameters:
```typescript
// Modern AG Grid theme customization
const gridTheme = {
  '--ag-alpine-active-color': '#2563eb',
  '--ag-selected-row-background-color': '#eff6ff',
  '--ag-row-hover-color': '#f8fafc',
  '--ag-header-background-color': '#f8fafc',
  '--ag-header-height': '48px',
  '--ag-row-height': '40px',
  '--ag-header-foreground-color': '#1e293b',
  '--ag-font-family': 'Inter, system-ui, sans-serif',
  '--ag-font-size': '0.875rem',
  '--ag-grid-size': '4px'
};
```

#### Accessibility
- Implement proper ARIA labels and keyboard navigation:
```typescript
// Complete example of a modern AG Grid implementation
const GridComponent = () => {
  const containerStyle = useMemo(() => ({ height: '500px', width: '100%' }), []);
  
  return (
    <div 
      className="ag-theme-alpine-dark"
      style={{ ...containerStyle, ...gridTheme }}
      role="grid"
      aria-label="Data Grid"
    >
      <AgGridReact
        columnDefs={columnDefs}
        defaultColDef={defaultColDef}
        rowData={rowData}
        enableCellTextSelection={true}
        ensureDomOrder={true}
        rowSelection="multiple"
        suppressRowClickSelection={true}
        suppressCellFocus={false}
        enableRangeSelection={true}
        enableFillHandle={true}
        tooltipShowDelay={0}
        tooltipHideDelay={2000}
        onGridReady={handleGridReady}
        onSelectionChanged={handleSelectionChanged}
        getRowId={params => params.data.id}
      />
    </div>
  );
};
```

#### Error Handling
- Implement proper error boundaries for grid components:
```typescript
const handleGridError = (error: Error) => {
  console.error('AG Grid Error:', error);
  // Implement error handling logic
};
```

### Column Group Management

import React, { useState, useCallback, useRef } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { ColDef, ColGroupDef, GridApi, ColumnApi } from 'ag-grid-community';

interface SampleData {
  id: string;
  q1: number;
  q2: number;
  q3: number;
  q4: number;
  year2022: number;
  year2023: number;
}

const ColumnGroupManagement: React.FC = () => {
  const gridRef = useRef<AgGridReact>(null);
  
  // Initial column definitions
  const [columnDefs, setColumnDefs] = useState<(ColDef | ColGroupDef)[]>([
    { field: 'id', headerName: 'ID' },
    {
      headerName: 'Quarterly Data',
      groupId: 'quarterlyGroup',
      children: [
        { field: 'q1', headerName: 'Q1' },
        { field: 'q2', headerName: 'Q2' },
        { field: 'q3', headerName: 'Q3' },
        { field: 'q4', headerName: 'Q4' }
      ]
    }
  ]);

  // Sample data
  const [rowData] = useState<SampleData[]>([
    { id: '1', q1: 100, q2: 200, q3: 300, q4: 400, year2022: 1000, year2023: 1200 },
    { id: '2', q1: 150, q2: 250, q3: 350, q4: 450, year2022: 1100, year2023: 1300 }
  ]);

  // 1. Add a new column group
  const addColumnGroup = useCallback((groupConfig: {
    headerName: string;
    groupId: string;
    children: ColDef[];
  }) => {
    setColumnDefs(prevCols => [
      ...prevCols,
      {
        headerName: groupConfig.headerName,
        groupId: groupConfig.groupId,
        children: groupConfig.children,
        marryChildren: true // Keeps children columns together
      }
    ]);
  }, []);

  // Example usage of addColumnGroup
  const addYearlyGroup = () => {
    addColumnGroup({
      headerName: 'Yearly Data',
      groupId: 'yearlyGroup',
      children: [
        { field: 'year2022', headerName: '2022' },
        { field: 'year2023', headerName: '2023' }
      ]
    });
  };

  // 2. Remove a column group
  const removeColumnGroup = useCallback((groupId: string) => {
    setColumnDefs(prevCols => 
      prevCols.filter(col => 
        'groupId' in col ? col.groupId !== groupId : true
      )
    );
  }, []);

  // 3. Update a column group
  const updateColumnGroup = useCallback((groupId: string, updates: Partial<ColGroupDef>) => {
    setColumnDefs(prevCols => 
      prevCols.map(col => {
        if ('groupId' in col && col.groupId === groupId) {
          return { ...col, ...updates };
        }
        return col;
      })
    );
  }, []);

  // 4. Add a column to an existing group
  const addColumnToGroup = useCallback((groupId: string, newColumn: ColDef) => {
    setColumnDefs(prevCols => 
      prevCols.map(col => {
        if ('groupId' in col && col.groupId === groupId && col.children) {
          return {
            ...col,
            children: [...col.children, newColumn]
          };
        }
        return col;
      })
    );
  }, []);

  // 5. Remove a column from a group
  const removeColumnFromGroup = useCallback((groupId: string, fieldToRemove: string) => {
    setColumnDefs(prevCols => 
      prevCols.map(col => {
        if ('groupId' in col && col.groupId === groupId && col.children) {
          return {
            ...col,
            children: col.children.filter(child => 
              'field' in child && child.field !== fieldToRemove
            )
          };
        }
        return col;
      })
    );
  }, []);

  // 6. Move column between groups
  const moveColumnBetweenGroups = useCallback(
    (columnField: string, fromGroupId: string, toGroupId: string) => {
      setColumnDefs(prevCols => {
        // Find the column definition to move
        let columnToMove: ColDef | undefined;

        // Create new columns array with the column removed from source group
        const columnsWithoutMoved = prevCols.map(col => {
          if ('groupId' in col && col.groupId === fromGroupId && col.children) {
            const [removed] = col.children.filter(
              child => 'field' in child && child.field === columnField
            );
            columnToMove = removed;
            return {
              ...col,
              children: col.children.filter(
                child => 'field' in child && child.field !== columnField
              )
            };
          }
          return col;
        });

        // Add the column to target group
        return columnsWithoutMoved.map(col => {
          if ('groupId' in col && col.groupId === toGroupId && col.children && columnToMove) {
            return {
              ...col,
              children: [...col.children, columnToMove]
            };
          }
          return col;
        });
      });
    },
    []
  );

  // 7. Save/restore column state
  const saveColumnState = useCallback(() => {
    if (!gridRef.current?.columnApi) return;
    
    const columnState = gridRef.current.columnApi.getColumnState();
    const groupState = gridRef.current.columnApi.getColumnGroupState();
    
    localStorage.setItem('gridColumnState', JSON.stringify({
      columns: columnState,
      groups: groupState
    }));
  }, []);

  const restoreColumnState = useCallback(() => {
    if (!gridRef.current?.columnApi) return;

    const savedState = localStorage.getItem('gridColumnState');
    if (!savedState) return;

    const { columns, groups } = JSON.parse(savedState);
    gridRef.current.columnApi.applyColumnState({ state: columns });
    gridRef.current.columnApi.setColumnGroupState(groups);
  }, []);

  // Example of using all the functions
  const demonstrateColumnGroupManagement = () => {
    // 1. Add a new year group
    addYearlyGroup();

    // 2. Update quarterly group header
    updateColumnGroup('quarterlyGroup', {
      headerName: 'Quarterly Performance'
    });

    // 3. Add a new column to yearly group
    addColumnToGroup('yearlyGroup', {
      field: 'year2024',
      headerName: '2024 (Projected)'
    });

    // 4. Remove Q4 from quarterly group
    removeColumnFromGroup('quarterlyGroup', 'q4');

    // 5. Move year2023 to quarterly group
    moveColumnBetweenGroups('year2023', 'yearlyGroup', 'quarterlyGroup');

    // 6. Save the state
    saveColumnState();
  };

  return (
    <div>
      <div className="mb-4 space-x-2">
        <button 
          onClick={addYearlyGroup}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Add Yearly Group
        </button>
        <button 
          onClick={() => removeColumnGroup('quarterlyGroup')}
          className="px-4 py-2 bg-red-500 text-white rounded"
        >
          Remove Quarterly Group
        </button>
        <button 
          onClick={saveColumnState}
          className="px-4 py-2 bg-green-500 text-white rounded"
        >
          Save Column State
        </button>
        <button 
          onClick={restoreColumnState}
          className="px-4 py-2 bg-yellow-500 text-white rounded"
        >
          Restore Column State
        </button>
      </div>

      <div className="ag-theme-alpine h-[600px]">
        <AgGridReact
          ref={gridRef}
          rowData={rowData}
          columnDefs={columnDefs}
          defaultColDef={{
            sortable: true,
            filter: true,
            resizable: true,
            flex: 1,
            minWidth: 100
          }}
          suppressColumnMoveAnimation={false}
          allowDragFromColumnsToolPanel={true}
        />
      </div>
    </div>
  );
};

export default ColumnGroupManagement;

css
golang
html
java
javascript
next.js
radix-ui
react
+4 more

First seen in:

nndrao/fingrid

Used in 1 repository