From 1f3435b12ef0a9334d9d14d5b00215441e3d4347 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 11:50:09 -0800 Subject: [PATCH 01/11] wip: Introduce FormInput and Badge components; refactor existing components to use them --- src/components/DocsCalloutQueryGG.tsx | 4 +- src/components/FeedbackModerationList.tsx | 21 +- src/components/LogoQueryGG.tsx | 221 ------------ src/components/LogoQueryGGSmall.tsx | 228 ------------- src/components/NotesModerationList.tsx | 25 +- src/components/SimpleMarkdown.tsx | 30 +- src/components/admin/BannerEditor.tsx | 31 +- src/components/admin/CapabilityBadge.tsx | 37 +- src/components/admin/FeedEntryEditor.tsx | 14 +- src/components/admin/StatusBadge.tsx | 43 +-- src/components/markdown/Markdown.tsx | 50 +-- src/routes/learn.tsx | 2 +- src/ui/Badge.tsx | 50 +++ src/ui/FormInput.tsx | 29 ++ src/ui/InlineCode.tsx | 16 + src/ui/LogoQueryGG.tsx | 389 ++++++++++++++++++++++ src/ui/MarkdownImg.tsx | 22 ++ src/ui/index.ts | 5 + 18 files changed, 591 insertions(+), 626 deletions(-) delete mode 100644 src/components/LogoQueryGG.tsx delete mode 100644 src/components/LogoQueryGGSmall.tsx create mode 100644 src/ui/Badge.tsx create mode 100644 src/ui/FormInput.tsx create mode 100644 src/ui/InlineCode.tsx create mode 100644 src/ui/LogoQueryGG.tsx create mode 100644 src/ui/MarkdownImg.tsx create mode 100644 src/ui/index.ts diff --git a/src/components/DocsCalloutQueryGG.tsx b/src/components/DocsCalloutQueryGG.tsx index 1e0643c1f..c3c02eca7 100644 --- a/src/components/DocsCalloutQueryGG.tsx +++ b/src/components/DocsCalloutQueryGG.tsx @@ -1,4 +1,4 @@ -import { LogoQueryGGSmall } from '~/components/LogoQueryGGSmall' +import { LogoQueryGG } from '~/ui' import { useQueryGGPPPDiscount } from '~/hooks/useQueryGGPPPDiscount' export function DocsCalloutQueryGG() { @@ -15,7 +15,7 @@ export function DocsCalloutQueryGG() {
Want to Skip the Docs?
- +
“If you’re serious about *really* understanding React Query, there’s diff --git a/src/components/FeedbackModerationList.tsx b/src/components/FeedbackModerationList.tsx index c62a818d8..12642d02a 100644 --- a/src/components/FeedbackModerationList.tsx +++ b/src/components/FeedbackModerationList.tsx @@ -16,6 +16,7 @@ import type { DocFeedback } from '~/db/types' import { calculatePoints } from '~/utils/docFeedback.client' import { Check, Lightbulb, TriangleAlert } from 'lucide-react' import { MessageSquare, X } from 'lucide-react' +import { Badge } from '~/ui' interface FeedbackModerationListProps { data: @@ -192,20 +193,18 @@ export function FeedbackModerationList({ - {feedback.status.charAt(0).toUpperCase() + feedback.status.slice(1)} - +
diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx deleted file mode 100644 index 207f4e681..000000000 --- a/src/components/LogoQueryGG.tsx +++ /dev/null @@ -1,221 +0,0 @@ -export function LogoQueryGG(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx deleted file mode 100644 index 0ff66c196..000000000 --- a/src/components/LogoQueryGGSmall.tsx +++ /dev/null @@ -1,228 +0,0 @@ -export function LogoQueryGGSmall(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/src/components/NotesModerationList.tsx b/src/components/NotesModerationList.tsx index 96973f726..bc1cd8186 100644 --- a/src/components/NotesModerationList.tsx +++ b/src/components/NotesModerationList.tsx @@ -14,6 +14,7 @@ import { Spinner } from './Spinner' import type { DocFeedback } from '~/db/types' import { calculatePoints } from '~/utils/docFeedback.client' import { ExternalLink, TriangleAlert } from 'lucide-react' +import { Badge } from '~/ui' interface NotesModerationListProps { data: @@ -130,15 +131,12 @@ export function NotesModerationList({ (feedback.content.length > 100 ? '...' : '') const charCount = feedback.content.length - // Status badge styling - const statusStyles = { - pending: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400', - approved: - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', - denied: - 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400', - } + // Status badge variant mapping + const statusVariants = { + pending: 'warning', + approved: 'success', + denied: 'error', + } as const return ( @@ -153,14 +151,9 @@ export function NotesModerationList({ {(page - 1) * pageSize + index + 1} - + {feedback.status} - +
diff --git a/src/components/SimpleMarkdown.tsx b/src/components/SimpleMarkdown.tsx index 3a3e019ac..d30dc53ea 100644 --- a/src/components/SimpleMarkdown.tsx +++ b/src/components/SimpleMarkdown.tsx @@ -8,7 +8,7 @@ import parse, { HTMLReactParserOptions, } from 'html-react-parser' import { renderMarkdown } from '~/utils/markdown' -import { getNetlifyImageUrl } from '~/utils/netlifyImage' +import { InlineCode, MarkdownImg } from '~/ui' /** * Lightweight markdown renderer for simple content like excerpts. @@ -18,16 +18,7 @@ import { getNetlifyImageUrl } from '~/utils/netlifyImage' const markdownComponents: Record> = { a: MarkdownLink, - code: function Code({ className, ...rest }: HTMLProps) { - return ( - - ) - }, + code: InlineCode, pre: function Pre({ children, ...rest }: HTMLProps) { return (
> = {
       
) }, - img: ({ - alt, - src, - className, - children: _, - ...props - }: HTMLProps) => ( - {alt - ), + img: MarkdownImg, } const options: HTMLReactParserOptions = { diff --git a/src/components/admin/BannerEditor.tsx b/src/components/admin/BannerEditor.tsx index 9938d4bcd..3fb488a5a 100644 --- a/src/components/admin/BannerEditor.tsx +++ b/src/components/admin/BannerEditor.tsx @@ -22,6 +22,7 @@ import { Gift, ExternalLink, } from 'lucide-react' +import { FormInput } from '~/ui' interface BannerEditorProps { banner: BannerWithMeta | null @@ -186,8 +187,6 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { 'px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50' const labelClass = 'block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2' - const inputClass = - 'w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow' return (
@@ -250,11 +249,10 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { - setTitle(e.target.value)} - className={inputClass} placeholder="Enter banner title" />
@@ -271,7 +269,7 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { value={content} onChange={(e) => setContent(e.target.value)} rows={2} - className={inputClass} + className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" placeholder="Optional additional details" />
@@ -301,11 +299,10 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { - setLinkUrl(e.target.value)} - className={inputClass} placeholder="https://example.com/page" />
@@ -318,11 +315,10 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { (Button text - defaults to "Learn More") - setLinkText(e.target.value)} - className={inputClass} placeholder="Learn More" /> @@ -484,7 +480,7 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { {/* Custom path input */}
- setNewPathPrefix(e.target.value)} @@ -495,7 +491,8 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { } }} placeholder="/custom/path" - className="flex-1 px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-purple-500 focus:border-transparent" + focusRing="purple" + className="flex-1 px-3 py-2 text-sm" />
@@ -605,11 +602,11 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { (Optional - never expires if empty) - setExpiresAt(e.target.value)} - className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-shadow" + focusRing="orange" /> @@ -621,11 +618,11 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { (Higher = shown first when multiple banners match) - setPriority(parseInt(e.target.value) || 0)} - className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-shadow" + focusRing="orange" /> diff --git a/src/components/admin/CapabilityBadge.tsx b/src/components/admin/CapabilityBadge.tsx index e9b536575..2bfb23029 100644 --- a/src/components/admin/CapabilityBadge.tsx +++ b/src/components/admin/CapabilityBadge.tsx @@ -1,4 +1,4 @@ -import { twMerge } from 'tailwind-merge' +import { Badge } from '~/ui' import type { Capability } from '~/db/types' type CapabilityBadgeProps = { @@ -6,36 +6,27 @@ type CapabilityBadgeProps = { className?: string } -const capabilityStyles: Record = { - admin: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', - disableAds: - 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', - builder: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', - feed: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300', - 'moderate-feedback': - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - 'moderate-showcases': - 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300', +const capabilityVariants: Record< + string, + 'error' | 'purple' | 'info' | 'orange' | 'success' | 'teal' | 'default' +> = { + admin: 'error', + disableAds: 'purple', + builder: 'info', + feed: 'orange', + 'moderate-feedback': 'success', + 'moderate-showcases': 'teal', } -const defaultStyle = - 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' - export function CapabilityBadge({ capability, className, }: CapabilityBadgeProps) { - const style = capabilityStyles[capability] || defaultStyle + const variant = capabilityVariants[capability] || 'default' return ( - + {capability} - + ) } diff --git a/src/components/admin/FeedEntryEditor.tsx b/src/components/admin/FeedEntryEditor.tsx index c43cddc2e..f7a77cc06 100644 --- a/src/components/admin/FeedEntryEditor.tsx +++ b/src/components/admin/FeedEntryEditor.tsx @@ -18,6 +18,7 @@ import { Calendar, Check, } from 'lucide-react' +import { FormInput } from '~/ui' interface FeedEntryEditorProps { entry: FeedEntry | null @@ -150,8 +151,6 @@ export function FeedEntryEditor({ 'px-6 py-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50' const labelClass = 'block text-sm font-medium text-gray-600 dark:text-gray-400 mb-2' - const inputClass = - 'w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow' const chipBase = 'px-3 py-1.5 rounded-full text-sm font-medium transition-all' const chipUnselected = 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600' @@ -217,11 +216,10 @@ export function FeedEntryEditor({ - setTitle(e.target.value)} - className={inputClass} placeholder="Enter a descriptive title" /> @@ -255,7 +253,7 @@ export function FeedEntryEditor({ value={excerpt} onChange={(e) => setExcerpt(e.target.value)} rows={2} - className={inputClass} + className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-shadow" placeholder="Brief summary for feed previews" /> @@ -266,11 +264,10 @@ export function FeedEntryEditor({ Published Date * - setPublishedAt(e.target.value)} - className={inputClass} /> @@ -366,11 +363,10 @@ export function FeedEntryEditor({ (comma-separated) - setTags(e.target.value)} - className={inputClass} placeholder="e.g., release:major, breaking-change" /> diff --git a/src/components/admin/StatusBadge.tsx b/src/components/admin/StatusBadge.tsx index 4c527f589..25489a76e 100644 --- a/src/components/admin/StatusBadge.tsx +++ b/src/components/admin/StatusBadge.tsx @@ -1,47 +1,38 @@ -import { twMerge } from 'tailwind-merge' +import { Badge } from '~/ui' type StatusBadgeProps = { status: string className?: string } -const statusStyles: Record = { +const statusVariants: Record< + string, + 'success' | 'warning' | 'error' | 'info' | 'default' +> = { // Approval statuses - approved: - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - pending: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', - denied: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', + approved: 'success', + pending: 'warning', + denied: 'error', // Active/Inactive - active: - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - inactive: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', + active: 'success', + inactive: 'default', // Boolean states - true: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - false: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', + true: 'success', + false: 'default', // OAuth providers - github: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', - google: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', + github: 'default', + google: 'info', } -const defaultStyle = - 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' - export function StatusBadge({ status, className }: StatusBadgeProps) { - const style = statusStyles[status.toLowerCase()] || defaultStyle + const variant = statusVariants[status.toLowerCase()] || 'default' return ( - + {status} - + ) } diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx index 580b4b197..056741c7f 100644 --- a/src/components/markdown/Markdown.tsx +++ b/src/components/markdown/Markdown.tsx @@ -1,19 +1,14 @@ +import type { HTMLProps } from 'react' import * as React from 'react' import { MarkdownLink } from './MarkdownLink' -import type { HTMLProps } from 'react' -import parse, { - attributesToProps, - domToReact, - Element, - HTMLReactParserOptions, -} from 'html-react-parser' +import parse, { attributesToProps, domToReact, Element, HTMLReactParserOptions, } from 'html-react-parser' import { renderMarkdown } from '~/utils/markdown' -import { getNetlifyImageUrl } from '~/utils/netlifyImage' import { CodeBlock } from './CodeBlock' import { handleTabsComponent } from './MarkdownTabsHandler' import { handleFrameworkComponent } from './MarkdownFrameworkHandler' +import { InlineCode, MarkdownImg } from '~/ui' type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' @@ -67,39 +62,6 @@ const makeHeading = /> ) -const InlineCode = React.memo(function InlineCode({ - className, - ...rest -}: HTMLProps) { - return ( - - ) -}) - -const MarkdownImage = React.memo(function MarkdownImage({ - alt, - src, - className, - children: _, - ...props -}: HTMLProps) { - return ( - {alt - ) -}) - const MarkdownIframe = React.memo(function MarkdownIframe( props: HTMLProps, ) { @@ -117,7 +79,7 @@ const markdownComponents: Record = { h6: makeHeading('h6'), code: InlineCode, iframe: MarkdownIframe, - img: MarkdownImage, + img: MarkdownImg, } const options: HTMLReactParserOptions = { @@ -178,13 +140,11 @@ export const Markdown = React.memo(function Markdown({ return { markup: '', headings: [] } }, [rawContent, htmlMarkup]) - const parsed = React.useMemo(() => { + return React.useMemo(() => { if (!rendered.markup) { return null } return parse(rendered.markup, options) }, [rendered.markup]) - - return parsed }) diff --git a/src/routes/learn.tsx b/src/routes/learn.tsx index 0855295d1..dafbf96da 100644 --- a/src/routes/learn.tsx +++ b/src/routes/learn.tsx @@ -1,7 +1,7 @@ import { Link, createFileRoute } from '@tanstack/react-router' import { seo } from '~/utils/seo' import { Users, Video, MapPin, Star } from 'lucide-react' -import { LogoQueryGG } from '~/components/LogoQueryGG' +import { LogoQueryGG } from '~/ui' import { CheckCircleIcon } from '~/components/icons/CheckCircleIcon' export const Route = createFileRoute('/learn')({ diff --git a/src/ui/Badge.tsx b/src/ui/Badge.tsx new file mode 100644 index 000000000..f97ba812f --- /dev/null +++ b/src/ui/Badge.tsx @@ -0,0 +1,50 @@ +import { twMerge } from 'tailwind-merge' + +type BadgeVariant = + | 'default' + | 'success' + | 'warning' + | 'error' + | 'info' + | 'purple' + | 'teal' + | 'orange' + +type BadgeProps = { + children: React.ReactNode + variant?: BadgeVariant + className?: string +} + +const variantStyles: Record = { + default: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300', + success: + 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', + warning: + 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', + error: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', + info: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', + purple: + 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', + teal: 'bg-teal-100 text-teal-800 dark:bg-teal-900/30 dark:text-teal-300', + orange: + 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300', +} + +export function Badge({ + children, + variant = 'default', + className, +}: BadgeProps) { + return ( + + {children} + + ) +} diff --git a/src/ui/FormInput.tsx b/src/ui/FormInput.tsx new file mode 100644 index 000000000..abf7f2ebf --- /dev/null +++ b/src/ui/FormInput.tsx @@ -0,0 +1,29 @@ +import * as React from 'react' +import { twMerge } from 'tailwind-merge' + +type FormInputProps = React.InputHTMLAttributes & { + /** Ring color on focus. Defaults to blue. */ + focusRing?: 'blue' | 'orange' | 'purple' +} + +const ringStyles = { + blue: 'focus:ring-blue-500', + orange: 'focus:ring-orange-500', + purple: 'focus:ring-purple-500', +} + +export const FormInput = React.forwardRef( + function FormInput({ className, focusRing = 'blue', ...props }, ref) { + return ( + + ) + }, +) diff --git a/src/ui/InlineCode.tsx b/src/ui/InlineCode.tsx new file mode 100644 index 000000000..9e0dfa9a6 --- /dev/null +++ b/src/ui/InlineCode.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' +import type { HTMLProps } from 'react' + +export const InlineCode = React.memo(function InlineCode({ + className, + ...rest +}: HTMLProps) { + return ( + + ) +}) diff --git a/src/ui/LogoQueryGG.tsx b/src/ui/LogoQueryGG.tsx new file mode 100644 index 000000000..af86c606e --- /dev/null +++ b/src/ui/LogoQueryGG.tsx @@ -0,0 +1,389 @@ +type LogoQueryGGProps = Omit, 'size'> & { + /** Size variant. 'small' uses compact viewBox. Defaults to 'default'. */ + size?: 'default' | 'small' +} + +export function LogoQueryGG({ size = 'default', ...props }: LogoQueryGGProps) { + if (size === 'small') { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) + } + + // Default (large) size - keeping original SVG content + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/ui/MarkdownImg.tsx b/src/ui/MarkdownImg.tsx new file mode 100644 index 000000000..141dac6bf --- /dev/null +++ b/src/ui/MarkdownImg.tsx @@ -0,0 +1,22 @@ +import * as React from 'react' +import type { HTMLProps } from 'react' +import { getNetlifyImageUrl } from '~/utils/netlifyImage' + +export const MarkdownImg = React.memo(function MarkdownImg({ + alt, + src, + className, + children: _, + ...props +}: HTMLProps) { + return ( + {alt + ) +}) diff --git a/src/ui/index.ts b/src/ui/index.ts new file mode 100644 index 000000000..7858ac67d --- /dev/null +++ b/src/ui/index.ts @@ -0,0 +1,5 @@ +export { Badge } from './Badge' +export { FormInput } from './FormInput' +export { InlineCode } from './InlineCode' +export { MarkdownImg } from './MarkdownImg' +export { LogoQueryGG } from './LogoQueryGG' From d4edcd2e6c8981a22eaceea820ba439f97fe48f0 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 12:02:57 -0800 Subject: [PATCH 02/11] feat: Replace status indicators with Badge component in submissions and users --- src/routes/account/submissions.tsx | 13 +++---- src/routes/admin/users.tsx | 57 +++++------------------------- 2 files changed, 16 insertions(+), 54 deletions(-) diff --git a/src/routes/account/submissions.tsx b/src/routes/account/submissions.tsx index c3adeb291..f878624d1 100644 --- a/src/routes/account/submissions.tsx +++ b/src/routes/account/submissions.tsx @@ -17,6 +17,7 @@ import { } from 'lucide-react' import type { Showcase } from '~/db/types' import { Button, buttonStyles } from '~/components/Button' +import { Badge } from '~/ui' export const Route = createFileRoute('/account/submissions')({ loader: async ({ context: { queryClient } }) => { @@ -79,24 +80,24 @@ function AccountSubmissionsPage() { switch (status) { case 'pending': return ( - + Pending Review - + ) case 'approved': return ( - + Approved - + ) case 'denied': return ( - + Denied - + ) } } diff --git a/src/routes/admin/users.tsx b/src/routes/admin/users.tsx index 0d804bcda..f5188a607 100644 --- a/src/routes/admin/users.tsx +++ b/src/routes/admin/users.tsx @@ -47,6 +47,7 @@ import { useAdminGuard } from '~/hooks/useAdminGuard' import { useToggleArray } from '~/hooks/useToggleArray' import { handleAdminError } from '~/utils/adminErrors' import { requireCapability } from '~/utils/auth.server' +import { Badge } from '~/ui' // User type for table - matches the shape returned by listUsers type User = { @@ -129,12 +130,9 @@ function UserRolesCell({ return (
{(userRoles || []).map((role) => ( - + {role.name} - + ))} {(!userRoles || userRoles.length === 0) && ( @@ -163,12 +161,9 @@ function EffectiveCapabilitiesCell({ return (
{(effectiveCapabilities || []).map((capability: string) => ( - + {capability} - + ))} {(!effectiveCapabilities || effectiveCapabilities.length === 0) && ( None @@ -231,14 +226,6 @@ function UsersPage() { const sortBy = search.sortBy const sortDir = search.sortDir - const hasActiveFilters = - emailFilter !== '' || - nameFilter !== '' || - capabilityFilters.length > 0 || - noCapabilitiesFilter || - adsDisabledFilter !== 'all' || - waitlistFilter !== 'all' - const handleClearFilters = () => { navigate({ resetScroll: false, @@ -479,23 +466,6 @@ function UsersPage() { [adminSetAdsDisabled], ) - const handleCapabilityToggle = useCallback( - (capability: string) => { - const newFilters = capabilityFilters.includes(capability) - ? capabilityFilters.filter((c: string) => c !== capability) - : [...capabilityFilters, capability] - navigate({ - resetScroll: false, - search: (prev: UsersSearch) => ({ - ...prev, - cap: newFilters.length > 0 ? newFilters : undefined, - page: 0, - }), - }) - }, - [capabilityFilters, navigate], - ) - const handleSort = useCallback( (column: Column) => { const columnId = column.id @@ -627,12 +597,9 @@ function UsersPage() { ) : (
{(user.capabilities || []).map((capability: string) => ( - + {capability} - + ))} {(!user.capabilities || user.capabilities.length === 0) && ( @@ -710,15 +677,9 @@ function UsersPage() { const user = row.original const onWaitlist = Boolean(user.interestedInHidingAds) return ( - + {onWaitlist ? 'Yes' : 'No'} - + ) }, }, From 0baa2d5c07875ff8fb19d0946e901decf91fe642 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 12:52:58 -0800 Subject: [PATCH 03/11] feat: Replace input fields with FormInput component in moderation and feedback sections --- src/components/FeedbackModerationTopBar.tsx | 10 ++-- src/components/NotesModerationTopBar.tsx | 10 ++-- src/components/ShowcaseModerationList.tsx | 31 ++++++------ src/components/ShowcaseSubmitForm.tsx | 43 ++++++++-------- src/components/UserFeedbackSection.tsx | 28 +++++------ src/components/UsersTopBarFilters.tsx | 8 +-- src/routes/account/integrations.tsx | 8 +-- src/routes/admin/feedback_.$id.tsx | 39 +++++++-------- src/routes/admin/index.tsx | 11 ++--- src/routes/admin/logins.tsx | 23 ++------- src/routes/admin/roles.$roleId.tsx | 15 ++---- src/routes/admin/roles.index.tsx | 32 +++--------- src/routes/admin/showcases_.$id.tsx | 54 +++++++++------------ src/routes/admin/users.$userId.tsx | 41 +++++----------- src/ui/FormInput.tsx | 2 +- 15 files changed, 146 insertions(+), 209 deletions(-) diff --git a/src/components/FeedbackModerationTopBar.tsx b/src/components/FeedbackModerationTopBar.tsx index d50d1f944..e00cba36f 100644 --- a/src/components/FeedbackModerationTopBar.tsx +++ b/src/components/FeedbackModerationTopBar.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import { libraries } from '~/libraries' import { DOC_FEEDBACK_STATUSES, type DocFeedbackStatus } from '~/db/types' import { @@ -9,6 +8,7 @@ import { FilterCheckbox, getFilterChipLabel, } from '~/components/FilterComponents' +import { FormInput } from '~/ui' interface FeedbackModerationTopBarProps { filters: { @@ -169,14 +169,14 @@ export function FeedbackModerationTopBar({ > From - onFilterChange({ dateFrom: e.target.value || undefined }) } - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="text-sm" />
@@ -186,14 +186,14 @@ export function FeedbackModerationTopBar({ > To - onFilterChange({ dateTo: e.target.value || undefined }) } - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="text-sm" />
diff --git a/src/components/NotesModerationTopBar.tsx b/src/components/NotesModerationTopBar.tsx index fd9c98ea0..2926e20ab 100644 --- a/src/components/NotesModerationTopBar.tsx +++ b/src/components/NotesModerationTopBar.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import { libraries } from '~/libraries' import { TopBarFilter, @@ -7,6 +6,7 @@ import { FilterDropdownSection, FilterCheckbox, } from '~/components/FilterComponents' +import { FormInput } from '~/ui' interface NotesModerationTopBarProps { filters: { @@ -128,14 +128,14 @@ export function NotesModerationTopBar({ > From - onFilterChange({ dateFrom: e.target.value || undefined }) } - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="text-sm" />
@@ -145,14 +145,14 @@ export function NotesModerationTopBar({ > To - onFilterChange({ dateTo: e.target.value || undefined }) } - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="text-sm" />
diff --git a/src/components/ShowcaseModerationList.tsx b/src/components/ShowcaseModerationList.tsx index 48ac9a1c1..3c731bb75 100644 --- a/src/components/ShowcaseModerationList.tsx +++ b/src/components/ShowcaseModerationList.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import { Link } from '@tanstack/react-router' import { twMerge } from 'tailwind-merge' import { @@ -23,6 +22,8 @@ import { ThumbsDown, } from 'lucide-react' import { libraries } from '~/libraries' +import { Badge } from '~/ui' +import { Fragment, useState } from 'react' interface ShowcaseModerationListProps { data: @@ -77,8 +78,8 @@ export function ShowcaseModerationList({ onVote, isModeratingId, }: ShowcaseModerationListProps) { - const [expandedIds, setExpandedIds] = React.useState>(new Set()) - const [moderationNotes, setModerationNotes] = React.useState< + const [expandedIds, setExpandedIds] = useState>(new Set()) + const [moderationNotes, setModerationNotes] = useState< Record >({}) @@ -206,7 +207,7 @@ export function ShowcaseModerationList({ const isModeratingThis = isModeratingId === showcase.id return ( - + toggleExpanded(showcase.id)} @@ -239,20 +240,18 @@ export function ShowcaseModerationList({
- {showcase.status.charAt(0).toUpperCase() + showcase.status.slice(1)} - +
@@ -575,7 +574,7 @@ export function ShowcaseModerationList({ )} - + ) })} diff --git a/src/components/ShowcaseSubmitForm.tsx b/src/components/ShowcaseSubmitForm.tsx index 6ad3a6c2f..77c79e086 100644 --- a/src/components/ShowcaseSubmitForm.tsx +++ b/src/components/ShowcaseSubmitForm.tsx @@ -1,4 +1,3 @@ -import * as React from 'react' import { useMutation } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' import { submitShowcase, updateShowcase } from '~/utils/showcase.functions' @@ -16,6 +15,8 @@ import { useToast } from './ToastProvider' import { Check, AlertCircle } from 'lucide-react' import { Button } from './Button' import { ImageUpload } from './ImageUpload' +import { FormInput } from '~/ui' +import { FormEvent, useMemo, useState } from 'react' // Filter to only show libraries with proper configuration const selectableLibraries = libraries.filter( @@ -32,29 +33,29 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { const { notify } = useToast() const isEditMode = !!showcase - const [name, setName] = React.useState(showcase?.name ?? '') - const [tagline, setTagline] = React.useState(showcase?.tagline ?? '') - const [description, setDescription] = React.useState( + const [name, setName] = useState(showcase?.name ?? '') + const [tagline, setTagline] = useState(showcase?.tagline ?? '') + const [description, setDescription] = useState( showcase?.description ?? '', ) - const [url, setUrl] = React.useState(showcase?.url ?? '') - const [logoUrl, setLogoUrl] = React.useState( + const [url, setUrl] = useState(showcase?.url ?? '') + const [logoUrl, setLogoUrl] = useState( showcase?.logoUrl ?? undefined, ) - const [screenshotUrl, setScreenshotUrl] = React.useState( + const [screenshotUrl, setScreenshotUrl] = useState( showcase?.screenshotUrl ?? undefined, ) - const [selectedLibraries, setSelectedLibraries] = React.useState( + const [selectedLibraries, setSelectedLibraries] = useState( showcase?.libraries ?? [], ) - const [selectedUseCases, setSelectedUseCases] = React.useState< + const [selectedUseCases, setSelectedUseCases] = useState< ShowcaseUseCase[] >(showcase?.useCases ?? []) - const [isOpenSource, setIsOpenSource] = React.useState(!!showcase?.sourceUrl) - const [sourceUrl, setSourceUrl] = React.useState(showcase?.sourceUrl ?? '') + const [isOpenSource, setIsOpenSource] = useState(!!showcase?.sourceUrl) + const [sourceUrl, setSourceUrl] = useState(showcase?.sourceUrl ?? '') // Get auto-included libraries based on selection - const autoIncluded = React.useMemo( + const autoIncluded = useMemo( () => getAutoIncludedLibraries(selectedLibraries), [selectedLibraries], ) @@ -121,7 +122,7 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { ) } - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = (e: FormEvent) => { e.preventDefault() if (selectedLibraries.length === 0) { @@ -207,14 +208,14 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { > Project Name * - setName(e.target.value)} required maxLength={255} - className="mt-1 block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="mt-1 px-4 py-3" placeholder="My Awesome App" />
@@ -227,13 +228,13 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { > Project URL * - setUrl(e.target.value)} required - className="mt-1 block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="mt-1 px-4 py-3" placeholder="https://your-project.com" /> @@ -246,14 +247,14 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { > Tagline * - setTagline(e.target.value)} required maxLength={500} - className="mt-1 block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="mt-1 px-4 py-3" placeholder="A brief description of your project" />

@@ -323,13 +324,13 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { > Source Code URL * - setSourceUrl(e.target.value)} required - className="mt-1 block w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="mt-1 px-4 py-3" placeholder="https://github.com/username/repo" />

diff --git a/src/components/UserFeedbackSection.tsx b/src/components/UserFeedbackSection.tsx index 059d286e1..cc10fa430 100644 --- a/src/components/UserFeedbackSection.tsx +++ b/src/components/UserFeedbackSection.tsx @@ -1,20 +1,20 @@ -import * as React from 'react' import { useQuery } from '@tanstack/react-query' import { Link } from '@tanstack/react-router' import { getUserDocFeedbackQueryOptions } from '~/queries/docFeedback' -import { twMerge } from 'tailwind-merge' import { PaginationControls } from './PaginationControls' import { Spinner } from './Spinner' import { calculatePoints } from '~/utils/docFeedback.client' import { Award, ExternalLink, Lightbulb, MessageSquare } from 'lucide-react' +import { Badge } from '~/ui' +import { useState } from 'react' interface UserFeedbackSectionProps { userId: string } export function UserFeedbackSection({}: UserFeedbackSectionProps) { - const [page, setPage] = React.useState(1) - const [pageSize, setPageSize] = React.useState(10) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) const { data, isLoading } = useQuery( getUserDocFeedbackQueryOptions({ @@ -154,20 +154,18 @@ export function UserFeedbackSection({}: UserFeedbackSectionProps) { {/* Status & Points */}

- {item.status.charAt(0).toUpperCase() + item.status.slice(1)} - + {calculatePoints( item.characterCount, diff --git a/src/components/UsersTopBarFilters.tsx b/src/components/UsersTopBarFilters.tsx index 6997eb13b..1cc3aa8a3 100644 --- a/src/components/UsersTopBarFilters.tsx +++ b/src/components/UsersTopBarFilters.tsx @@ -1,5 +1,3 @@ -import * as React from 'react' -import { VALID_CAPABILITIES, type Capability } from '~/db/types' import { TopBarFilter, FilterChip, @@ -8,6 +6,8 @@ import { FilterCheckbox, getFilterChipLabel, } from '~/components/FilterComponents' +import { FormInput } from '~/ui' +import { VALID_CAPABILITIES } from '~/auth' interface UsersFilters { email?: string @@ -127,14 +127,14 @@ export function UsersTopBarFilters({ {/* Name Filter */} - onFilterChange({ name: e.target.value || undefined }) } placeholder="Filter by name..." - className="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500" + className="text-sm" /> diff --git a/src/routes/account/integrations.tsx b/src/routes/account/integrations.tsx index 904abbfb5..ffef50379 100644 --- a/src/routes/account/integrations.tsx +++ b/src/routes/account/integrations.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { createFileRoute, Link } from '@tanstack/react-router' +import { createFileRoute } from '@tanstack/react-router' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { listMcpApiKeys, @@ -14,7 +14,6 @@ import { import { useToast } from '~/components/ToastProvider' import { Card } from '~/components/Card' import { Button } from '~/components/Button' -import { CodeBlock } from '~/components/markdown' import { Key, Plus, @@ -26,6 +25,7 @@ import { Clock, Link2, } from 'lucide-react' +import { FormInput } from '~/ui' export const Route = createFileRoute('/account/integrations')({ component: IntegrationsPage, }) @@ -225,13 +225,13 @@ function IntegrationsPage() { > Name - setNewKeyName(e.target.value)} placeholder="e.g., My Claude Desktop" - className="w-full max-w-sm border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 text-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100" + className="max-w-sm text-sm rounded-md" />
diff --git a/src/routes/admin/feedback_.$id.tsx b/src/routes/admin/feedback_.$id.tsx index d6a0eac40..f788b6051 100644 --- a/src/routes/admin/feedback_.$id.tsx +++ b/src/routes/admin/feedback_.$id.tsx @@ -13,12 +13,12 @@ import { FileText, Check, X, - ExternalLink, Clock, AlertTriangle, } from 'lucide-react' import { Card } from '~/components/Card' import { Button } from '~/components/Button' +import { Badge } from '~/ui' import { format } from '~/utils/dates' export const Route = createFileRoute('/admin/feedback_/$id')({ @@ -92,19 +92,16 @@ function FeedbackDetailPage() { const { feedback, user } = data const library = libraries.find((l) => l.id === feedback.libraryId) - const statusColors = { - pending: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', - approved: - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - denied: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', - } + const statusVariant = { + pending: 'warning', + approved: 'success', + denied: 'error', + } as const - const typeColors = { - improvement: - 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300', - note: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300', - } + const typeVariant = { + improvement: 'info', + note: 'purple', + } as const return (
@@ -128,16 +125,20 @@ function FeedbackDetailPage() {

Feedback

- {feedback.status} - - + {feedback.type} - + {feedback.isDetached && ( diff --git a/src/routes/admin/index.tsx b/src/routes/admin/index.tsx index 89a9737e1..f110cf5db 100644 --- a/src/routes/admin/index.tsx +++ b/src/routes/admin/index.tsx @@ -26,6 +26,7 @@ import { Users, } from 'lucide-react' import { Card } from '~/components/Card' +import { Badge } from '~/ui' import * as v from 'valibot' import { TimeSeriesChart } from '~/components/charts/TimeSeriesChart' import { ChartControls } from '~/components/charts/ChartControls' @@ -728,15 +729,9 @@ function ActivityTab({ key={provider} className="flex items-center justify-between" > - + {provider} - +
{count.toLocaleString()} diff --git a/src/routes/admin/logins.tsx b/src/routes/admin/logins.tsx index 272822c30..bc395d90e 100644 --- a/src/routes/admin/logins.tsx +++ b/src/routes/admin/logins.tsx @@ -22,6 +22,7 @@ import { import * as v from 'valibot' import { listLoginHistory } from '~/utils/audit.functions' import { LogIn } from 'lucide-react' +import { Badge } from '~/ui' import { AdminAccessDenied, AdminLoading, @@ -120,8 +121,6 @@ function LoginsPage() { placeholderData: keepPreviousData, }) - const hasActiveFilters = userIdFilter !== '' || !!providerFilter - const handleClearFilters = () => { navigate({ resetScroll: false, @@ -205,15 +204,9 @@ function LoginsPage() { cell: ({ getValue }) => { const provider = getValue() as string return ( - + {provider} - + ) }, }, @@ -223,15 +216,9 @@ function LoginsPage() { cell: ({ getValue }) => { const isNew = getValue() as boolean return ( - + {isNew ? 'Signup' : 'Login'} - + ) }, }, diff --git a/src/routes/admin/roles.$roleId.tsx b/src/routes/admin/roles.$roleId.tsx index f555c6c32..983698992 100644 --- a/src/routes/admin/roles.$roleId.tsx +++ b/src/routes/admin/roles.$roleId.tsx @@ -13,6 +13,7 @@ import { import { ArrowLeft, Lock, Trash, User, Users } from 'lucide-react' import { requireCapability } from '~/utils/auth.server' import { hasCapability } from '~/db/types' +import { Badge } from '~/ui' export const Route = createFileRoute('/admin/roles/$roleId')({ beforeLoad: async () => { @@ -169,12 +170,9 @@ function RoleDetailPage() { return (
{(user.capabilities || []).map((capability: string) => ( - + {capability} - + ))} {(!user.capabilities || user.capabilities.length === 0) && ( @@ -288,12 +286,9 @@ function RoleDetailPage() {
{role.capabilities.map((capability) => ( - + {capability} - + ))}
diff --git a/src/routes/admin/roles.index.tsx b/src/routes/admin/roles.index.tsx index fb78cbd6e..35c4cc4ab 100644 --- a/src/routes/admin/roles.index.tsx +++ b/src/routes/admin/roles.index.tsx @@ -41,6 +41,7 @@ import { useAdminGuard } from '~/hooks/useAdminGuard' import { requireCapability } from '~/utils/auth.server' import { useToggleArray } from '~/hooks/useToggleArray' import { useDeleteWithConfirmation } from '~/hooks/useDeleteWithConfirmation' +import { Badge, FormInput } from '~/ui' // Role type for table - matches the shape returned by listRoles interface Role { @@ -205,22 +206,6 @@ function RolesPage() { itemLabel: 'role', }) - const handleCapabilityFilterToggle = useCallback( - (capability: string) => { - const newFilters = capabilityFilters.includes(capability) - ? capabilityFilters.filter((c: string) => c !== capability) - : [...capabilityFilters, capability] - navigate({ - resetScroll: false, - search: (prev: { name?: string; cap?: string | string[] }) => ({ - ...prev, - cap: newFilters.length > 0 ? newFilters : undefined, - }), - }) - }, - [capabilityFilters, navigate], - ) - const handleSendTestEmail = useCallback(async () => { setTestEmailStatus({ loading: true }) try { @@ -312,12 +297,9 @@ function RolesPage() { ) : (
{(role.capabilities || []).map((capability) => ( - + {capability} - + ))} {(!role.capabilities || role.capabilities.length === 0) && ( @@ -502,11 +484,11 @@ function RolesPage() { > Name - setEditingName(e.target.value)} - className="w-full px-3 py-2 border rounded-md bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white" + className="rounded-md" placeholder="Role name" />
@@ -517,11 +499,11 @@ function RolesPage() { > Description - setEditingDescription(e.target.value)} - className="w-full px-3 py-2 border rounded-md bg-white dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white" + className="rounded-md" placeholder="Role description" />
diff --git a/src/routes/admin/showcases_.$id.tsx b/src/routes/admin/showcases_.$id.tsx index da5a0d6f1..eea03c0d7 100644 --- a/src/routes/admin/showcases_.$id.tsx +++ b/src/routes/admin/showcases_.$id.tsx @@ -29,6 +29,7 @@ import { } from 'lucide-react' import { Card } from '~/components/Card' import { Button } from '~/components/Button' +import { Badge, FormInput } from '~/ui' import { ImageUpload } from '~/components/ImageUpload' import { format } from '~/utils/dates' import { @@ -230,16 +231,12 @@ function ShowcaseDetailPage() { .map((libId: string) => libraries.find((l) => l.id === libId)) .filter(Boolean) - const statusColors = { - pending: - 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300', - approved: - 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300', - denied: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300', - } + const statusVariant = { + pending: 'warning', + approved: 'success', + denied: 'error', + } as const - const inputClass = - 'w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent' const labelClass = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1' @@ -330,7 +327,7 @@ function ShowcaseDetailPage() {
{isEditing ? (
- @@ -338,10 +335,10 @@ function ShowcaseDetailPage() { prev ? { ...prev, name: e.target.value } : null, ) } - className={`${inputClass} text-xl font-bold`} + className="text-xl font-bold" placeholder="Showcase name" /> - @@ -349,7 +346,6 @@ function ShowcaseDetailPage() { prev ? { ...prev, tagline: e.target.value } : null, ) } - className={inputClass} placeholder="Tagline" />
@@ -359,15 +355,17 @@ function ShowcaseDetailPage() {

{showcase.name}

- {showcase.status} - + {showcase.isFeatured && ( - - Featured - + Featured )}

@@ -417,7 +415,7 @@ function ShowcaseDetailPage() { -

@@ -446,7 +443,7 @@ function ShowcaseDetailPage() { -

@@ -476,7 +472,7 @@ function ShowcaseDetailPage() { prev ? { ...prev, description: e.target.value } : null, ) } - className={`${inputClass} min-h-[100px]`} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent min-h-[100px]" placeholder="Describe the showcase..." /> @@ -484,7 +480,7 @@ function ShowcaseDetailPage() { -

@@ -511,7 +506,7 @@ function ShowcaseDetailPage() { - @@ -788,7 +782,7 @@ function ShowcaseDetailPage() { : null, ) } - className={inputClass} + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" > {SHOWCASE_STATUSES.map((status) => (

{user.capabilities && user.capabilities.length > 0 ? ( user.capabilities.map((cap: string) => ( - + {cap} - + )) ) : ( @@ -224,9 +221,10 @@ function UserDetailPage() { key={role._id} to="/admin/roles/$roleId" params={{ roleId: role._id }} - className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-900/50" > - {role.name} + + {role.name} + )) ) : ( @@ -244,12 +242,9 @@ function UserDetailPage() {
{effectiveCapabilities.length > 0 ? ( effectiveCapabilities.map((cap) => ( - + {cap} - + )) ) : ( @@ -272,15 +267,9 @@ function UserDetailPage() { Ads Disabled
- + {user.adsDisabled ? 'Yes' : 'No'} - +
@@ -288,15 +277,11 @@ function UserDetailPage() { Interested in Hiding Ads
- {user.interestedInHidingAds ? 'Yes' : 'No'} - +
diff --git a/src/ui/FormInput.tsx b/src/ui/FormInput.tsx index abf7f2ebf..7996cdab8 100644 --- a/src/ui/FormInput.tsx +++ b/src/ui/FormInput.tsx @@ -18,7 +18,7 @@ export const FormInput = React.forwardRef( Date: Wed, 14 Jan 2026 14:27:54 -0800 Subject: [PATCH 04/11] feat: Replace button elements with Button component in various pages --- src/components/AvatarCropModal.tsx | 16 ++++----- src/components/BottomCTA.tsx | 2 +- src/components/Button.tsx | 40 --------------------- src/components/CookieConsent.tsx | 22 +++--------- src/components/CopyMarkdownButton.tsx | 2 +- src/components/CopyPageDropdown.tsx | 17 +++++++-- src/components/DefaultCatchBoundary.tsx | 14 +++----- src/components/FeedbackModerationList.tsx | 18 ++++++---- src/components/LazySponsorSection.tsx | 2 +- src/components/LibraryHero.tsx | 2 +- src/components/MaintainersSection.tsx | 4 +-- src/components/NotFound.tsx | 13 ++----- src/components/PartnersSection.tsx | 4 +-- src/components/RedirectVersionBanner.tsx | 8 +++-- src/components/SearchButton.tsx | 4 ++- src/components/ShowcaseDetail.tsx | 2 +- src/components/ShowcaseGallery.tsx | 2 +- src/components/ShowcaseModerationList.tsx | 26 ++++++++------ src/components/ShowcaseSection.tsx | 14 ++++---- src/components/ShowcaseSubmitForm.tsx | 13 +++---- src/components/SponsorsSection.tsx | 2 +- src/components/ThemeToggle.tsx | 4 +-- src/components/admin/AdminAccessDenied.tsx | 8 ++--- src/components/admin/AdminEmptyState.tsx | 8 ++--- src/components/admin/BannerEditor.tsx | 25 +++++-------- src/components/admin/FeedEntryEditor.tsx | 17 +++------ src/components/admin/FeedSyncStatus.tsx | 7 ++-- src/components/landing/ConfigLanding.tsx | 2 +- src/components/landing/McpLanding.tsx | 8 ++--- src/components/landing/StartLanding.tsx | 2 +- src/components/markdown/CodeBlock.tsx | 4 ++- src/components/markdown/MarkdownContent.tsx | 4 ++- src/routes/$libraryId/$version.index.tsx | 2 +- src/routes/account/index.tsx | 2 +- src/routes/account/integrations.tsx | 3 +- src/routes/account/submissions.tsx | 15 ++++---- src/routes/admin/banners.$id.tsx | 7 ++-- src/routes/admin/banners.index.tsx | 13 ++++--- src/routes/admin/feed.$id.tsx | 7 ++-- src/routes/admin/feed.index.tsx | 13 ++++--- src/routes/admin/feedback_.$id.tsx | 3 +- src/routes/admin/github-stats.tsx | 13 +++---- src/routes/admin/index.tsx | 16 +++------ src/routes/admin/npm-stats.tsx | 13 +++---- src/routes/admin/roles.$roleId.tsx | 20 +++++------ src/routes/admin/roles.index.tsx | 30 ++++++---------- src/routes/admin/showcases_.$id.tsx | 3 +- src/routes/admin/users.tsx | 22 ++++++------ src/routes/blog.tsx | 2 +- src/routes/index.tsx | 6 ++-- src/routes/oauth/authorize.tsx | 2 +- src/routes/paid-support.tsx | 8 ++--- src/routes/partners.tsx | 7 ++-- src/ui/index.ts | 1 + 54 files changed, 223 insertions(+), 301 deletions(-) delete mode 100644 src/components/Button.tsx diff --git a/src/components/AvatarCropModal.tsx b/src/components/AvatarCropModal.tsx index 5a7edf3f6..b40d7c51f 100644 --- a/src/components/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal.tsx @@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog' import Cropper from 'react-easy-crop' import type { Area, Point } from 'react-easy-crop' import { X } from 'lucide-react' +import { Button } from '~/ui' interface AvatarCropModalProps { open: boolean @@ -153,21 +154,20 @@ export function AvatarCropModal({
- - +
diff --git a/src/components/BottomCTA.tsx b/src/components/BottomCTA.tsx index a7b6a8841..7bd49ffed 100644 --- a/src/components/BottomCTA.tsx +++ b/src/components/BottomCTA.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import type { LinkProps } from '@tanstack/react-router' import { Link } from '@tanstack/react-router' -import { Button } from './Button' +import { Button } from '~/ui' type BottomCTAProps = { linkProps: Omit diff --git a/src/components/Button.tsx b/src/components/Button.tsx deleted file mode 100644 index f3e22d9dc..000000000 --- a/src/components/Button.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react' -import { twMerge } from 'tailwind-merge' - -export const buttonStyles = [ - 'flex items-center gap-1.5 rounded-md px-2 py-1.5', - 'border border-gray-200 dark:border-gray-700', - 'hover:bg-gray-100 dark:hover:bg-gray-800', - 'cursor-pointer transition-colors duration-200 text-xs font-medium', -].join(' ') - -type ButtonOwnProps = { - as?: TElement - children: React.ReactNode - className?: string -} - -type ButtonProps = - ButtonOwnProps & - Omit< - React.ComponentPropsWithoutRef, - keyof ButtonOwnProps - > - -type ButtonComponent = ( - props: ButtonProps, -) => React.ReactNode - -export const Button: ButtonComponent = ({ - as, - children, - className, - ...props -}) => { - const Component = as || 'button' - return React.createElement( - Component, - { className: twMerge(buttonStyles, className), ...props }, - children, - ) -} diff --git a/src/components/CookieConsent.tsx b/src/components/CookieConsent.tsx index 0acd24bdb..9feb11f3f 100644 --- a/src/components/CookieConsent.tsx +++ b/src/components/CookieConsent.tsx @@ -1,6 +1,6 @@ import { Link } from '@tanstack/react-router' import { useEffect, useState } from 'react' -import { Button } from './Button' +import { Button } from '~/ui' declare global { interface Window { @@ -184,22 +184,13 @@ export default function CookieConsent() { for details.
- - -
@@ -264,10 +255,7 @@ export default function CookieConsent() {
-
diff --git a/src/components/CopyMarkdownButton.tsx b/src/components/CopyMarkdownButton.tsx index 4e174766e..4754b8e87 100644 --- a/src/components/CopyMarkdownButton.tsx +++ b/src/components/CopyMarkdownButton.tsx @@ -4,7 +4,7 @@ import { useState, useTransition } from 'react' import { type MouseEventHandler, useEffect, useRef } from 'react' import { useToast } from '~/components/ToastProvider' import { Check, Copy } from 'lucide-react' -import { Button } from './Button' +import { Button } from '~/ui' export function useCopyButton( onCopy: () => void | Promise, diff --git a/src/components/CopyPageDropdown.tsx b/src/components/CopyPageDropdown.tsx index 8489ef93e..6a4da6fd2 100644 --- a/src/components/CopyPageDropdown.tsx +++ b/src/components/CopyPageDropdown.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { ChevronDown, Copy, Check } from 'lucide-react' import { useToast } from '~/components/ToastProvider' -import { Button } from './Button' +import { Button } from '~/ui' import { ButtonGroup } from './ButtonGroup' import { Dropdown, @@ -242,7 +242,13 @@ export function CopyPageDropdown({ return ( - - diff --git a/src/components/DefaultCatchBoundary.tsx b/src/components/DefaultCatchBoundary.tsx index 18ed9bd3a..02c2c7600 100644 --- a/src/components/DefaultCatchBoundary.tsx +++ b/src/components/DefaultCatchBoundary.tsx @@ -8,7 +8,7 @@ import { } from '@tanstack/react-router' import * as Sentry from '@sentry/tanstackstart-react' -import { Button } from './Button' +import { Button } from '~/ui' import { useEffect } from 'react' // type DefaultCatchBoundaryType = { @@ -67,26 +67,20 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
{isRoot ? ( - ) : ( - +
)} {isModeratingThis && ( diff --git a/src/components/LazySponsorSection.tsx b/src/components/LazySponsorSection.tsx index 42a080546..2d7915c63 100644 --- a/src/components/LazySponsorSection.tsx +++ b/src/components/LazySponsorSection.tsx @@ -3,7 +3,7 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { ArrowRight } from 'lucide-react' import { useIntersectionObserver } from '~/hooks/useIntersectionObserver' import { getSponsorsForSponsorPack } from '~/server/sponsors' -import { Button } from './Button' +import { Button } from '~/ui' import SponsorPack from './SponsorPack' import PlaceholderSponsorPack from './PlaceholderSponsorPack' diff --git a/src/components/LibraryHero.tsx b/src/components/LibraryHero.tsx index 218b01e89..5c4fd69fd 100644 --- a/src/components/LibraryHero.tsx +++ b/src/components/LibraryHero.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { twMerge } from 'tailwind-merge' import { Link, LinkProps } from '@tanstack/react-router' import type { Library } from '~/libraries' -import { Button } from './Button' +import { Button } from '~/ui' type LibraryHeroProps = { project: Library diff --git a/src/components/MaintainersSection.tsx b/src/components/MaintainersSection.tsx index 2006dd6f7..acd08d805 100644 --- a/src/components/MaintainersSection.tsx +++ b/src/components/MaintainersSection.tsx @@ -1,7 +1,7 @@ import { Link } from '@tanstack/react-router' import { LibraryId } from '~/libraries' import { getLibraryMaintainers } from '~/libraries/maintainers' -import { Button } from './Button' +import { Button } from '~/ui' import { CompactMaintainerCard } from './MaintainerCard' type MaintainersSectionProps = { @@ -33,7 +33,7 @@ export function MaintainersSection({ libraryId }: MaintainersSectionProps) { to="/$libraryId/$version/docs/contributors" params={{ libraryId, version: 'latest' } as never} > - View All Maintainers → + View All Maintainers diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx index 5e0f60264..34c8c5106 100644 --- a/src/components/NotFound.tsx +++ b/src/components/NotFound.tsx @@ -1,6 +1,6 @@ import { Link } from '@tanstack/react-router' import { Ghost } from 'lucide-react' -import { Button } from './Button' +import { Button } from '~/ui' export function NotFound({ children }: { children?: any }) { return ( @@ -13,17 +13,10 @@ export function NotFound({ children }: { children?: any }) {

The page you are looking for does not exist.

{children || (

- -

diff --git a/src/components/PartnersSection.tsx b/src/components/PartnersSection.tsx index 5e7c6fe30..effa88b64 100644 --- a/src/components/PartnersSection.tsx +++ b/src/components/PartnersSection.tsx @@ -4,7 +4,7 @@ import { partners } from '~/utils/partners' import { PartnersGrid } from './PartnersGrid' import { PartnershipCallout } from './PartnershipCallout' import { LibraryId } from '~/libraries' -import { Button } from './Button' +import { Button } from '~/ui' type PartnersSectionProps = { libraryId?: LibraryId @@ -45,7 +45,7 @@ export function PartnersSection({ : { status: 'inactive' }) as any } > - View Previous Partners → + View Previous Partners ) : null} diff --git a/src/components/RedirectVersionBanner.tsx b/src/components/RedirectVersionBanner.tsx index bf9560d21..6ca077dbe 100644 --- a/src/components/RedirectVersionBanner.tsx +++ b/src/components/RedirectVersionBanner.tsx @@ -1,7 +1,7 @@ import { useLocalStorage } from '~/utils/useLocalStorage' import { useMounted } from '~/hooks/useMounted' import { Link, useMatches } from '@tanstack/react-router' -import { Button } from './Button' +import { Button } from '~/ui' export function RedirectVersionBanner(props: { version: string @@ -43,13 +43,15 @@ export function RedirectVersionBanner(props: { to={activeMatch.fullPath} params={{ version: 'latest' } as never} replace - className="bg-black border-black hover:bg-gray-800 dark:bg-white dark:border-white dark:hover:bg-gray-200 dark:text-black text-white w-full lg:w-auto justify-center" + variant="secondary" + className="w-full lg:w-auto justify-center" > Latest diff --git a/src/components/SearchButton.tsx b/src/components/SearchButton.tsx index 29f1d0c79..78b98e364 100644 --- a/src/components/SearchButton.tsx +++ b/src/components/SearchButton.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Command, Search } from 'lucide-react' import { twMerge } from 'tailwind-merge' import { useSearchContext } from '~/contexts/SearchContext' -import { Button } from './Button' +import { Button } from '~/ui' interface SearchButtonProps { className?: string @@ -14,6 +14,8 @@ export function SearchButton({ className }: SearchButtonProps) { return ( + )} {showcase.status !== 'denied' && ( - + )} - + )}
diff --git a/src/components/ShowcaseSection.tsx b/src/components/ShowcaseSection.tsx index a768e7201..0ab0485af 100644 --- a/src/components/ShowcaseSection.tsx +++ b/src/components/ShowcaseSection.tsx @@ -8,7 +8,7 @@ import { } from '~/queries/showcases' import { voteShowcase } from '~/utils/showcase.functions' import { ShowcaseCard, ShowcaseCardSkeleton } from './ShowcaseCard' -import { buttonStyles } from './Button' +import { Button } from '~/ui' import { ArrowRight, Plus } from 'lucide-react' import { useCurrentUser } from '~/hooks/useCurrentUser' import { useLoginModal } from '~/contexts/LoginModalContext' @@ -232,13 +232,11 @@ export function ShowcaseSection({ {showViewAll && (
- - View all projects - + +
)} diff --git a/src/components/ShowcaseSubmitForm.tsx b/src/components/ShowcaseSubmitForm.tsx index 77c79e086..e98b20faa 100644 --- a/src/components/ShowcaseSubmitForm.tsx +++ b/src/components/ShowcaseSubmitForm.tsx @@ -13,9 +13,8 @@ import { } from '~/utils/showcase.client' import { useToast } from './ToastProvider' import { Check, AlertCircle } from 'lucide-react' -import { Button } from './Button' +import { Button, FormInput } from '~/ui' import { ImageUpload } from './ImageUpload' -import { FormInput } from '~/ui' import { FormEvent, useMemo, useState } from 'react' // Filter to only show libraries with proper configuration @@ -35,9 +34,7 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { const [name, setName] = useState(showcase?.name ?? '') const [tagline, setTagline] = useState(showcase?.tagline ?? '') - const [description, setDescription] = useState( - showcase?.description ?? '', - ) + const [description, setDescription] = useState(showcase?.description ?? '') const [url, setUrl] = useState(showcase?.url ?? '') const [logoUrl, setLogoUrl] = useState( showcase?.logoUrl ?? undefined, @@ -48,9 +45,9 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) { const [selectedLibraries, setSelectedLibraries] = useState( showcase?.libraries ?? [], ) - const [selectedUseCases, setSelectedUseCases] = useState< - ShowcaseUseCase[] - >(showcase?.useCases ?? []) + const [selectedUseCases, setSelectedUseCases] = useState( + showcase?.useCases ?? [], + ) const [isOpenSource, setIsOpenSource] = useState(!!showcase?.sourceUrl) const [sourceUrl, setSourceUrl] = useState(showcase?.sourceUrl ?? '') diff --git a/src/components/SponsorsSection.tsx b/src/components/SponsorsSection.tsx index bc681b545..837466dce 100644 --- a/src/components/SponsorsSection.tsx +++ b/src/components/SponsorsSection.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { Await } from '@tanstack/react-router' import { Spinner } from '~/components/Spinner' import SponsorPack from '~/components/SponsorPack' -import { Button } from '~/components/Button' +import { Button } from '~/ui' import { twMerge } from 'tailwind-merge' type SponsorsSectionProps = { diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 2b3bad032..a0e894a08 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { useTheme } from './ThemeProvider' import { Moon, Sun, SunMoon } from 'lucide-react' -import { Button } from './Button' +import { Button } from '~/ui' export function ThemeToggle() { const { themeMode, toggleMode } = useTheme() @@ -16,7 +16,7 @@ export function ThemeToggle() { themeMode === 'auto' ? 'Auto' : themeMode === 'light' ? 'Light' : 'Dark' return ( - diff --git a/src/components/admin/AdminEmptyState.tsx b/src/components/admin/AdminEmptyState.tsx index 84d9aa0a8..6fa2cc84a 100644 --- a/src/components/admin/AdminEmptyState.tsx +++ b/src/components/admin/AdminEmptyState.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { Link } from '@tanstack/react-router' +import { Button } from '~/ui' type AdminEmptyStateProps = { icon: React.ReactNode @@ -32,11 +33,8 @@ export function AdminEmptyState({ )} {action && (
- - {action.label} + +
)} diff --git a/src/components/admin/BannerEditor.tsx b/src/components/admin/BannerEditor.tsx index 3fb488a5a..4827535d2 100644 --- a/src/components/admin/BannerEditor.tsx +++ b/src/components/admin/BannerEditor.tsx @@ -22,7 +22,7 @@ import { Gift, ExternalLink, } from 'lucide-react' -import { FormInput } from '~/ui' +import { FormInput, Button } from '~/ui' interface BannerEditorProps { banner: BannerWithMeta | null @@ -204,21 +204,14 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) {

- - + +
@@ -494,18 +487,18 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) { focusRing="purple" className="flex-1 px-3 py-2 text-sm" /> - + {/* Selected paths */} diff --git a/src/components/admin/FeedEntryEditor.tsx b/src/components/admin/FeedEntryEditor.tsx index f7a77cc06..8705ad5db 100644 --- a/src/components/admin/FeedEntryEditor.tsx +++ b/src/components/admin/FeedEntryEditor.tsx @@ -18,7 +18,7 @@ import { Calendar, Check, } from 'lucide-react' -import { FormInput } from '~/ui' +import { FormInput, Button } from '~/ui' interface FeedEntryEditorProps { entry: FeedEntry | null @@ -171,21 +171,14 @@ export function FeedEntryEditor({

- - + +
diff --git a/src/components/admin/FeedSyncStatus.tsx b/src/components/admin/FeedSyncStatus.tsx index ec3cd52cf..6d13f3841 100644 --- a/src/components/admin/FeedSyncStatus.tsx +++ b/src/components/admin/FeedSyncStatus.tsx @@ -8,6 +8,7 @@ import { } from '~/utils/admin' import { Spinner } from '~/components/Spinner' import { RefreshCw } from 'lucide-react' +import { Button } from '~/ui' export function FeedSyncStatus({ onSyncComplete, @@ -63,10 +64,10 @@ export function FeedSyncStatus({

Sync Status

- +
diff --git a/src/components/landing/ConfigLanding.tsx b/src/components/landing/ConfigLanding.tsx index 6cdd0d75a..7e2fc16d9 100644 --- a/src/components/landing/ConfigLanding.tsx +++ b/src/components/landing/ConfigLanding.tsx @@ -6,7 +6,7 @@ import { configProject } from '~/libraries/config' import { getLibrary } from '~/libraries' import { LibraryFeatureHighlights } from '~/components/LibraryFeatureHighlights' import LandingPageGad from '~/components/LandingPageGad' -import { Button } from '~/components/Button' +import { Button } from '~/ui' import { PartnersSection } from '~/components/PartnersSection' import { MaintainersSection } from '~/components/MaintainersSection' import { CheckCircleIcon } from '~/components/icons/CheckCircleIcon' diff --git a/src/components/landing/McpLanding.tsx b/src/components/landing/McpLanding.tsx index c9cfb3dd4..801d9fcaa 100644 --- a/src/components/landing/McpLanding.tsx +++ b/src/components/landing/McpLanding.tsx @@ -7,7 +7,7 @@ import { mcpProject } from '~/libraries/mcp' import { getLibrary } from '~/libraries' import { LibraryFeatureHighlights } from '~/components/LibraryFeatureHighlights' import LandingPageGad from '~/components/LandingPageGad' -import { Button } from '~/components/Button' +import { Button } from '~/ui' import { MaintainersSection } from '~/components/MaintainersSection' import { LibraryPageContainer } from '~/components/LibraryPageContainer' import { Key } from 'lucide-react' @@ -23,11 +23,7 @@ export default function McpLanding() { project={mcpProject} actions={ <> - {canApiKeys && ( diff --git a/src/components/landing/StartLanding.tsx b/src/components/landing/StartLanding.tsx index c4e0f9d8d..6f3ab38df 100644 --- a/src/components/landing/StartLanding.tsx +++ b/src/components/landing/StartLanding.tsx @@ -10,7 +10,7 @@ import LandingPageGad from '~/components/LandingPageGad' import { PartnersSection } from '~/components/PartnersSection' import { MaintainersSection } from '~/components/MaintainersSection' import { LibraryTestimonials } from '~/components/LibraryTestimonials' -import { Button } from '~/components/Button' +import { Button } from '~/ui' import { GithubIcon } from '~/components/icons/GithubIcon' import { Book, Wallpaper } from 'lucide-react' import { BrandXIcon } from '~/components/icons/BrandXIcon' diff --git a/src/components/markdown/CodeBlock.tsx b/src/components/markdown/CodeBlock.tsx index e73762db7..93473e61f 100644 --- a/src/components/markdown/CodeBlock.tsx +++ b/src/components/markdown/CodeBlock.tsx @@ -5,7 +5,7 @@ import { Copy } from 'lucide-react' import type { Mermaid } from 'mermaid' import { transformerNotationDiff } from '@shikijs/transformers' import { createHighlighter, type HighlighterGeneric } from 'shiki' -import { Button } from '../Button' +import { Button } from '~/ui' // Language aliases mapping const LANG_ALIASES: Record = { @@ -221,6 +221,8 @@ export function CodeBlock({ - - - Edit + + + ) diff --git a/src/routes/admin/banners.index.tsx b/src/routes/admin/banners.index.tsx index aa94f0c29..23265fbe4 100644 --- a/src/routes/admin/banners.index.tsx +++ b/src/routes/admin/banners.index.tsx @@ -21,6 +21,7 @@ import { ExternalLink, Flag, } from 'lucide-react' +import { Button } from '~/ui' import * as v from 'valibot' import { formatDistanceToNow } from '~/utils/dates' import { @@ -125,13 +126,11 @@ function BannersAdminPage() { title="Banner Management" isLoading={bannersQuery.isLoading} actions={ - - - Create Banner + + } /> diff --git a/src/routes/admin/feed.$id.tsx b/src/routes/admin/feed.$id.tsx index 86fdef6b8..6349ed600 100644 --- a/src/routes/admin/feed.$id.tsx +++ b/src/routes/admin/feed.$id.tsx @@ -6,6 +6,7 @@ import { useCapabilities } from '~/hooks/useCapabilities' import { useCurrentUserQuery } from '~/hooks/useCurrentUser' import { hasCapability } from '~/db/types' import { getFeedEntryQueryOptions } from '~/queries/feed' +import { Button } from '~/ui' import * as v from 'valibot' export const Route = createFileRoute('/admin/feed/$id')({ component: FeedEditorPage, @@ -60,12 +61,12 @@ function FeedEditorPage() {

Entry Not Found

- +
) diff --git a/src/routes/admin/feed.index.tsx b/src/routes/admin/feed.index.tsx index a3ee61ffc..37b4639f9 100644 --- a/src/routes/admin/feed.index.tsx +++ b/src/routes/admin/feed.index.tsx @@ -6,6 +6,7 @@ import { } from '~/utils/mutations' import * as v from 'valibot' import { Plus } from 'lucide-react' +import { Button } from '~/ui' import { FeedEntry } from '~/components/FeedEntry' import { FeedSyncStatus } from '~/components/admin/FeedSyncStatus' import { FeedPage as FeedPageComponent } from '~/components/FeedPage' @@ -121,13 +122,11 @@ function FeedAdminPage() {

Feed Admin

- - - Create Entry + +
diff --git a/src/routes/admin/feedback_.$id.tsx b/src/routes/admin/feedback_.$id.tsx index f788b6051..b42cde6e5 100644 --- a/src/routes/admin/feedback_.$id.tsx +++ b/src/routes/admin/feedback_.$id.tsx @@ -17,8 +17,7 @@ import { AlertTriangle, } from 'lucide-react' import { Card } from '~/components/Card' -import { Button } from '~/components/Button' -import { Badge } from '~/ui' +import { Badge, Button } from '~/ui' import { format } from '~/utils/dates' export const Route = createFileRoute('/admin/feedback_/$id')({ diff --git a/src/routes/admin/github-stats.tsx b/src/routes/admin/github-stats.tsx index 32ce9d0e4..cead57142 100644 --- a/src/routes/admin/github-stats.tsx +++ b/src/routes/admin/github-stats.tsx @@ -25,6 +25,7 @@ import { formatDistanceToNow } from '~/utils/dates' import { GithubIcon } from '~/components/icons/GithubIcon' import { Box, RefreshCw, Star, Users } from 'lucide-react' import { Card } from '~/components/Card' +import { Button } from '~/ui' type GitHubStatsEntry = { cacheKey: string @@ -283,17 +284,17 @@ function GitHubStatsAdmin() { cell: ({ row }) => { const isRefreshing = refreshingKey === row.original.cacheKey return ( - + ) }, }, @@ -323,16 +324,16 @@ function GitHubStatsAdmin() {

{cacheEntries && cacheEntries.length > 0 && ( - + )}
diff --git a/src/routes/admin/index.tsx b/src/routes/admin/index.tsx index f110cf5db..dc2c4a6a4 100644 --- a/src/routes/admin/index.tsx +++ b/src/routes/admin/index.tsx @@ -26,7 +26,7 @@ import { Users, } from 'lucide-react' import { Card } from '~/components/Card' -import { Badge } from '~/ui' +import { Badge, Button } from '~/ui' import * as v from 'valibot' import { TimeSeriesChart } from '~/components/charts/TimeSeriesChart' import { ChartControls } from '~/components/charts/ChartControls' @@ -74,11 +74,8 @@ function AdminPage() {

You don't have permission to access the admin area.

- - Back to Home + + @@ -483,11 +480,8 @@ function UsersTab({ View and manage individual user accounts, roles, and capabilities.

- - Manage Users + + diff --git a/src/routes/admin/npm-stats.tsx b/src/routes/admin/npm-stats.tsx index 5b3418909..2f417b2d0 100644 --- a/src/routes/admin/npm-stats.tsx +++ b/src/routes/admin/npm-stats.tsx @@ -27,6 +27,7 @@ import { formatDistanceToNow } from '~/utils/dates' import { Download, RefreshCw } from 'lucide-react' import { NpmIcon } from '~/components/icons/NpmIcon' import { Card } from '~/components/Card' +import { Button } from '~/ui' type NpmPackage = { id: string @@ -249,18 +250,18 @@ function NpmStatsAdmin() { id: 'actions', header: 'Actions', cell: ({ row }) => ( - + ), }, ], @@ -304,13 +305,13 @@ function NpmStatsAdmin() { rebuild all caches.

- {/* Org Stats Section - Top of Page */} diff --git a/src/routes/admin/roles.$roleId.tsx b/src/routes/admin/roles.$roleId.tsx index 983698992..a764d38a6 100644 --- a/src/routes/admin/roles.$roleId.tsx +++ b/src/routes/admin/roles.$roleId.tsx @@ -13,7 +13,7 @@ import { import { ArrowLeft, Lock, Trash, User, Users } from 'lucide-react' import { requireCapability } from '~/utils/auth.server' import { hasCapability } from '~/db/types' -import { Badge } from '~/ui' +import { Badge, Button } from '~/ui' export const Route = createFileRoute('/admin/roles/$roleId')({ beforeLoad: async () => { @@ -298,7 +298,7 @@ function RoleDetailPage() { {selectedUserIds.size} user(s) selected - + )} @@ -325,13 +325,13 @@ function RoleDetailPage() { Remove {confirmRemove.name} from role "{role?.name}"?

- - +
diff --git a/src/routes/admin/roles.index.tsx b/src/routes/admin/roles.index.tsx index 35c4cc4ab..8ce88aac3 100644 --- a/src/routes/admin/roles.index.tsx +++ b/src/routes/admin/roles.index.tsx @@ -41,7 +41,7 @@ import { useAdminGuard } from '~/hooks/useAdminGuard' import { requireCapability } from '~/utils/auth.server' import { useToggleArray } from '~/hooks/useToggleArray' import { useDeleteWithConfirmation } from '~/hooks/useDeleteWithConfirmation' -import { Badge, FormInput } from '~/ui' +import { Badge, Button, FormInput } from '~/ui' // Role type for table - matches the shape returned by listRoles interface Role { @@ -422,13 +422,10 @@ function RolesPage() { isLoading={rolesQuery.isFetching} actions={ !isCreating && ( - + ) } /> @@ -458,14 +455,15 @@ function RolesPage() { ))} - + } /> @@ -531,20 +529,14 @@ function RolesPage() {
- - + +
diff --git a/src/routes/admin/showcases_.$id.tsx b/src/routes/admin/showcases_.$id.tsx index eea03c0d7..a63ebfded 100644 --- a/src/routes/admin/showcases_.$id.tsx +++ b/src/routes/admin/showcases_.$id.tsx @@ -28,8 +28,7 @@ import { RotateCcw, } from 'lucide-react' import { Card } from '~/components/Card' -import { Button } from '~/components/Button' -import { Badge, FormInput } from '~/ui' +import { Badge, Button, FormInput } from '~/ui' import { ImageUpload } from '~/components/ImageUpload' import { format } from '~/utils/dates' import { diff --git a/src/routes/admin/users.tsx b/src/routes/admin/users.tsx index f5188a607..8530faff9 100644 --- a/src/routes/admin/users.tsx +++ b/src/routes/admin/users.tsx @@ -47,7 +47,7 @@ import { useAdminGuard } from '~/hooks/useAdminGuard' import { useToggleArray } from '~/hooks/useToggleArray' import { handleAdminError } from '~/utils/adminErrors' import { requireCapability } from '~/utils/auth.server' -import { Badge } from '~/ui' +import { Badge, Button } from '~/ui' // User type for table - matches the shape returned by listUsers type User = { @@ -835,13 +835,13 @@ function UsersPage() { ))} - +
diff --git a/src/routes/blog.tsx b/src/routes/blog.tsx index 0f30ebf28..6f4bf25dd 100644 --- a/src/routes/blog.tsx +++ b/src/routes/blog.tsx @@ -1,6 +1,6 @@ import { Link, createFileRoute } from '@tanstack/react-router' import { seo } from '~/utils/seo' -import { Button } from '~/components/Button' +import { Button } from '~/ui' export const Route = createFileRoute('/blog')({ head: () => ({ diff --git a/src/routes/index.tsx b/src/routes/index.tsx index 504cbe3f6..f2e9a7a5f 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -28,7 +28,7 @@ import { ArrowRight, Code2, Layers, Shield, Zap } from 'lucide-react' import { Card } from '~/components/Card' import LibraryCard from '~/components/LibraryCard' import { FeaturedShowcases } from '~/components/ShowcaseSection' -import { Button } from '~/components/Button' +import { Button } from '~/ui' export const textColors = [ `text-rose-500`, @@ -295,7 +295,7 @@ function Index() {
@@ -534,7 +534,7 @@ function Index() {
diff --git a/src/routes/oauth/authorize.tsx b/src/routes/oauth/authorize.tsx index ec9d22f2d..be10c0d1b 100644 --- a/src/routes/oauth/authorize.tsx +++ b/src/routes/oauth/authorize.tsx @@ -4,7 +4,7 @@ import * as v from 'valibot' import { createAuthorizationCode } from '~/utils/oauthClient.functions' import { getCurrentUser } from '~/utils/auth.server' import { Card } from '~/components/Card' -import { Button } from '~/components/Button' +import { Button } from '~/ui' import { useIsDark } from '~/hooks/useIsDark' import { BrandContextMenu } from '~/components/BrandContextMenu' diff --git a/src/routes/paid-support.tsx b/src/routes/paid-support.tsx index 682fd8890..39d5f37b4 100644 --- a/src/routes/paid-support.tsx +++ b/src/routes/paid-support.tsx @@ -8,6 +8,7 @@ import { MaintainerRowCard, } from '~/components/MaintainerCard' import { Grid2x2, Grid3X3, LayoutList, Mail } from 'lucide-react' +import { Button } from '~/ui' export const Route = createFileRoute('/paid-support')({ component: PaidSupportComp, @@ -136,11 +137,8 @@ function PaidSupportComp() { We also offer professional workshops on TanStack libraries, delivered remotely or in-person by our creators and maintainers.

- - Learn More About Workshops + + diff --git a/src/routes/partners.tsx b/src/routes/partners.tsx index 146ae0e95..f62c65b5b 100644 --- a/src/routes/partners.tsx +++ b/src/routes/partners.tsx @@ -7,6 +7,7 @@ import { Library } from '~/libraries' import { useState } from 'react' import * as React from 'react' import { ListFilter, X } from 'lucide-react' +import { Button } from '~/ui' import { startProject } from '~/libraries/start' import { routerProject } from '~/libraries/router' import { queryProject } from '~/libraries/query' @@ -482,12 +483,12 @@ function RouteComp() {

No partners found for the selected filters.

- + ) : (

No partners to display.

diff --git a/src/ui/index.ts b/src/ui/index.ts index 7858ac67d..f341db8fc 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,4 +1,5 @@ export { Badge } from './Badge' +export { Button } from './Button' export { FormInput } from './FormInput' export { InlineCode } from './InlineCode' export { MarkdownImg } from './MarkdownImg' From 441cba08a76fa994abcc433035e89535664d32c6 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 14:28:47 -0800 Subject: [PATCH 05/11] feat: Add Button component with customizable styles and variants --- src/ui/Button.tsx | 141 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 src/ui/Button.tsx diff --git a/src/ui/Button.tsx b/src/ui/Button.tsx new file mode 100644 index 000000000..968ca934a --- /dev/null +++ b/src/ui/Button.tsx @@ -0,0 +1,141 @@ +import * as React from 'react' +import { twMerge } from 'tailwind-merge' + +type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'icon' + +type ButtonColor = + | 'blue' + | 'green' + | 'red' + | 'orange' + | 'purple' + | 'gray' + | 'emerald' + | 'cyan' + | 'yellow' + +type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon-md' + +type ButtonRounded = 'none' | 'md' | 'lg' | 'full' + +type ButtonOwnProps = { + as?: TElement + children: React.ReactNode + variant?: ButtonVariant + color?: ButtonColor + size?: ButtonSize + rounded?: ButtonRounded + className?: string +} + +type ButtonProps = + ButtonOwnProps & + Omit< + React.ComponentPropsWithoutRef, + keyof ButtonOwnProps + > + +type ButtonComponent = ( + props: ButtonProps, +) => React.ReactNode + +const primaryColorStyles: Record = { + blue: 'bg-blue-600 text-white border-blue-600 hover:bg-blue-700', + green: 'bg-green-600 text-white border-green-600 hover:bg-green-700', + red: 'bg-red-600 text-white border-red-600 hover:bg-red-700', + orange: 'bg-orange-600 text-white border-orange-600 hover:bg-orange-700', + purple: 'bg-purple-600 text-white border-purple-600 hover:bg-purple-700', + gray: 'bg-gray-600 text-white border-gray-600 hover:bg-gray-700', + emerald: 'bg-emerald-500 text-white border-emerald-500 hover:bg-emerald-600', + cyan: 'bg-cyan-500 text-white border-cyan-500 hover:bg-cyan-600', + yellow: 'bg-yellow-400 text-black border-yellow-400 hover:bg-yellow-500', +} + +const iconColorStyles: Record = { + blue: 'text-blue-600 hover:bg-blue-100 dark:hover:bg-blue-900/30', + green: 'text-green-600 hover:bg-green-100 dark:hover:bg-green-900/30', + red: 'text-red-600 hover:bg-red-100 dark:hover:bg-red-900/30', + orange: 'text-orange-600 hover:bg-orange-100 dark:hover:bg-orange-900/30', + purple: 'text-purple-600 hover:bg-purple-100 dark:hover:bg-purple-900/30', + gray: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700', + emerald: 'text-emerald-600 hover:bg-emerald-100 dark:hover:bg-emerald-900/30', + cyan: 'text-cyan-600 hover:bg-cyan-100 dark:hover:bg-cyan-900/30', + yellow: 'text-yellow-600 hover:bg-yellow-100 dark:hover:bg-yellow-900/30', +} + +const variantStyles: Record = { + primary: 'border font-medium', + secondary: + 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-700 border-transparent font-medium', + ghost: + 'border border-gray-200 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 font-medium', + icon: 'border-transparent', +} + +const sizeStyles: Record = { + xs: 'px-2 py-1.5 text-xs', + sm: 'px-3 py-1 text-sm', + md: 'px-4 py-2 text-sm', + lg: 'px-6 py-3 text-base', + 'icon-sm': 'p-1.5', + 'icon-md': 'p-2', +} + +const roundedStyles: Record = { + none: 'rounded-none', + md: 'rounded-md', + lg: 'rounded-lg', + full: 'rounded-full', +} + +const baseStyles = + 'inline-flex items-center justify-center gap-2 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed' + +function getDefaultSize(variant: ButtonVariant): ButtonSize { + if (variant === 'icon') return 'icon-md' + return 'md' +} + +function getDefaultRounded(size: ButtonSize): ButtonRounded { + if (size === 'xs' || size === 'sm') return 'md' + if (size === 'icon-sm' || size === 'icon-md') return 'lg' + return 'lg' +} + +export const Button: ButtonComponent = ({ + as, + children, + variant = 'primary', + color = 'blue', + size, + rounded, + className, + ...props +}) => { + const Component = as || 'button' + const resolvedSize = size ?? getDefaultSize(variant) + const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize) + + const colorStyles = + variant === 'primary' + ? primaryColorStyles[color] + : variant === 'icon' + ? iconColorStyles[color] + : '' + + return React.createElement( + Component, + { + className: twMerge( + baseStyles, + variantStyles[variant], + sizeStyles[resolvedSize], + roundedStyles[resolvedRounded], + colorStyles, + className, + ), + ...props, + }, + children, + ) +} From 394bc10796402da048ac8942857a47a058af1847 Mon Sep 17 00:00:00 2001 From: Lynn Fisher Date: Wed, 14 Jan 2026 15:32:25 -0700 Subject: [PATCH 06/11] Change ui.dev to Fireship (#654) * change ui.dev to Fireship * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/components/DocsCalloutBytes.tsx | 4 +- src/components/DocsLayout.tsx | 2 +- src/components/LogoQueryGG.tsx | 221 +++++++++++++++++++++++++++ src/components/LogoQueryGGSmall.tsx | 228 ++++++++++++++++++++++++++++ src/images/bytes-fireship.png | Bin 0 -> 6638 bytes src/images/bytes-uidotdev.png | Bin 7048 -> 0 bytes src/routes/learn.tsx | 2 +- src/utils/gh-sponsor-meta.json | 6 +- src/utils/partners.tsx | 16 +- 9 files changed, 464 insertions(+), 15 deletions(-) create mode 100644 src/components/LogoQueryGG.tsx create mode 100644 src/components/LogoQueryGGSmall.tsx create mode 100644 src/images/bytes-fireship.png delete mode 100644 src/images/bytes-uidotdev.png diff --git a/src/components/DocsCalloutBytes.tsx b/src/components/DocsCalloutBytes.tsx index 2fdeaa4a9..5fd6945bd 100644 --- a/src/components/DocsCalloutBytes.tsx +++ b/src/components/DocsCalloutBytes.tsx @@ -8,8 +8,8 @@ export function DocsCalloutBytes(props: React.HTMLProps) { Subscribe to Bytes

- Your weekly dose of JavaScript news. Delivered every Monday to over - 100,000 devs, for free. + Your weekly dose of JavaScript news. Delivered every Tuesday and + Friday to over 200,000 devs, for free.

diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 6eadd3e1f..6b2b028d9 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -789,7 +789,7 @@ export function DocsLayout({ {activePartners - .filter((d) => d.id !== 'ui-dev') + .filter((d) => d.id !== 'fireship') .map((partner) => { // flexBasis as percentage based on score, flexGrow to fill remaining row space const widthPercent = Math.round(partner.score * 100) diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx new file mode 100644 index 000000000..bfc3c6070 --- /dev/null +++ b/src/components/LogoQueryGG.tsx @@ -0,0 +1,221 @@ +export function LogoQueryGG(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx new file mode 100644 index 000000000..56d00f537 --- /dev/null +++ b/src/components/LogoQueryGGSmall.tsx @@ -0,0 +1,228 @@ +export function LogoQueryGGSmall(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/images/bytes-fireship.png b/src/images/bytes-fireship.png new file mode 100644 index 0000000000000000000000000000000000000000..96e1f3b29c0734332a5202175a8917be3e4f1cfd GIT binary patch literal 6638 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 BZ^Zxr literal 0 HcmV?d00001 diff --git a/src/images/bytes-uidotdev.png b/src/images/bytes-uidotdev.png deleted file mode 100644 index 171332d150ca3e38d813ef6fdd16d356ab36c7d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7048 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 diff --git a/src/routes/learn.tsx b/src/routes/learn.tsx index dafbf96da..c490ec743 100644 --- a/src/routes/learn.tsx +++ b/src/routes/learn.tsx @@ -78,7 +78,7 @@ function LearnPage() {
Created by{' '} Dominik Dorfmeister and{' '} - ui.dev + Fireship
diff --git a/src/utils/gh-sponsor-meta.json b/src/utils/gh-sponsor-meta.json index 753a969d6..aa98dc874 100644 --- a/src/utils/gh-sponsor-meta.json +++ b/src/utils/gh-sponsor-meta.json @@ -250,9 +250,9 @@ "linkUrl": "https://tripwire.com/?utm_source=tanstack" }, { - "login": "uidotdev", - "name": "ui.dev", - "linkUrl": "https://ui.dev/react-query?from=tanstack" + "login": "fireship-io", + "name": "Fireship", + "linkUrl": "https://fireship.dev/react-query?from=tanstack" }, { "login": "perscharbel", diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 4d0a1b304..91362b7b1 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -1,7 +1,7 @@ import agGridDarkSvg from '~/images/ag-grid-dark.svg' import agGridLightSvg from '~/images/ag-grid-light.svg' import nozzleImage from '~/images/nozzle.png' -import bytesUidotdevImage from '~/images/bytes-uidotdev.png' +import bytesFireshipImage from '~/images/bytes-fireship.png' import vercelLightSvg from '~/images/vercel-light.svg' import vercelDarkSvg from '~/images/vercel-dark.svg' import netlifyLightSvg from '~/images/netlify-light.svg' @@ -443,19 +443,19 @@ const sentry = (() => { } })() -const uiDev = (() => { +const fireship = (() => { const href = 'https://bytes.dev?utm_source-tanstack&utm_campaign=tanstack' return { - name: 'UI.dev', - id: 'ui-dev', + name: 'Fireship', + id: 'fireship', libraries: [], status: 'active' as const, score: 0.014, href, tagline: 'Dev Education', image: { - src: bytesUidotdevImage, + src: bytesFireshipImage, }, llmDescription: 'Educational platform and Bytes.dev newsletter. Official learning resources and news partner for the TanStack ecosystem.', @@ -472,14 +472,14 @@ const uiDev = (() => { className="text-blue-500 underline cursor-pointer p-0 m-0 bg-transparent border-none inline" onClick={() => window.open( - 'https://ui.dev/?utm_source=tanstack&utm_campaign=tanstack', + 'https://fireship.dev/?utm_source=tanstack&utm_campaign=tanstack', '_blank', 'noopener,noreferrer', ) } tabIndex={0} > - ui.dev + Fireship {' '} to provide best-in-class education about TanStack products. It doesn't stop at TanStack though, with their sister @@ -842,7 +842,7 @@ export const partners: Partner[] = [ prisma, strapi, unkey, - uiDev, + fireship, nozzle, vercel, speakeasy, From 7778f5b55a018d1a42007ef3fe9570931b8e5d72 Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 11:50:09 -0800 Subject: [PATCH 07/11] wip: Introduce FormInput and Badge components; refactor existing components to use them --- src/components/LogoQueryGG.tsx | 221 --------------------------- src/components/LogoQueryGGSmall.tsx | 228 ---------------------------- 2 files changed, 449 deletions(-) delete mode 100644 src/components/LogoQueryGG.tsx delete mode 100644 src/components/LogoQueryGGSmall.tsx diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx deleted file mode 100644 index bfc3c6070..000000000 --- a/src/components/LogoQueryGG.tsx +++ /dev/null @@ -1,221 +0,0 @@ -export function LogoQueryGG(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx deleted file mode 100644 index 56d00f537..000000000 --- a/src/components/LogoQueryGGSmall.tsx +++ /dev/null @@ -1,228 +0,0 @@ -export function LogoQueryGGSmall(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} From b6efd596012e83b9ab517e13493db272e7f70962 Mon Sep 17 00:00:00 2001 From: Lynn Fisher Date: Wed, 14 Jan 2026 15:32:25 -0700 Subject: [PATCH 08/11] Change ui.dev to Fireship (#654) * change ui.dev to Fireship * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/components/LogoQueryGG.tsx | 221 +++++++++++++++++++++++++++ src/components/LogoQueryGGSmall.tsx | 228 ++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 src/components/LogoQueryGG.tsx create mode 100644 src/components/LogoQueryGGSmall.tsx diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx new file mode 100644 index 000000000..bfc3c6070 --- /dev/null +++ b/src/components/LogoQueryGG.tsx @@ -0,0 +1,221 @@ +export function LogoQueryGG(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx new file mode 100644 index 000000000..56d00f537 --- /dev/null +++ b/src/components/LogoQueryGGSmall.tsx @@ -0,0 +1,228 @@ +export function LogoQueryGGSmall(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} From 9c7075d1e272131875cd5cd44fb0b74a0ae74efb Mon Sep 17 00:00:00 2001 From: ladybluenotes Date: Wed, 14 Jan 2026 11:50:09 -0800 Subject: [PATCH 09/11] wip: Introduce FormInput and Badge components; refactor existing components to use them --- src/components/LogoQueryGG.tsx | 221 --------------------------- src/components/LogoQueryGGSmall.tsx | 228 ---------------------------- 2 files changed, 449 deletions(-) delete mode 100644 src/components/LogoQueryGG.tsx delete mode 100644 src/components/LogoQueryGGSmall.tsx diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx deleted file mode 100644 index bfc3c6070..000000000 --- a/src/components/LogoQueryGG.tsx +++ /dev/null @@ -1,221 +0,0 @@ -export function LogoQueryGG(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx deleted file mode 100644 index 56d00f537..000000000 --- a/src/components/LogoQueryGGSmall.tsx +++ /dev/null @@ -1,228 +0,0 @@ -export function LogoQueryGGSmall(props: React.HTMLProps) { - return ( -
- - Query.gg - The Official React Query Course - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ) -} From 9fc1139312c4041bccc9da856371cdd85bc95b80 Mon Sep 17 00:00:00 2001 From: Lynn Fisher Date: Wed, 14 Jan 2026 15:32:25 -0700 Subject: [PATCH 10/11] Change ui.dev to Fireship (#654) * change ui.dev to Fireship * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/components/LogoQueryGG.tsx | 221 +++++++++++++++++++++++++++ src/components/LogoQueryGGSmall.tsx | 228 ++++++++++++++++++++++++++++ 2 files changed, 449 insertions(+) create mode 100644 src/components/LogoQueryGG.tsx create mode 100644 src/components/LogoQueryGGSmall.tsx diff --git a/src/components/LogoQueryGG.tsx b/src/components/LogoQueryGG.tsx new file mode 100644 index 000000000..bfc3c6070 --- /dev/null +++ b/src/components/LogoQueryGG.tsx @@ -0,0 +1,221 @@ +export function LogoQueryGG(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} diff --git a/src/components/LogoQueryGGSmall.tsx b/src/components/LogoQueryGGSmall.tsx new file mode 100644 index 000000000..56d00f537 --- /dev/null +++ b/src/components/LogoQueryGGSmall.tsx @@ -0,0 +1,228 @@ +export function LogoQueryGGSmall(props: React.HTMLProps) { + return ( +
+ + Query.gg - The Official React Query Course + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ) +} From b161fa8be267219f222d1c582b09f486b22457f1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 23:03:57 +0000 Subject: [PATCH 11/11] ci: apply automated fixes --- src/components/markdown/Markdown.tsx | 7 ++++++- src/routes/admin/users.$userId.tsx | 6 +----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/markdown/Markdown.tsx b/src/components/markdown/Markdown.tsx index 056741c7f..a5fa01be5 100644 --- a/src/components/markdown/Markdown.tsx +++ b/src/components/markdown/Markdown.tsx @@ -2,7 +2,12 @@ import type { HTMLProps } from 'react' import * as React from 'react' import { MarkdownLink } from './MarkdownLink' -import parse, { attributesToProps, domToReact, Element, HTMLReactParserOptions, } from 'html-react-parser' +import parse, { + attributesToProps, + domToReact, + Element, + HTMLReactParserOptions, +} from 'html-react-parser' import { renderMarkdown } from '~/utils/markdown' import { CodeBlock } from './CodeBlock' diff --git a/src/routes/admin/users.$userId.tsx b/src/routes/admin/users.$userId.tsx index 3e098b9f7..2c966a670 100644 --- a/src/routes/admin/users.$userId.tsx +++ b/src/routes/admin/users.$userId.tsx @@ -1,8 +1,4 @@ -import { - createFileRoute, - Link, - redirect, -} from '@tanstack/react-router' +import { createFileRoute, Link, redirect } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' import { getUser } from '~/utils/users.server' import { getUserRoles } from '~/utils/roles.functions'