forked from wishonia/wishonia
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add comprehensive treatment and condition search functionality
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
Showing
8 changed files
with
511 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
Oops, something went wrong.