major update
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { RankingEntry } from '../../lib/types';
|
||||
|
||||
@@ -5,18 +6,31 @@ interface RankingsTableProps {
|
||||
rankings: RankingEntry[];
|
||||
}
|
||||
|
||||
function scoreColor(score: number): string {
|
||||
function scoreColor(score: number | null): string {
|
||||
if (score === null || score === undefined) return 'text-gray-600';
|
||||
if (score > 70) return 'text-emerald-400';
|
||||
if (score >= 40) return 'text-amber-400';
|
||||
return 'text-red-400';
|
||||
}
|
||||
|
||||
function capitalize(s: string): string {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
export function RankingsTable({ rankings }: RankingsTableProps) {
|
||||
if (rankings.length === 0) {
|
||||
return <p className="py-8 text-center text-sm text-gray-500">No rankings available.</p>;
|
||||
}
|
||||
|
||||
const dimensionNames = rankings.length > 0 ? rankings[0].dimensions.map((d) => d.dimension) : [];
|
||||
const dimensionNames = useMemo(() => {
|
||||
const nameSet = new Set<string>();
|
||||
for (const entry of rankings) {
|
||||
for (const d of entry.dimensions) {
|
||||
nameSet.add(d.dimension);
|
||||
}
|
||||
}
|
||||
return Array.from(nameSet);
|
||||
}, [rankings]);
|
||||
|
||||
return (
|
||||
<div className="glass overflow-x-auto">
|
||||
@@ -27,7 +41,7 @@ export function RankingsTable({ rankings }: RankingsTableProps) {
|
||||
<th className="px-4 py-3">Symbol</th>
|
||||
<th className="px-4 py-3">Composite</th>
|
||||
{dimensionNames.map((dim) => (
|
||||
<th key={dim} className="px-4 py-3">{dim}</th>
|
||||
<th key={dim} className="px-4 py-3">{capitalize(dim)}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -43,11 +57,15 @@ export function RankingsTable({ rankings }: RankingsTableProps) {
|
||||
<td className={`px-4 py-3.5 font-semibold ${scoreColor(entry.composite_score)}`}>
|
||||
{Math.round(entry.composite_score)}
|
||||
</td>
|
||||
{entry.dimensions.map((dim) => (
|
||||
<td key={dim.dimension} className={`px-4 py-3.5 font-mono ${scoreColor(dim.score)}`}>
|
||||
{Math.round(dim.score)}
|
||||
</td>
|
||||
))}
|
||||
{dimensionNames.map((dim) => {
|
||||
const found = entry.dimensions.find((d) => d.dimension === dim);
|
||||
const score = found ? found.score : null;
|
||||
return (
|
||||
<td key={dim} className={`px-4 py-3.5 font-mono ${scoreColor(score)}`}>
|
||||
{score !== null ? Math.round(score) : 'N/A'}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, type FormEvent } from 'react';
|
||||
import { useState, useMemo, type FormEvent } from 'react';
|
||||
import { useUpdateWeights } from '../../hooks/useScores';
|
||||
|
||||
interface WeightsFormProps {
|
||||
@@ -6,17 +6,33 @@ interface WeightsFormProps {
|
||||
}
|
||||
|
||||
export function WeightsForm({ weights }: WeightsFormProps) {
|
||||
const [localWeights, setLocalWeights] = useState<Record<string, number>>(weights);
|
||||
// Convert API decimal weights (0-1) to 0-100 integer scale on mount
|
||||
const [sliderValues, setSliderValues] = useState<Record<string, number>>(() =>
|
||||
Object.fromEntries(
|
||||
Object.entries(weights).map(([key, w]) => [key, Math.round(w * 100)])
|
||||
)
|
||||
);
|
||||
const updateWeights = useUpdateWeights();
|
||||
|
||||
const allZero = useMemo(
|
||||
() => Object.values(sliderValues).every((v) => v === 0),
|
||||
[sliderValues]
|
||||
);
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
const num = parseFloat(value);
|
||||
if (!isNaN(num)) setLocalWeights((prev) => ({ ...prev, [key]: num }));
|
||||
const num = parseInt(value, 10);
|
||||
if (!isNaN(num)) setSliderValues((prev) => ({ ...prev, [key]: num }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
updateWeights.mutate(localWeights);
|
||||
if (allZero) return;
|
||||
|
||||
const total = Object.values(sliderValues).reduce((sum, v) => sum + v, 0);
|
||||
const normalized = Object.fromEntries(
|
||||
Object.entries(sliderValues).map(([key, v]) => [key, v / total])
|
||||
);
|
||||
updateWeights.mutate(normalized);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -24,23 +40,35 @@ export function WeightsForm({ weights }: WeightsFormProps) {
|
||||
<h3 className="mb-4 text-xs font-semibold uppercase tracking-widest text-gray-500">
|
||||
Scoring Weights
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{Object.keys(weights).map((key) => (
|
||||
<label key={key} className="flex flex-col gap-1.5">
|
||||
<span className="text-xs text-gray-400 capitalize">{key.replace(/_/g, ' ')}</span>
|
||||
<input
|
||||
type="number"
|
||||
step="any"
|
||||
value={localWeights[key] ?? 0}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
className="input-glass px-2.5 py-1.5 text-sm"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={sliderValues[key] ?? 0}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
className="h-2 w-full cursor-pointer appearance-none rounded-lg bg-gray-700 accent-indigo-500"
|
||||
/>
|
||||
<span className="min-w-[2ch] text-right text-sm font-medium text-gray-300">
|
||||
{sliderValues[key] ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{allZero && (
|
||||
<p className="mt-3 text-xs text-red-400">
|
||||
At least one weight must be greater than zero
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={updateWeights.isPending}
|
||||
disabled={updateWeights.isPending || allZero}
|
||||
className="mt-4 btn-gradient px-4 py-2 text-sm disabled:opacity-50"
|
||||
>
|
||||
<span>{updateWeights.isPending ? 'Updating…' : 'Update Weights'}</span>
|
||||
|
||||
Reference in New Issue
Block a user