Skip to content

Commit

Permalink
Add comprehensive treatment and condition search functionality
Browse files Browse the repository at this point in the history
Implemented a new TreatmentList component with sorting and effectiveness score calculations. Added a search box for treatments and conditions to the DFDA page. Updated condition pages to dynamically load treatments and sort conditions by number of treatments.
  • Loading branch information
mikepsinn committed Sep 9, 2024
1 parent ad76cfc commit 9ad3fed
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 82 deletions.
97 changes: 97 additions & 0 deletions app/dfda/components/TreatmentConditionSearchBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
'use client'

import { useState, useEffect, useRef } from 'react'
import { Input } from "@/components/ui/input"
import {Loader2, Search} from "lucide-react"
import Link from 'next/link'
import {searchTreatmentsAndConditions} from "@/app/dfda/dfdaActions";

type SearchResult = {
id: number
name: string
type: 'treatment' | 'condition'
}

export default function TreatmentConditionSearchBox() {
const [query, setQuery] = useState('')
const [results, setResults] = useState<SearchResult[]>([])
const [isLoading, setIsLoading] = useState(false)
const [showDropdown, setShowDropdown] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)

useEffect(() => {
const handleSearch = async () => {
if (query.length > 2) {
setIsLoading(true)
const searchResults = await searchTreatmentsAndConditions(query)
setResults(searchResults.map(result => ({
...result,
type: result.type as 'treatment' | 'condition'
})))
setIsLoading(false)
setShowDropdown(true)
} else {
setResults([])
setShowDropdown(false)
}
}

const debounce = setTimeout(() => {
handleSearch()
}, 300)

return () => clearTimeout(debounce)
}, [query])

useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setShowDropdown(false)
}
}

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

return (
<div className="w-full max-w-md mx-auto relative" ref={dropdownRef}>
<div className="relative">
<Input
type="search"
placeholder="Search condition, treatments..."
className="w-full pl-4 pr-10 py-2 rounded-full border-input bg-background text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{isLoading ? (
<Loader2 className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5 animate-spin" />
) : (
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-5 w-5" />
)}
</div>
{showDropdown && (
<div className="absolute z-10 w-full mt-1 bg-background rounded-md shadow-lg border border-input">
{results.length > 0 ? (
<ul className="max-h-60 overflow-auto">
{results.map((result) => (
<li key={`${result.type}-${result.id}`}>
<Link
href={`/dfda//${result.type}s/${encodeURIComponent(result.name)}`}
className="block px-4 py-2 hover:bg-accent hover:text-accent-foreground text-sm"
>
{result.name} ({result.type})
</Link>
</li>
))}
</ul>
) : (
<div className="p-2 text-center text-muted-foreground">No results found</div>
)}
</div>
)}
</div>
)
}
149 changes: 149 additions & 0 deletions app/dfda/components/TreatmentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
'use client'

import { useState } from 'react'
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"
import { Card, CardContent } from "@/components/ui/card"
import Image from 'next/image'
import Link from 'next/link'
import { DfdaCondition, DfdaConditionTreatment } from "@prisma/client";
import { Badge } from "@/components/ui/badge";

type TreatmentListProps = {
condition: DfdaCondition & {
conditionTreatments: (DfdaConditionTreatment & {
treatment: { name: string }
})[]
}
};

function calculateEffectivenessScore(treatment: {
majorImprovement: number;
moderateImprovement: number;
noEffect: number;
worse: number;
muchWorse: number;
}): number {
const weights = {
majorImprovement: 2,
moderateImprovement: 1,
noEffect: 0,
worse: -1,
muchWorse: -2
};

const totalResponses =
treatment.majorImprovement +
treatment.moderateImprovement +
treatment.noEffect +
treatment.worse +
treatment.muchWorse;

if (totalResponses === 0) return 0;

const weightedSum =
weights.majorImprovement * treatment.majorImprovement +
weights.moderateImprovement * treatment.moderateImprovement +
weights.noEffect * treatment.noEffect +
weights.worse * treatment.worse +
weights.muchWorse * treatment.muchWorse;

// Normalize to a 0-100 scale
return ((weightedSum / totalResponses) + 2) * 25;
}

function toTitleCase(str: string): string {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
}

