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) => (
-
- ),
+ 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) {
Title *
- 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) {
Link URL
- 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"
/>
- setStartsAt(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"
/>
@@ -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({
Title *
- 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 (
-
- )
-})
-
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 (
+
+ )
+})
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() {
URL
-
@@ -446,7 +443,7 @@ function ShowcaseDetailPage() {
Source Code URL (optional)
-
@@ -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() {
Tranco Rank
-
@@ -511,7 +506,7 @@ function ShowcaseDetailPage() {
Vote Score
-
@@ -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) => (
@@ -839,7 +833,7 @@ function ShowcaseDetailPage() {
prev ? { ...prev, moderationNote: e.target.value } : null,
)
}
- className={`${inputClass} min-h-[80px]`}
+ 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-[80px]"
placeholder="Add a note about this moderation decision..."
/>
diff --git a/src/routes/admin/users.$userId.tsx b/src/routes/admin/users.$userId.tsx
index b1f9182c9..3e098b9f7 100644
--- a/src/routes/admin/users.$userId.tsx
+++ b/src/routes/admin/users.$userId.tsx
@@ -1,7 +1,6 @@
import {
createFileRoute,
Link,
- notFound,
redirect,
} from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
@@ -12,6 +11,7 @@ import { ArrowLeft, User, Shield, Calendar, Mail, AtSign } from 'lucide-react'
import { requireCapability } from '~/utils/auth.server'
import { Card } from '~/components/Card'
import { format } from '~/utils/dates'
+import { Badge } from '~/ui'
export const Route = createFileRoute('/admin/users/$userId')({
beforeLoad: async () => {
@@ -198,12 +198,9 @@ function UserDetailPage() {
{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({
- onOpenChange(false)}
- className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
Cancel
-
-
+
{isProcessing ? 'Processing...' : 'Save'}
-
+
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.
-
+
Reject All
-
+
Customize
-
+
Accept All
@@ -264,10 +255,7 @@ export default function CookieConsent() {
-
+
Save
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 (
-
+
{copied ? (
<>
@@ -257,7 +263,12 @@ export function CopyPageDropdown({
-
+
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) {
{
router.invalidate()
}}
- className="bg-gray-600 border-gray-600 hover:bg-gray-700 text-white"
>
Try Again
{isRoot ? (
-
+
TanStack Home
) : (
{
e.preventDefault()
window.history.back()
diff --git a/src/components/FeedbackModerationList.tsx b/src/components/FeedbackModerationList.tsx
index 12642d02a..2c86feb21 100644
--- a/src/components/FeedbackModerationList.tsx
+++ b/src/components/FeedbackModerationList.tsx
@@ -16,7 +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'
+import { Badge, Button } from '~/ui'
interface FeedbackModerationListProps {
data:
@@ -237,20 +237,24 @@ export function FeedbackModerationList({
e.stopPropagation()}>
{isPending && !isModeratingThis && (
- handleModerate(feedback.id, 'approve')}
- className="px-3 py-1 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded transition-colors"
title="Approve"
>
-
-
+ handleModerate(feedback.id, 'deny')}
- className="px-3 py-1 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded transition-colors"
title="Deny"
>
-
+
)}
{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 || (
- window.history.back()}
- className="bg-emerald-500 border-emerald-500 hover:bg-emerald-600 text-white"
- >
+ window.history.back()}>
Go back
-
+
Start Over
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
setShowModal(false)}
- 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"
+ className="w-full lg:w-auto justify-center"
>
Hide
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 (
diff --git a/src/components/ShowcaseDetail.tsx b/src/components/ShowcaseDetail.tsx
index 07edc9ed4..da78eb81f 100644
--- a/src/components/ShowcaseDetail.tsx
+++ b/src/components/ShowcaseDetail.tsx
@@ -9,7 +9,7 @@ import {
Sparkles,
Code,
} from 'lucide-react'
-import { Button } from './Button'
+import { Button } from '~/ui'
import { twMerge } from 'tailwind-merge'
import { libraries, type LibraryId } from '~/libraries'
import { USE_CASE_LABELS } from '~/utils/showcase.client'
diff --git a/src/components/ShowcaseGallery.tsx b/src/components/ShowcaseGallery.tsx
index 61ac6bbfa..afeeadd46 100644
--- a/src/components/ShowcaseGallery.tsx
+++ b/src/components/ShowcaseGallery.tsx
@@ -12,7 +12,7 @@ import { PaginationControls } from './PaginationControls'
import { ShowcaseTopBarFilters } from './ShowcaseTopBarFilters'
import type { ShowcaseUseCase } from '~/db/types'
import { Plus } from 'lucide-react'
-import { Button } from './Button'
+import { Button } from '~/ui'
import { useCurrentUser } from '~/hooks/useCurrentUser'
import { useLoginModal } from '~/contexts/LoginModalContext'
diff --git a/src/components/ShowcaseModerationList.tsx b/src/components/ShowcaseModerationList.tsx
index 3c731bb75..769a60cc4 100644
--- a/src/components/ShowcaseModerationList.tsx
+++ b/src/components/ShowcaseModerationList.tsx
@@ -22,7 +22,7 @@ import {
ThumbsDown,
} from 'lucide-react'
import { libraries } from '~/libraries'
-import { Badge } from '~/ui'
+import { Badge, Button } from '~/ui'
import { Fragment, useState } from 'react'
interface ShowcaseModerationListProps {
@@ -361,26 +361,33 @@ export function ShowcaseModerationList({
) : (
{showcase.status !== 'approved' && (
-
handleModerate(showcase.id, 'approve')
}
- className="p-1.5 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded transition-colors"
title="Approve"
>
-
+
)}
{showcase.status !== 'denied' && (
- handleModerate(showcase.id, 'deny')}
- className="p-1.5 text-xs font-medium text-white bg-orange-600 hover:bg-orange-700 rounded transition-colors"
title="Deny"
>
-
+
)}
- {
if (
confirm(
@@ -390,11 +397,10 @@ export function ShowcaseModerationList({
onDelete(showcase.id)
}
}}
- className="p-1.5 text-xs font-medium text-white bg-red-600 hover:bg-red-700 rounded transition-colors"
title="Delete"
>
-
+
)}
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
-
+
+
+ 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 (
-
+
{themeMode === 'auto' ? (
) : (
diff --git a/src/components/admin/AdminAccessDenied.tsx b/src/components/admin/AdminAccessDenied.tsx
index cc6c7c685..48a6d6876 100644
--- a/src/components/admin/AdminAccessDenied.tsx
+++ b/src/components/admin/AdminAccessDenied.tsx
@@ -1,5 +1,6 @@
import { Link } from '@tanstack/react-router'
import { Lock } from 'lucide-react'
+import { Button } from '~/ui'
export function AdminAccessDenied() {
return (
@@ -10,11 +11,8 @@ export function AdminAccessDenied() {
You don't have permission to access the admin area.
-
- Back to Home
+
+ Back to Home
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}
+
+ {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) {
-
+
Cancel
-
-
+
+
{saving ? 'Saving...' : 'Save Banner'}
-
+
@@ -494,18 +487,18 @@ export function BannerEditor({ banner, onSave, onCancel }: BannerEditorProps) {
focusRing="purple"
className="flex-1 px-3 py-2 text-sm"
/>
- {
if (newPathPrefix.trim()) {
addPathPrefix(newPathPrefix.trim())
}
}}
disabled={!newPathPrefix.trim()}
- className="px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
-
+
{/* 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({
-
+
Cancel
-
-
+
+
{saving ? 'Saving...' : 'Save Entry'}
-
+
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
-
{syncingAll ? (
@@ -74,7 +75,7 @@ export function FeedSyncStatus({
)}
Sync All
-
+
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={
<>
-
+
Get Started
{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({
{
let copyContent =
diff --git a/src/components/markdown/MarkdownContent.tsx b/src/components/markdown/MarkdownContent.tsx
index 7567b046f..86553d4a3 100644
--- a/src/components/markdown/MarkdownContent.tsx
+++ b/src/components/markdown/MarkdownContent.tsx
@@ -5,7 +5,7 @@ import { DocTitle } from '~/components/DocTitle'
import { Markdown } from './Markdown'
import { CopyPageDropdown } from '~/components/CopyPageDropdown'
import { DocFeedbackProvider } from '~/components/DocFeedbackProvider'
-import { Button } from '~/components/Button'
+import { Button } from '~/ui'
type MarkdownContentProps = {
title: string
@@ -93,6 +93,8 @@ export function MarkdownContent({
diff --git a/src/routes/$libraryId/$version.index.tsx b/src/routes/$libraryId/$version.index.tsx
index 3d53c84ec..a3a5a4771 100644
--- a/src/routes/$libraryId/$version.index.tsx
+++ b/src/routes/$libraryId/$version.index.tsx
@@ -10,7 +10,7 @@ import { getLibrary } from '~/libraries'
import type { LibraryId } from '~/libraries'
import { seo } from '~/utils/seo'
import { ossStatsQuery } from '~/queries/stats'
-import { Button } from '~/components/Button'
+import { Button } from '~/ui'
// Lazy-loaded landing components for each library
const landingComponents: Partial<
diff --git a/src/routes/account/index.tsx b/src/routes/account/index.tsx
index e6801858a..bf89d5436 100644
--- a/src/routes/account/index.tsx
+++ b/src/routes/account/index.tsx
@@ -22,7 +22,7 @@ import {
Trash2,
} from 'lucide-react'
import { Card } from '~/components/Card'
-import { Button } from '~/components/Button'
+import { Button } from '~/ui'
import { Avatar } from '~/components/Avatar'
import { AvatarCropModal } from '~/components/AvatarCropModal'
import { useUploadThing } from '~/utils/uploadthing'
diff --git a/src/routes/account/integrations.tsx b/src/routes/account/integrations.tsx
index ffef50379..72ccc8953 100644
--- a/src/routes/account/integrations.tsx
+++ b/src/routes/account/integrations.tsx
@@ -13,7 +13,7 @@ import {
} from '~/utils/oauthClient.functions'
import { useToast } from '~/components/ToastProvider'
import { Card } from '~/components/Card'
-import { Button } from '~/components/Button'
+import { Button, FormInput } from '~/ui'
import {
Key,
Plus,
@@ -25,7 +25,6 @@ import {
Clock,
Link2,
} from 'lucide-react'
-import { FormInput } from '~/ui'
export const Route = createFileRoute('/account/integrations')({
component: IntegrationsPage,
})
diff --git a/src/routes/account/submissions.tsx b/src/routes/account/submissions.tsx
index f878624d1..3f14411bd 100644
--- a/src/routes/account/submissions.tsx
+++ b/src/routes/account/submissions.tsx
@@ -16,8 +16,7 @@ import {
Pencil,
} from 'lucide-react'
import type { Showcase } from '~/db/types'
-import { Button, buttonStyles } from '~/components/Button'
-import { Badge } from '~/ui'
+import { Badge, Button } from '~/ui'
export const Route = createFileRoute('/account/submissions')({
loader: async ({ context: { queryClient } }) => {
@@ -194,13 +193,11 @@ function AccountSubmissionsPage() {
Visit
-
-
- Edit
+
+
+
+ Edit
+
handleDelete(showcase.id)}>
diff --git a/src/routes/admin/banners.$id.tsx b/src/routes/admin/banners.$id.tsx
index cc4ddda11..2d1d66943 100644
--- a/src/routes/admin/banners.$id.tsx
+++ b/src/routes/admin/banners.$id.tsx
@@ -5,6 +5,7 @@ import { getBanner, type BannerWithMeta } from '~/utils/banner.functions'
import { useCapabilities } from '~/hooks/useCapabilities'
import { useCurrentUserQuery } from '~/hooks/useCurrentUser'
import { hasCapability } from '~/db/types'
+import { Button } from '~/ui'
import * as v from 'valibot'
export const Route = createFileRoute('/admin/banners/$id')({
@@ -62,12 +63,12 @@ function BannerEditorPage() {
Banner Not Found
- navigate({ to: '/admin/banners' })}
- className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
>
Back to Banners
-
+
)
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
+
+
+
+ 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
- navigate({ to: '/admin/feed' })}
- className="mt-4 px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg"
>
Back to Feed Admin
-
+
)
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
+
+
+
+ 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 (
- {
setRefreshingKey(row.original.cacheKey)
refreshMutation.mutate(row.original.cacheKey)
}}
disabled={isRefreshing || refreshAllMutation.isPending}
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
>
Refresh
-
+
)
},
},
@@ -323,16 +324,16 @@ function GitHubStatsAdmin() {
{cacheEntries && cacheEntries.length > 0 && (
-
refreshAllMutation.mutate()}
disabled={refreshAllMutation.isPending || refreshingKey !== null}
- className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Refresh All
-
+
)}
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
+
+ Back to Home
@@ -483,11 +480,8 @@ function UsersTab({
View and manage individual user accounts, roles, and capabilities.
-
- Manage Users
+
+ 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 }) => (
-
refreshPackageMutation.mutate(row.original.packageName)
}
disabled={refreshPackageMutation.isPending}
- className="flex items-center gap-2 px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors text-sm"
>
Refresh
-
+
),
},
],
@@ -304,13 +305,13 @@ function NpmStatsAdmin() {
rebuild all caches.
- {
console.log('[Admin UI] Refresh button clicked')
refreshAllMutation.mutate('tanstack')
}}
disabled={refreshAllMutation.isPending}
- className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
title="Complete refresh: discover packages, fetch fresh stats with growth rates, and 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
- {
if (
window.confirm(
@@ -308,10 +308,10 @@ function RoleDetailPage() {
handleRemoveUsers()
}
}}
- className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors"
+ color="red"
>
Remove from Role
-
+
)}
@@ -325,13 +325,13 @@ function RoleDetailPage() {
Remove {confirmRemove.name} from role "{role?.name}"?
- setConfirmRemove(null)}
- className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-white rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600"
+ variant="secondary"
>
Cancel
-
-
+ {
try {
await removeUsersFromRole.mutateAsync({
@@ -353,10 +353,10 @@ function RoleDetailPage() {
)
}
}}
- className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700"
+ color="red"
>
Remove
-
+
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 && (
-
+
Create Role
-
+
)
}
/>
@@ -458,14 +455,15 @@ function RolesPage() {
))}
-
{testEmailStatus.loading ? 'Sending...' : 'Test Email'}
-
+
}
/>
@@ -531,20 +529,14 @@ function RolesPage() {
-
+
Save
-
-
+
+
Cancel
-
+
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() {
))}
-
Assign
-
+
{availableCapabilities.map((capability) => (
- handleBulkUpdateCapabilities([capability])}
- className="px-3 py-1 text-sm bg-green-600 text-white rounded-md hover:bg-green-700"
+ color="green"
+ size="sm"
>
Add {capability}
-
+
))}
- handleBulkUpdateCapabilities([])}
- className="px-3 py-1 text-sm bg-red-600 text-white rounded-md hover:bg-red-700"
+ color="red"
+ size="sm"
>
Clear All
-
+
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() {
- More Libraries →
+ More Libraries
@@ -534,7 +534,7 @@ function Index() {
- View All Maintainers →
+ View All Maintainers
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
+
+ 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.
- navigate({ search: {}, replace: true })}
- className="inline-block px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm"
>
View All Partners
-
+
) : (
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'