Skip to content

Refactor conference details and improve component visibility#332

Merged
Starefossen merged 4 commits intomainfrom
search-text-color
Feb 13, 2026
Merged

Refactor conference details and improve component visibility#332
Starefossen merged 4 commits intomainfrom
search-text-color

Conversation

@Starefossen
Copy link
Copy Markdown
Member

@Starefossen Starefossen commented Feb 13, 2026

User description

Update conference references from "Bergen" to "Norway" and enhance the visibility of the SearchInput component's text color. Introduce new MetricCard, StatCard, and StatsGrid components for better metrics display and responsive layouts. Adjust documentation and mock data accordingly.


PR Type

Enhancement, Tests


Description

  • Introduce composable DataTable components with TableContainer, TableHeader, TableBody, and TableEmptyState primitives

  • Create new stats display system with StatCard, MetricCard, and StatsGrid components for dashboards

  • Refactor SpeakerTable and SponsorContactTable to use new DataTable components

  • Update conference references from "Bergen" to "Norway" across all components and documentation

  • Improve SearchInput text color visibility in dark mode


Diagram Walkthrough

flowchart LR
  A["New DataTable<br/>Components"] --> B["TableContainer<br/>TableHeader<br/>TableBody<br/>TableEmptyState"]
  C["New Stats<br/>Components"] --> D["StatCard<br/>MetricCard<br/>StatsGrid"]
  B --> E["Refactored<br/>SpeakerTable"]
  B --> F["Refactored<br/>SponsorContactTable"]
  D --> G["Dashboard<br/>Examples"]
  H["Conference<br/>Branding Update"] --> I["Bergen → Norway<br/>Domain Updates"]
  J["SearchInput<br/>Enhancement"] --> K["Better Text<br/>Visibility"]
Loading

File Walkthrough

Relevant files
Enhancement
17 files
index.ts
New DataTable component exports                                                   
+12/-0   
DataTable.tsx
High-level DataTable component with column definitions     
+107/-0 
TableContainer.tsx
Table wrapper with shadow and ring styling                             
+22/-0   
TableHeader.tsx
Styled table header and Th cell components                             
+66/-0   
TableBody.tsx
Styled table body, row, and cell components                           
+97/-0   
TableEmptyState.tsx
Empty state display for tables with no data                           
+35/-0   
TableToolbar.tsx
Search, filter, and result count toolbar component             
+76/-0   
index.ts
New stats component exports                                                           
+6/-0     
StatCard.tsx
Simple stat card with value, label, and color themes         
+63/-0   
MetricCard.tsx
Enhanced metric card with icon, trend, and loading state 
+79/-0   
StatsGrid.tsx
Responsive grid wrapper for stat cards                                     
+51/-0   
SpeakerTable.tsx
Refactor to use new DataTable and TableToolbar components
+347/-402
SponsorContactTable.tsx
Refactor to use new DataTable primitive components             
+44/-76 
OrdersTableWithSearch.tsx
Update table headers to use Th component                                 
+29/-43 
AdminPageHeader.tsx
Re-export StatCardProps from stats module                               
+3/-47   
SponsorDashboardMetrics.tsx
Replace inline MetricCard with imported component               
+1/-61   
index.ts
Export new stats components and types                                       
+8/-0     
Documentation
22 files
DataTable.stories.tsx
Comprehensive Storybook stories for DataTable system         
+463/-0 
Stats.stories.tsx
Comprehensive Storybook stories for stats components         
+415/-0 
config.ts
Update domain examples from cloudnativeday to cloudnativedays
+4/-4     
AdminPages.stories.tsx
Add DashboardWithStats story and update conference references
+118/-2 
SpeakerActions.stories.tsx
Update mock conference data to Norway branding                     
+11/-11 
BadgePreviewModal.stories.tsx
Update mock badge data to Norway branding                               
+5/-5     
ProposalManagementModal.stories.tsx
Update mock conference to Norway branding                               
+6/-6     
ProposalGuidanceSidebar.stories.tsx
Update mock conference to Norway branding                               
+6/-6     
ProposalActionModal.stories.tsx
Update domain references to cloudnativedays.no                     
+3/-3     
AdminPageHeader.stories.tsx
Update context highlight to Norway branding                           
+2/-2     
SponsorEmailTemplateEditor.stories.tsx
Update mock conference to Norway branding                               
+7/-7     
SponsorDiscountEmailModal.stories.tsx
Update email templates to Norway branding                               
+4/-4     
SponsorIndividualEmailModal.stories.tsx
Update email subject to Norway branding                                   
+2/-2     
SponsorTemplatePicker.stories.tsx
Update mock conference to Norway branding                               
+4/-4     
SpeakerEmailModal.stories.tsx
Update mock proposal conference to Norway branding             
+1/-1     
ScheduleEditor.stories.tsx
Update mock conference to Norway branding                               
+2/-2     
SponsorCRMPipeline.stories.tsx
Update pipeline header to Norway branding                               
+1/-1     
ProposalDetailsForm.stories.tsx
Update mock conference to Norway branding                               
+2/-2     
SponsorOnboardingForm.stories.tsx
Update conference name to Norway branding                               
+1/-1     
GeneralBroadcastModal.stories.tsx
Update event name to Norway branding                                         
+1/-1     
CallToAction.stories.tsx
Update base conference to Norway branding                               
+2/-2     
EmailModal.stories.tsx
Update email subject to Norway branding                                   
+1/-1     
Bug_fix
1 files
SearchInput.tsx
Add text-gray-900 class for better dark mode visibility   
+1/-1     
Tests
3 files
conference.ts
Update mock conference domain to cloudnativedays.no           
+1/-1     
badge-e2e.test.ts
Update test host domain reference                                               
+1/-1     
route.test.ts.skip
Update test domain references to cloudnativedays.no           
+6/-6     

