"use client"; import type { FeedbackComment, FeedbackEntry as FeedbackEntryData, } from "convex-feedback"; import { createContext, useContext, type ButtonHTMLAttributes, type FormHTMLAttributes, type HTMLAttributes, type InputHTMLAttributes, type SelectHTMLAttributes, type TextareaHTMLAttributes, } from "react"; import { useFeedbackUi } from "../shared/context/FeedbackProvider"; import type { BoardRootBaseProps, BoardSearchBaseProps, BoardSearchState, CommentActionBaseProps, CommentActionState, CommentLikeBaseProps, CommentLikeState, CommentRootBaseProps, ChoiceChipsBaseProps, EntryRootBaseProps, EntryUpvoteBaseProps, EntryUpvoteState, FormSubmitBaseProps, FormSubmitState, RepliesButtonBaseProps, RepliesButtonState, } from "../shared/types"; import { feedbackCssVariables } from "./css.js"; function classes( unstyled: boolean, base: string, className?: string, ): string | undefined { if (unstyled) return className; return className === undefined ? base : `${base} ${className}`; } /** * Props for `FeedbackBoard.Root`. */ export interface BoardRootProps extends HTMLAttributes, BoardRootBaseProps {} function BoardRoot({ primaryColor, backgroundColor, surfaceColor, inputColor, textColor, mutedColor, borderColor, dangerColor, unstyled: localUnstyled, className, style, ...props }: BoardRootProps) { const ui = useFeedbackUi(); const unstyled = localUnstyled ?? ui.unstyled; const variables = feedbackCssVariables(ui.theme, { ...(primaryColor === undefined ? {} : { primaryColor }), ...(backgroundColor === undefined ? {} : { backgroundColor }), ...(surfaceColor === undefined ? {} : { surfaceColor }), ...(inputColor === undefined ? {} : { inputColor }), ...(textColor === undefined ? {} : { textColor }), ...(mutedColor === undefined ? {} : { mutedColor }), ...(borderColor === undefined ? {} : { borderColor }), ...(dangerColor === undefined ? {} : { dangerColor }), }); return (
); } function BoardHeader(props: HTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } function BoardTitle(props: HTMLAttributes) { const { messages, unstyled } = useFeedbackUi(); return (

{props.children ?? messages.board.title}

); } /** * Props for `FeedbackBoard.Search`. */ export interface BoardSearchProps extends Omit< InputHTMLAttributes, "children" | "value" | "onChange" >, BoardSearchBaseProps {} function BoardSearch({ value, onValueChange, children, className, ...props }: BoardSearchProps) { const { messages, unstyled } = useFeedbackUi(); const state: BoardSearchState = { value, setValue: onValueChange }; if (typeof children === "function") return <>{children(state)}; return ( onValueChange(event.currentTarget.value)} placeholder={props.placeholder ?? messages.board.searchPlaceholder} /> ); } function BoardList(props: HTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } function BoardState(props: HTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } export const FeedbackBoard = { Root: BoardRoot, Header: BoardHeader, Title: BoardTitle, Search: BoardSearch, List: BoardList, State: BoardState, }; /** * Props for the reusable web `ChoiceChips` primitive. */ export interface ChoiceChipsProps extends Omit, "children">, ChoiceChipsBaseProps {} export function ChoiceChips({ options, value, onValueChange, className, role, ...props }: ChoiceChipsProps) { const { unstyled } = useFeedbackUi(); return (
{options.map((option) => { const selected = option.value === value; return ( ); })}
); } const EntryContext = createContext(null); function useEntryContext(): FeedbackEntryData { const entry = useContext(EntryContext); if (entry === null) throw new Error( "FeedbackEntry compound must be inside FeedbackEntry.Root.", ); return entry; } /** * Props for `FeedbackEntry.Root`. */ export interface EntryRootProps extends HTMLAttributes, EntryRootBaseProps {} function EntryRoot({ entry, className, ...props }: EntryRootProps) { const { unstyled } = useFeedbackUi(); return (
); } /** * Props for `FeedbackEntry.Upvote`. */ export interface EntryUpvoteProps extends Omit< ButtonHTMLAttributes, "children" | "onClick" | "onToggle" >, EntryUpvoteBaseProps {} function EntryUpvote({ onToggle, children, "aria-label": accessibilityLabel, className, ...props }: EntryUpvoteProps) { const entry = useEntryContext(); const { messages, unstyled } = useFeedbackUi(); const toggle = () => onToggle(!entry.viewerHasUpvoted); const state: EntryUpvoteState = { entry, active: entry.viewerHasUpvoted, count: entry.upvoteCount, toggle, }; if (typeof children === "function") return <>{children(state)}; return ( ); } function EntryContent(props: HTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } function EntryKind(props: HTMLAttributes) { const entry = useEntryContext(); const { messages, unstyled } = useFeedbackUi(); return ( {props.children ?? messages.kinds[entry.kind]} ); } function EntryTitle(props: HTMLAttributes) { const entry = useEntryContext(); const { unstyled } = useFeedbackUi(); return (

{props.children ?? entry.title}

); } function EntryBody(props: HTMLAttributes) { const entry = useEntryContext(); const { unstyled } = useFeedbackUi(); return (

{props.children ?? entry.body}

); } function EntryStatus(props: HTMLAttributes) { const entry = useEntryContext(); const { messages, unstyled } = useFeedbackUi(); return ( {props.children ?? messages.statuses[entry.status]} ); } function EntryCommentCount(props: HTMLAttributes) { const entry = useEntryContext(); const { messages, unstyled } = useFeedbackUi(); return ( {props.children ?? messages.entry.comments(entry.commentCount)} ); } export const FeedbackEntry = { Root: EntryRoot, Upvote: EntryUpvote, Content: EntryContent, Kind: EntryKind, Title: EntryTitle, Body: EntryBody, Status: EntryStatus, CommentCount: EntryCommentCount, }; const CommentContext = createContext(null); function useCommentContext(): FeedbackComment { const comment = useContext(CommentContext); if (comment === null) throw new Error("Comment compound must be inside Comment.Root."); return comment; } /** * Props for `Comment.Root`. */ export interface CommentRootProps extends HTMLAttributes, CommentRootBaseProps {} function CommentRoot({ comment, className, ...props }: CommentRootProps) { const { unstyled } = useFeedbackUi(); return (
); } function CommentBody(props: HTMLAttributes) { const comment = useCommentContext(); const { unstyled } = useFeedbackUi(); return (

{props.children ?? comment.body}

); } /** * Props for `Comment.Like`. */ export interface CommentLikeProps extends Omit< ButtonHTMLAttributes, "children" | "onClick" | "onToggle" >, CommentLikeBaseProps {} function CommentLike({ onToggle, children, "aria-label": accessibilityLabel, className, ...props }: CommentLikeProps) { const comment = useCommentContext(); const { messages, unstyled } = useFeedbackUi(); const toggle = () => onToggle(!comment.viewerHasLiked); const state: CommentLikeState = { comment, active: comment.viewerHasLiked, count: comment.likeCount, toggle, }; if (typeof children === "function") return <>{children(state)}; return ( ); } /** * Props for a simple comment action. */ export interface CommentActionProps extends Omit< ButtonHTMLAttributes, "children" | "onClick" | "onToggle" >, CommentActionBaseProps {} function CommentReply({ onActivate, children, className, ...props }: CommentActionProps) { const comment = useCommentContext(); const { messages, unstyled } = useFeedbackUi(); const state: CommentActionState = { comment, activate: onActivate }; return typeof children === "function" ? ( <>{children(state)} ) : ( ); } /** * Props for `Comment.RepliesButton`. */ export interface RepliesButtonProps extends Omit< ButtonHTMLAttributes, "children" | "onClick" | "onToggle" >, RepliesButtonBaseProps {} function CommentRepliesButton({ expanded, onExpandedChange, children, className, ...props }: RepliesButtonProps) { const comment = useCommentContext(); const { messages, unstyled } = useFeedbackUi(); const activate = () => onExpandedChange(!expanded); const state: RepliesButtonState = { comment, expanded, count: comment.replyCount, activate, }; if (typeof children === "function") return <>{children(state)}; if (comment.replyCount === 0) return null; return ( ); } function CommentChildren(props: HTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } export const Comment = { Root: CommentRoot, Body: CommentBody, Like: CommentLike, Reply: CommentReply, RepliesButton: CommentRepliesButton, Children: CommentChildren, }; function FormRoot(props: FormHTMLAttributes) { const { unstyled } = useFeedbackUi(); return (
); } function FormInput(props: InputHTMLAttributes) { const { unstyled } = useFeedbackUi(); return ( ); } function FormTextarea(props: TextareaHTMLAttributes) { const { unstyled } = useFeedbackUi(); return (