Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
939083e
replaces geist and styled-components with tailwind
michaelwschultz Dec 28, 2025
035c688
increase max-width of pages
michaelwschultz Dec 28, 2025
551c440
improve app structure, replace old chart, move to firebase-lite
michaelwschultz Dec 29, 2025
b15c1da
remove unused deps, replace twitter url with bluesky, fix auth
michaelwschultz Dec 29, 2025
15441e5
adds build:prod script to generate correct firebase rules
michaelwschultz Dec 29, 2025
9f44cb5
sort imports
michaelwschultz Dec 29, 2025
0a1aaa8
rename infusion to treatment
michaelwschultz Dec 29, 2025
3d93c9a
rename factor to medication on table
michaelwschultz Dec 29, 2025
3e099d6
adds firebase-tools back to dev deps
michaelwschultz Dec 29, 2025
5205d4f
fix formatting
michaelwschultz Dec 29, 2025
869f1f4
fix effect deps
michaelwschultz Dec 29, 2025
2ecc2c4
try to fix ci workflow
michaelwschultz Dec 29, 2025
5696b94
fix hydration issues and treatmet dialog
michaelwschultz Jan 10, 2026
9f048bc
update cypress tests
michaelwschultz Jan 10, 2026
b145f2c
add vscode settings with biome defaults
michaelwschultz Jan 10, 2026
062fab7
add cursor rules
michaelwschultz Jan 11, 2026
b2a1849
remove d.ts files from biome
michaelwschultz Jan 11, 2026
807da62
update vscode settings to autoformat on save
michaelwschultz Jan 11, 2026
acc9023
replace modal with silk component
michaelwschultz Jan 11, 2026
34f1d3c
fixes
michaelwschultz Jan 25, 2026
956cdf3
fix biome
michaelwschultz Jan 25, 2026
647e05e
fix lint errors
michaelwschultz Jan 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"**/*.tsx",
"**/*.js",
"**/*.jsx",
"**/*.css",
"!**/*.min.js",
"!**/node_modules",
"!**/dist",
Expand Down Expand Up @@ -45,6 +46,11 @@
"trailingCommas": "es5"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
},
"assist": {
"enabled": true,
"actions": {
Expand Down
81 changes: 81 additions & 0 deletions components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type React from 'react'

export interface ModalProps {
visible: boolean
onClose?: () => void
children: React.ReactNode
title?: string
}

export function Modal({
visible,
onClose,
children,
title,
}: ModalProps): JSX.Element | null {
if (!visible) {
return null
}

const handleBackdropClick = (
event: React.MouseEvent<HTMLDivElement>
): void => {
if (event.target === event.currentTarget && onClose) {
onClose()
}
}

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
if (event.key === 'Escape' && onClose) {
onClose()
}
}

return (
<div
className='fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50'
onClick={handleBackdropClick}
onKeyDown={handleKeyDown}
role='dialog'
aria-modal='true'
aria-labelledby={title ? 'modal-title' : undefined}
>
<div className='bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto'>
{title && (
<div className='flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-700'>
<h2
id='modal-title'
className='text-lg font-semibold text-gray-900 dark:text-white'
>
{title}
</h2>
{onClose && (
<button
onClick={onClose}
className='text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 p-1 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700'
aria-label='Close modal'
type='button'
>
<svg
className='w-6 h-6'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
aria-hidden='true'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
)}
</div>
)}
<div className='p-6'>{children}</div>
</div>
</div>
)
}
165 changes: 165 additions & 0 deletions components/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import type React from 'react'
import { useState, useRef, useEffect } from 'react'

export type PopoverPlacement =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-start'
| 'top-end'
| 'bottom-start'
| 'bottom-end'
| 'left-start'
| 'left-end'
| 'right-start'
| 'right-end'

export interface PopoverItemProps {
children: React.ReactNode
onClick?: () => void
disabled?: boolean
title?: boolean
line?: boolean
}

export interface PopoverContentProps {
children: React.ReactNode
}

export interface PopoverProps {
content: React.ReactNode
children: React.ReactElement
placement?: PopoverPlacement
trigger?: 'click' | 'hover'
}

export function PopoverItem({
children,
onClick,
disabled,
title,
line,
}: PopoverItemProps): JSX.Element {
const baseClasses = 'w-full px-4 py-2 text-left text-sm transition-colors'

const classes = title
? `${baseClasses} font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700`
: line
? `${baseClasses} border-t border-gray-200 dark:border-gray-700`
: disabled
? `${baseClasses} text-gray-400 dark:text-gray-500 cursor-not-allowed`
: `${baseClasses} text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer`

const handleClick = (): void => {
if (!disabled && onClick) {
onClick()
}
}

return (
<button
className={classes}
onClick={handleClick}
disabled={disabled}
type='button'
>
{children}
</button>
)
}

export function PopoverContent({ children }: PopoverContentProps): JSX.Element {
return <div className='py-1'>{children}</div>
}

export function Popover({
content,
children,
placement = 'bottom-end',
trigger = 'click',
}: PopoverProps): JSX.Element {
const [isOpen, setIsOpen] = useState<boolean>(false)
const popoverRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)

useEffect(() => {
const handleClickOutside = (event: MouseEvent): void => {
if (
popoverRef.current &&
!popoverRef.current.contains(event.target as Node) &&
triggerRef.current &&
!triggerRef.current.contains(event.target as Node)
) {
setIsOpen(false)
}
}

if (isOpen) {
document.addEventListener('mousedown', handleClickOutside)
}

return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [isOpen])

const handleTriggerClick = (): void => {
if (trigger === 'click') {
setIsOpen(!isOpen)
}
}

const handleTriggerMouseEnter = (): void => {
if (trigger === 'hover') {
setIsOpen(true)
}
}

const handleTriggerMouseLeave = (): void => {
if (trigger === 'hover') {
setIsOpen(false)
}
}

const getPopoverClasses = (): string => {
const baseClasses =
'absolute z-50 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none'

const placementClasses = {
top: 'bottom-full mb-2',
'top-start': 'bottom-full right-0 mb-2',
'top-end': 'bottom-full left-0 mb-2',
bottom: 'top-full mt-2',
'bottom-start': 'top-full right-0 mt-2',
'bottom-end': 'top-full left-0 mt-2',
left: 'right-full mr-2 top-1/2 transform -translate-y-1/2',
'left-start': 'right-full mr-2 top-0',
'left-end': 'right-full mr-2 bottom-0',
right: 'left-full ml-2 top-1/2 transform -translate-y-1/2',
'right-start': 'left-full ml-2 top-0',
'right-end': 'left-full ml-2 bottom-0',
}

return `${baseClasses} ${placementClasses[placement]} ${isOpen ? 'block' : 'hidden'}`
}

return (
<div className='relative'>
<button
ref={triggerRef}
onClick={handleTriggerClick}
onMouseEnter={handleTriggerMouseEnter}
onMouseLeave={handleTriggerMouseLeave}
type='button'
className='cursor-pointer'
>
{children}
</button>

<div ref={popoverRef} className={getPopoverClasses()}>
{content}
</div>
</div>
)
}
108 changes: 108 additions & 0 deletions components/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState } from 'react'

export interface TabsProps {
initialValue?: string
value?: string
onChange?: (value: string) => void
children: React.ReactNode
className?: string
}

export interface TabsItemProps {
label: string
value: string
children: React.ReactNode
}

export function TabsItem({ children }: TabsItemProps): JSX.Element {
return <>{children}</>
}

export function Tabs({
initialValue,
value,
onChange,
children,
className = '',
}: TabsProps): JSX.Element {
const [activeTab, setActiveTab] = useState<string>(initialValue || '')

const currentValue = value !== undefined ? value : activeTab

const handleTabChange = (newValue: string): void => {
if (value === undefined) {
setActiveTab(newValue)
}
if (onChange) {
onChange(newValue)
}
}

const tabItems: {
label: string
value: string
children: React.ReactNode
}[] = []

React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.type === TabsItem) {
const {
label,
value: tabValue,
children: tabChildren,
} = child.props as TabsItemProps
tabItems.push({ label, value: tabValue, children: tabChildren })
}
})

const activeTabContent = tabItems.find(
(item) => item.value === currentValue
)?.children

return (
<>
<div
className={`border-b border-gray-200 dark:border-gray-700 ${className}`}
>
<nav className='flex space-x-8'>
{tabItems.map((item) => {
const isActive = item.value === currentValue

return (
<button
key={item.value}
onClick={() => handleTabChange(item.value)}
className={`
whitespace-nowrap py-2 px-1 border-b-2 font-medium text-sm transition-colors cursor-pointer
${
isActive
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
}
`}
role='tab'
aria-selected={isActive}
type='button'
>
{item.label}
</button>
)
})}
</nav>
</div>
{activeTabContent}
</>
)
}

export interface TabsContentProps {
children: React.ReactNode
className?: string
}

export function TabsContent({
children,
className = '',
}: TabsContentProps): JSX.Element {
return <div className={`py-6 ${className}`}>{children}</div>
}
Loading