…ss components and stories

- Changed conference title and organizer in various components to "Cloud Native Days Norway 2025".
- Updated email templates and mock data to reflect the new conference name and URLs.
- Introduced new MetricCard and StatCard components for improved metrics display.
- Added StatsGrid component for responsive layout of stat cards.
- Updated design system examples to utilize new stats components.
- Adjusted badge configuration documentation to reflect domain changes.
@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 13, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cloudnativedays Ready Ready Preview, Comment Feb 13, 2026 11:10am

Request Review

@qodo-code-review
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Type Safety

The fallback cell rendering converts values via String((item as Record<string, unknown>)[column.key] ?? ''). This can hide type errors and stringify non-primitive values (objects -> "[object Object]"), producing confusing UI. Consider constraining Column.key to keyof T (or providing a valueGetter) so that accidental mismatches are caught at compile time and complex values are intentionally rendered.

export interface Column<T> {
  key: string
  header: string
  width?: string
  align?: 'left' | 'center' | 'right'
  hiddenBelow?: 'sm' | 'md' | 'lg' | 'xl'
  render?: (item: T, index: number) => ReactNode
  className?: string
}

export interface DataTableProps<T> {
  data: T[]
  columns: Column<T>[]
  keyExtractor: (item: T, index: number) => string
  emptyState?: {
    icon?: React.ElementType
    title: string
    description?: string
    action?: ReactNode
  }
  onRowClick?: (item: T, index: number) => void
  isRowSelected?: (item: T, index: number) => boolean
  tableClassName?: string
  className?: string
}