export default function TreatmentList({ condition }: TreatmentListProps) {

const [treatments, setTreatments] = useState(condition.conditionTreatments)
const [sortBy, setSortBy] = useState<'effectiveness' | 'popularity'>('effectiveness')

const handleSort = (type: 'effectiveness' | 'popularity') => {
setSortBy(type)
const sorted = [...treatments].sort((a, b) =>
type === 'popularity'
? b.popularity - a.popularity
: calculateEffectivenessScore(b) - calculateEffectivenessScore(a)
)
setTreatments(sorted)
}

const getConfidence = (popularity: number) => {
if (popularity > 50) return { level: 'HIGH', color: 'bg-green-500' };
if (popularity > 25) return { level: 'MEDIUM', color: 'bg-yellow-500' };
return { level: 'LOW', color: 'bg-red-500' };
}

return (
<>
<div className="flex justify-between items-center mb-4">
<h2
className={`text-xl font-semibold cursor-pointer ${sortBy === 'popularity' ? 'text-primary' : 'text-muted-foreground'}`}
onClick={() => handleSort('popularity')}
>
MOST TRIED
</h2>
<h2
className={`text-xl font-semibold cursor-pointer ${sortBy === 'effectiveness' ? 'text-primary' : 'text-muted-foreground'}`}
onClick={() => handleSort('effectiveness')}
>
HIGHEST AVERAGE RATING
</h2>
</div>
<Select>
<SelectTrigger className="w-full">
<SelectValue placeholder="All treatments" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All treatments</SelectItem>
</SelectContent>
</Select>
<div className="space-y-4 mt-4">
{treatments.map((treatment, index) => {
const effectivenessScore = calculateEffectivenessScore(treatment);
const confidence = getConfidence(treatment.popularity);
return (
<Card key={treatment.id} className="hover:shadow-lg transition-shadow duration-200">
<CardContent className="flex items-start p-4">
<div className="mr-4 flex flex-col items-center">
<span className="text-xs text-muted-foreground mb-1">Confidence</span>
<Badge className={`${confidence.color} text-white`}>
{confidence.level}
</Badge>
</div>
<div className="flex-grow">
<div className="text-primary font-bold">#{index + 1}</div>
<Link
href={`/dfda/treatments/${encodeURIComponent(treatment.treatment.name)}`}
className="hover:underline"
>
<h3 className="font-bold">{toTitleCase(treatment.treatment.name)}</h3>
</Link>
<div className="mt-2">
<div className="flex items-center">
<span className="text-xs text-muted-foreground mr-2">Effectiveness</span>
<div className="flex-grow bg-secondary rounded-full h-2.5">
<div
className="bg-primary h-2.5 rounded-full"
style={{ width: `${effectivenessScore}%` }}
></div>
</div>
<span className="text-xs font-semibold ml-2">{Math.round(effectivenessScore)}%</span>
</div>
</div>
</div>
</CardContent>
</Card>
)
})}
</div>
</>
)
}
18 changes: 13 additions & 5 deletions app/dfda/conditions/[conditionName]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
"use client"
import React from 'react'
import {getConditionByName} from "@/app/dfda/dfdaActions";
import TreatmentList from "@/app/dfda/components/TreatmentList";


export default async function ConditionPage({ params }: { params: { conditionName: string } }) {
// Decode the conditionName from the URL
const decodedConditionName = decodeURIComponent(params.conditionName);

const condition = await getConditionByName(decodedConditionName)

export default function ConditionPage() {
if (!condition) {
return <div>Condition not found</div>
}

return (
<div className="container mx-auto px-4 py-8 space-y-8">

<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">{condition.name}</h1>
<TreatmentList condition={condition} />
</div>
)
}
21 changes: 15 additions & 6 deletions app/dfda/conditions/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
"use client"
import React from 'react'
import Link from 'next/link'
import { fetchConditions } from '../dfdaActions'
export default async function ConditionListPage() {

export default async function ConditionListPage() {
const conditions = await fetchConditions()
const sortedConditions = conditions.sort((a, b) => b.numberOfTreatments - a.numberOfTreatments)

return (
<div className="container mx-auto px-4 py-8 space-y-8">
<h1 className="text-2xl font-bold">Conditions</h1>
<ul>
{conditions.map((condition) => (
<li key={condition.id}>{condition.name}</li>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{sortedConditions.map((condition) => (
<Link href={`/dfda/conditions/${condition.name}`} key={condition.id}>
<div className="card p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300">
<h2 className="text-lg font-semibold">{condition.name}</h2>
<span className="inline-block mt-2 text-xs font-semibold px-2 py-1 rounded-full bg-opacity-20">
{condition.numberOfTreatments} Treatments
</span>
</div>
</Link>
))}
</ul>
</div>
</div>
)
}
Loading

0 comments on commit 9ad3fed

Please sign in to comment.