export function DataTable<T>({
  data,
  columns,
  keyExtractor,
  emptyState,
  onRowClick,
  isRowSelected,
  tableClassName,
  className,
}: DataTableProps<T>) {
  if (data.length === 0 && emptyState) {
    return (
      <TableEmptyState
        icon={emptyState.icon}
        title={emptyState.title}
        description={emptyState.description}
        action={emptyState.action}
        className={className}
      />
    )
  }

  return (
    <TableContainer className={className}>
      <table
        className={clsx(
          'min-w-full divide-y divide-gray-300 dark:divide-gray-700',
          tableClassName,
        )}
      >
        <TableHeader>
          <tr>
            {columns.map((column) => (
              <Th
                key={column.key}
                width={column.width}
                align={column.align}
                hiddenBelow={column.hiddenBelow}
                className={column.className}
              >
                {column.header}
              </Th>
            ))}
          </tr>
        </TableHeader>
        <TableBody>
          {data.map((item, index) => (
            <Tr
              key={keyExtractor(item, index)}
              onClick={onRowClick ? () => onRowClick(item, index) : undefined}
              selected={isRowSelected ? isRowSelected(item, index) : false}
              className={onRowClick ? 'cursor-pointer' : undefined}
            >
              {columns.map((column) => (
                <Td
                  key={column.key}
                  align={column.align}
                  hiddenBelow={column.hiddenBelow}
                  className={column.className}
                >
                  {column.render
                    ? column.render(item, index)
                    : String(
                        (item as Record<string, unknown>)[column.key] ?? '',
                      )}
                </Td>
              ))}
Accessibility

Clickable rows are implemented via onClick on tr with a cursor-pointer class. This pattern is typically not keyboard-accessible by default and may be missed by screen readers. If rows are intended to be interactive, consider adding keyboard handling (Enter/Space), focus styles, and an appropriate role/tabIndex, or move the interactive element inside the row (e.g., a link/button).

export interface TrProps extends HTMLAttributes<HTMLTableRowElement> {
  children: ReactNode
  hoverable?: boolean
  selected?: boolean
}

export function Tr({
  children,
  className,
  hoverable = true,
  selected,
  ...props
}: TrProps) {
  return (
    <tr
      className={clsx(
        hoverable && 'hover:bg-gray-50 dark:hover:bg-gray-800',
        selected && 'bg-indigo-50 dark:bg-indigo-900/20',
        className,
      )}
      {...props}
    >
      {children}
    </tr>
  )
Client Boundary

TableToolbar is a client component and conditionally renders SearchInput based on searchValue/onSearchChange. Verify that all server components consuming TableToolbar either remain client-safe or avoid passing unstable callbacks across boundaries, and ensure the optional props combinations don't produce confusing states (e.g., showClearButton true without onClear, or searchValue defined without onSearchChange).

'use client'

import { ReactNode } from 'react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import clsx from 'clsx'
import { SearchInput } from '@/components/SearchInput'

export interface TableToolbarProps {
  searchValue?: string
  onSearchChange?: (value: string) => void
  searchPlaceholder?: string
  showClearButton?: boolean
  onClear?: () => void
  clearLabel?: string
  children?: ReactNode
  resultCount?: number
  totalCount?: number
  resultLabel?: string
  className?: string
}

export function TableToolbar({
  searchValue,
  onSearchChange,
  searchPlaceholder = 'Search...',
  showClearButton,
  onClear,
  clearLabel = 'Clear',
  children,
  resultCount,
  totalCount,
  resultLabel = 'items',
  className,
}: TableToolbarProps) {
  const hasSearch = searchValue !== undefined && onSearchChange !== undefined
  const hasResultCount = resultCount !== undefined

  return (
    <div className={clsx('space-y-4', className)}>
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-center gap-2">
          {hasSearch && (
            <SearchInput
              value={searchValue}
              onChange={onSearchChange}
              placeholder={searchPlaceholder}
              className="flex-1 sm:max-w-xs"
            />
          )}

          {showClearButton && onClear && (
            <button
              onClick={onClear}
              className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-gray-100 px-3 py-1.5 text-sm text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600"
            >
              <XMarkIcon className="h-4 w-4" />
              {clearLabel}
            </button>
          )}
        </div>

        {children && <div className="flex items-center gap-2">{children}</div>}
      </div>

      {hasResultCount && (
        <div className="text-sm text-gray-500 dark:text-gray-400">
          Showing {resultCount}
          {totalCount !== undefined && totalCount !== resultCount && (
            <> of {totalCount}</>
          )}{' '}
          {resultLabel}
        </div>
      )}
    </div>
  )
}

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented Feb 13, 2026

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Designate DataTable as client component
Suggestion Impact:The commit added the 'use client' directive at the top of DataTable.tsx, making the component a client component so click/keyboard event handlers can function.

code diff:

@@ -1,4 +1,6 @@
-import { ReactNode } from 'react'
+'use client'
+
+import { ReactNode, KeyboardEvent } from 'react'
 import clsx from 'clsx'

Add the 'use client' directive to the DataTable component to ensure event
handlers like onRowClick function correctly within the Next.js App Router.

src/components/DataTable/DataTable.tsx [1-3]

+'use client'
 import { ReactNode } from 'react'
 import clsx from 'clsx'
 import { TableContainer } from './TableContainer'

[Suggestion processed]

Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies that the DataTable component must be a client component due to its use of event handlers like onRowClick. Missing the 'use client' directive would break its functionality in a Next.js App Router environment.

High
Prevent potential runtime rendering errors
Suggestion Impact:The default cell rendering logic was updated to guard property access with `typeof item === 'object' && item !== null`, preventing runtime errors when `data` contains non-object values (implemented with an additional undefined check).

code diff:

+                    {column.render
+                      ? column.render(item, index)
+                      : String(
+                          typeof item === 'object' &&
+                            item !== null &&
+                            (item as Record<string, unknown>)[column.key] !==
+                              undefined
+                            ? (item as Record<string, unknown>)[column.key]
+                            : '',
+                        )}

Add a type check to ensure item is an object before accessing properties by key
in the default cell renderer to prevent potential runtime errors.

src/components/DataTable/DataTable.tsx [94-98]

 {column.render
   ? column.render(item, index)
   : String(
-      (item as Record<string, unknown>)[column.key] ?? '',
+      (typeof item === 'object' &&
+      item !== null &&
+      (item as Record<string, unknown>)[column.key]) ??
+        '',
     )}

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: This suggestion correctly identifies a potential runtime error if the data prop contains non-object items and provides a robust fix by adding a type check, improving the component's resilience.

Medium
General
Add keyboard support to rows
Suggestion Impact:The commit adds conditional role="button", tabIndex={0}, and an onKeyDown handler that triggers the row click on Enter/Space, implementing keyboard-operable rows when onRowClick is provided.

code diff:

+          {data.map((item, index) => {
+            const handleRowClick = onRowClick
+              ? () => onRowClick(item, index)
+              : undefined
+
+            const handleRowKeyDown = onRowClick
+              ? (event: KeyboardEvent<HTMLTableRowElement>) => {
+                  if (event.key === 'Enter' || event.key === ' ') {
+                    event.preventDefault()
+                    onRowClick(item, index)
+                  }
+                }
+              : undefined
+
+            return (
+              <Tr
+                key={keyExtractor(item, index)}
+                role={onRowClick ? 'button' : undefined}
+                tabIndex={onRowClick ? 0 : undefined}
+                onClick={handleRowClick}
+                onKeyDown={handleRowKeyDown}
+                selected={isRowSelected ? isRowSelected(item, index) : false}
+              >

Enhance keyboard accessibility for clickable rows in DataTable by adding
role="button", tabIndex={0}, and an onKeyDown handler for 'Enter' and 'Space'
keys.

src/components/DataTable/DataTable.tsx [81-86]

 <Tr
   key={keyExtractor(item, index)}
+  role="button"
+  tabIndex={onRowClick ? 0 : undefined}
   onClick={onRowClick ? () => onRowClick(item, index) : undefined}
+  onKeyDown={onRowClick ? e => {
+    if (e.key === 'Enter' || e.key === ' ') {
+      e.preventDefault()
+      onRowClick(item, index)
+    }
+  } : undefined}
   selected={isRowSelected ? isRowSelected(item, index) : false}
   className={onRowClick ? 'cursor-pointer' : undefined}
 >

[Suggestion processed]

Suggestion importance[1-10]: 7

__

Why: The suggestion provides a valuable accessibility improvement by making clickable table rows keyboard-operable, which is a significant enhancement for users relying on keyboard navigation.

Medium
Conditionally apply row hover effect
Suggestion Impact:Added an isClickable check from props.onClick, removed the default hoverable=true, and applied cursor-pointer only when clickable. Hover styling is now conditional via (hoverable ?? isClickable), aligning hover with clickability unless explicitly overridden by hoverable.

code diff:

@@ -28,14 +28,17 @@
 export function Tr({
   children,
   className,
-  hoverable = true,
+  hoverable,
   selected,
   ...props
 }: TrProps) {
+  const isClickable = Boolean(props.onClick)
+
   return (
     <tr
       className={clsx(
-        hoverable && 'hover:bg-gray-50 dark:hover:bg-gray-800',
+        (hoverable ?? isClickable) && 'hover:bg-gray-50 dark:hover:bg-gray-800',
+        isClickable && 'cursor-pointer',
         selected && 'bg-indigo-50 dark:bg-indigo-900/20',

Conditionally apply row hover and cursor pointer styles only when an onClick
handler is present to improve user experience.

src/components/DataTable/TableBody.tsx [28-47]

 export function Tr({
   children,
   className,
-  hoverable = true,
+  hoverable, // Deprecated, but kept for compatibility if needed.
   selected,
   ...props
 }: TrProps) {
+  const isClickable = !!props.onClick;
+
   return (
     <tr
       className={clsx(
-        hoverable && 'hover:bg-gray-50 dark:hover:bg-gray-800',
+        isClickable && 'cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800',
         selected && 'bg-indigo-50 dark:bg-indigo-900/20',
         className,
       )}
       {...props}
     >
       {children}
     </tr>
   )
 }

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 6

__

Why: The suggestion correctly identifies a UX issue where non-interactive rows have a hover effect and proposes a good solution by tying the hover effect to the presence of an onClick handler, which improves usability and simplifies the component's API.

Low
Make result count ARIA live region
Suggestion Impact:The result count container was updated to include role="status" and aria-live="polite", making it an ARIA live region as suggested.

code diff:

       {hasResultCount && (
-        <div className="text-sm text-gray-500 dark:text-gray-400">
+        <div
+          role="status"
+          aria-live="polite"
+          className="text-sm text-gray-500 dark:text-gray-400"
+        >

Improve accessibility by adding role="status" and aria-live="polite" to the
results count container in TableToolbar, making it an ARIA live region.

src/components/DataTable/TableToolbar.tsx [65-73]

 {hasResultCount && (
-  <div className="text-sm text-gray-500 dark:text-gray-400">
+  <div role="status" aria-live="polite" className="text-sm text-gray-500 dark:text-gray-400">
     Showing {resultCount}
     {totalCount !== undefined && totalCount !== resultCount && (
       <> of {totalCount}</>
     )}{' '}
     {resultLabel}
   </div>
 )}

[Suggestion processed]

Suggestion importance[1-10]: 6

__

Why: This suggestion significantly improves accessibility by making the results count a live region, ensuring that screen reader users are notified of updates when filtering or searching.

Low
Avoid rendering an empty table
Suggestion Impact:Updated the empty-data conditional to return TableEmptyState when provided, otherwise return null, preventing rendering of an empty table structure.

code diff:

-  if (data.length === 0 && emptyState) {
-    return (
-      <TableEmptyState
-        icon={emptyState.icon}
-        title={emptyState.title}
-        description={emptyState.description}
-        action={emptyState.action}
-        className={className}
-      />
-    )
+  if (data.length === 0) {
+    if (emptyState) {
+      return (
+        <TableEmptyState
+          icon={emptyState.icon}
+          title={emptyState.title}
+          description={emptyState.description}
+          action={emptyState.action}
+          className={className}
+        />
+      )
+    }
+
+    return null
   }

Return null from the DataTable component when data is empty and no emptyState is
provided to avoid rendering an empty table structure.

src/components/DataTable/DataTable.tsx [44-54]

-if (data.length === 0 && emptyState) {
-  return (
-    <TableEmptyState
-      icon={emptyState.icon}
-      title={emptyState.title}
-      description={emptyState.description}
-      action={emptyState.action}
-      className={className}
-    />
-  )
+if (data.length === 0) {
+  if (emptyState) {
+    return (
+      <TableEmptyState
+        icon={emptyState.icon}
+        title={emptyState.title}
+        description={emptyState.description}
+        action={emptyState.action}
+        className={className}
+      />
+    )
+  }
+  return null
 }

[Suggestion processed]

Suggestion importance[1-10]: 5

__

Why: The suggestion correctly identifies a potential UI issue where an empty table with only headers is rendered. Returning null is a reasonable improvement to avoid this awkward visual state.

Low
Hide clear button when empty
Suggestion Impact:Updated the clear button render condition to also require a truthy searchValue, hiding the button when the search input is empty.

code diff:

-          {showClearButton && onClear && (
+          {showClearButton && searchValue && onClear && (

Conditionally render the clear button in TableToolbar only when searchValue is
not empty to improve the user interface.

src/components/DataTable/TableToolbar.tsx [51-59]

-{showClearButton && onClear && (
+{showClearButton && searchValue && onClear && (
   <button
     onClick={onClear}
     className="inline-flex cursor-pointer items-center gap-1 rounded-md bg-gray-100 px-3 py-1.5 text-sm ..."
   >
     <XMarkIcon className="h-4 w-4" />
     {clearLabel}
   </button>
 )}

[Suggestion processed]

Suggestion importance[1-10]: 5

__

Why: This is a good UX improvement. The clear button should only be visible when there is content in the search input to clear, and the suggestion correctly implements this logic.

Low
High-level
Consolidate new stats and table components

To improve organization, move the new DataTable and Stats component systems from
their current separate directories into a single, unified
/components/DataDisplay directory.

Examples:

src/components/DataTable/DataTable.tsx [1-107]
import { ReactNode } from 'react'
import clsx from 'clsx'
import { TableContainer } from './TableContainer'
import { TableHeader, Th } from './TableHeader'
import { TableBody, Tr, Td } from './TableBody'
import { TableEmptyState } from './TableEmptyState'

export interface Column<T> {
  key: string
  header: string

 ... (clipped 97 lines)
src/components/admin/stats/MetricCard.tsx [1-79]
import { ReactNode } from 'react'
import clsx from 'clsx'

export type MetricTrend = 'up' | 'down' | 'neutral'

export interface MetricCardProps {
  /** Label/title for the metric */
  title: string
  /** The main value to display */
  value: string | number

 ... (clipped 69 lines)

Solution Walkthrough:

Before:

/src
├── /components
   ├── /admin
      ├── /stats
         ├── MetricCard.tsx
         ├── StatCard.tsx
         └── StatsGrid.tsx
      └── ...
   ├── /DataTable
      ├── DataTable.tsx
      ├── TableBody.tsx
      └── ...
   └── ...
└── ...

After:

/src
├── /components
   ├── /DataDisplay
      ├── /DataTable
         ├── DataTable.tsx
         └── ...
      ├── /Stats
         ├── MetricCard.tsx
         ├── StatCard.tsx
         └── StatsGrid.tsx
   └── ...
└── ...
Suggestion importance[1-10]: 6

__

Why: This is a valid architectural suggestion that improves the project's structure and component discoverability, though it is not a critical issue.

Low
  • Update

- Added keyboard accessibility for row clicks in DataTable by handling Enter and Space keys.
- Updated empty state rendering logic to return null if no data and no empty state provided.
- Improved Tr component to conditionally apply hover and cursor styles based on clickability.
- Modified TableToolbar to show clear button only when there is a search value.
- Added aria-live role to status message in TableToolbar for better screen reader support.
@Starefossen Starefossen merged commit 125f1fa into main Feb 13, 2026
9 checks passed
@Starefossen Starefossen deleted the search-text-color branch February 13, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant