| Epic | Status | Wat is gedaan |
|---------------------|---------------|---------------------| | E0: Foundation | ✅ Afgerond | Types + DB migratie | | E1: Leefgebieden | ✅ Afgerond | 3 componenten | | E2: AI Generatie | ⏳ Nog te doen | - | | E3: Behandelplan UI | ⏳ Nog te doen | - | Gemaakte bestanden: - lib/types/leefgebieden.ts - 7 domeinen met kleuren/emoji's - lib/types/behandelplan.ts - SMART doelen, interventies, Zod schemas - components/behandelplan/leefgebieden-form.tsx - Intake formulier - components/behandelplan/leefgebieden-scores.tsx - Score weergave - components/behandelplan/leefgebieden-badge.tsx - Domain badges - components/behandelplan/index.ts - Exports
This commit is contained in:
14
components/behandelplan/index.ts
Normal file
14
components/behandelplan/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Behandelplan Components
|
||||
*
|
||||
* Export all behandelplan-related components
|
||||
*/
|
||||
|
||||
// Leefgebieden (Life Domains)
|
||||
export { LeefgebiedenBadge, LeefgebiedenBadgeGroup } from './leefgebieden-badge';
|
||||
export {
|
||||
LeefgebiedenScores,
|
||||
LeefgebiedenScoresCard,
|
||||
LeefgebiedenScoreBar,
|
||||
} from './leefgebieden-scores';
|
||||
export { LeefgebiedenForm, LeefgebiedenQuickForm } from './leefgebieden-form';
|
||||
77
components/behandelplan/leefgebieden-badge.tsx
Normal file
77
components/behandelplan/leefgebieden-badge.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { type LifeDomain, LIFE_DOMAIN_META } from '@/lib/types/leefgebieden';
|
||||
|
||||
interface LeefgebiedenBadgeProps {
|
||||
domain: LifeDomain;
|
||||
showEmoji?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Colored badge for a life domain
|
||||
* Uses the domain's specific color from the meta definition
|
||||
*/
|
||||
export function LeefgebiedenBadge({
|
||||
domain,
|
||||
showEmoji = true,
|
||||
size = 'md',
|
||||
className,
|
||||
}: LeefgebiedenBadgeProps) {
|
||||
const meta = LIFE_DOMAIN_META[domain];
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'text-xs px-1.5 py-0.5',
|
||||
md: 'text-sm px-2 py-0.5',
|
||||
lg: 'text-base px-3 py-1',
|
||||
};
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={cn(
|
||||
'font-medium border-0 text-white',
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
style={{ backgroundColor: meta.color }}
|
||||
>
|
||||
{showEmoji && <span className="mr-1">{meta.emoji}</span>}
|
||||
{meta.shortLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeefgebiedenBadgeGroupProps {
|
||||
domains: LifeDomain[];
|
||||
showEmoji?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group of life domain badges
|
||||
*/
|
||||
export function LeefgebiedenBadgeGroup({
|
||||
domains,
|
||||
showEmoji = true,
|
||||
size = 'sm',
|
||||
className,
|
||||
}: LeefgebiedenBadgeGroupProps) {
|
||||
if (domains.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-wrap gap-1', className)}>
|
||||
{domains.map((domain) => (
|
||||
<LeefgebiedenBadge
|
||||
key={domain}
|
||||
domain={domain}
|
||||
showEmoji={showEmoji}
|
||||
size={size}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
368
components/behandelplan/leefgebieden-form.tsx
Normal file
368
components/behandelplan/leefgebieden-form.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
type LifeDomainScore,
|
||||
type LifeDomain,
|
||||
type Priority,
|
||||
LIFE_DOMAIN_META,
|
||||
LIFE_DOMAINS,
|
||||
createDefaultLifeDomainScores,
|
||||
getScoreColor,
|
||||
} from '@/lib/types/leefgebieden';
|
||||
|
||||
interface DomainFormRowProps {
|
||||
score: LifeDomainScore;
|
||||
onChange: (score: LifeDomainScore) => void;
|
||||
expanded?: boolean;
|
||||
onToggleExpand?: () => void;
|
||||
}
|
||||
|
||||
const SCORE_LABELS: Record<number, string> = {
|
||||
1: 'Zeer laag',
|
||||
2: 'Laag',
|
||||
3: 'Gemiddeld',
|
||||
4: 'Goed',
|
||||
5: 'Uitstekend',
|
||||
};
|
||||
|
||||
const PRIORITY_OPTIONS: { value: Priority; label: string; color: string }[] = [
|
||||
{ value: 'laag', label: 'Laag', color: 'bg-gray-200 text-gray-700' },
|
||||
{ value: 'middel', label: 'Middel', color: 'bg-blue-100 text-blue-700' },
|
||||
{ value: 'hoog', label: 'Hoog', color: 'bg-orange-100 text-orange-700' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Single domain row in the form
|
||||
*/
|
||||
function DomainFormRow({
|
||||
score,
|
||||
onChange,
|
||||
expanded = false,
|
||||
onToggleExpand,
|
||||
}: DomainFormRowProps) {
|
||||
const meta = LIFE_DOMAIN_META[score.domain];
|
||||
|
||||
const handleBaselineChange = (values: number[]) => {
|
||||
onChange({ ...score, baseline: values[0], current: values[0] });
|
||||
};
|
||||
|
||||
const handleTargetChange = (values: number[]) => {
|
||||
onChange({ ...score, target: values[0] });
|
||||
};
|
||||
|
||||
const handlePriorityChange = (priority: Priority) => {
|
||||
onChange({ ...score, priority });
|
||||
};
|
||||
|
||||
const handleNotesChange = (notes: string) => {
|
||||
onChange({ ...score, notes });
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border rounded-lg p-4 transition-all',
|
||||
expanded ? 'bg-muted/50' : 'hover:bg-muted/30',
|
||||
score.priority === 'hoog' && 'border-orange-300'
|
||||
)}
|
||||
>
|
||||
{/* Header Row */}
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer"
|
||||
onClick={onToggleExpand}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-xl"
|
||||
style={{ backgroundColor: `${meta.color}20` }}
|
||||
>
|
||||
{meta.emoji}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium">{meta.label}</h4>
|
||||
<p className="text-sm text-muted-foreground">{meta.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-lg font-bold"
|
||||
style={{ color: getScoreColor(score.baseline) }}
|
||||
>
|
||||
{score.baseline}
|
||||
</span>
|
||||
<span className="text-muted-foreground">→</span>
|
||||
<span className="text-lg font-bold text-foreground">
|
||||
{score.target}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{SCORE_LABELS[score.baseline]}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
className={cn(
|
||||
'w-5 h-5 text-muted-foreground transition-transform',
|
||||
expanded && 'rotate-180'
|
||||
)}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{expanded && (
|
||||
<div className="mt-4 space-y-4 pt-4 border-t">
|
||||
{/* Baseline Score Slider */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>Huidige score (baseline)</Label>
|
||||
<span className="text-sm font-medium">{score.baseline}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[score.baseline]}
|
||||
onValueChange={handleBaselineChange}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Zeer laag</span>
|
||||
<span>Uitstekend</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Target Score Slider */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<Label>Doelscore</Label>
|
||||
<span className="text-sm font-medium">{score.target}</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[score.target]}
|
||||
onValueChange={handleTargetChange}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Zeer laag</span>
|
||||
<span>Uitstekend</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Priority Selection */}
|
||||
<div className="space-y-2">
|
||||
<Label>Prioriteit voor behandeling</Label>
|
||||
<div className="flex gap-2">
|
||||
{PRIORITY_OPTIONS.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-md text-sm font-medium transition-all',
|
||||
score.priority === option.value
|
||||
? option.color
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
)}
|
||||
onClick={() => handlePriorityChange(option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<div className="space-y-2">
|
||||
<Label>Toelichting (optioneel)</Label>
|
||||
<Textarea
|
||||
value={score.notes}
|
||||
onChange={(e) => handleNotesChange(e.target.value)}
|
||||
placeholder={`Opmerkingen over ${meta.shortLabel.toLowerCase()}...`}
|
||||
className="resize-none"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeefgebiedenFormProps {
|
||||
initialScores?: LifeDomainScore[];
|
||||
onSave: (scores: LifeDomainScore[]) => void;
|
||||
onCancel?: () => void;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete form for entering life domain scores during intake
|
||||
*/
|
||||
export function LeefgebiedenForm({
|
||||
initialScores,
|
||||
onSave,
|
||||
onCancel,
|
||||
isSaving = false,
|
||||
}: LeefgebiedenFormProps) {
|
||||
const [scores, setScores] = useState<LifeDomainScore[]>(
|
||||
initialScores || createDefaultLifeDomainScores()
|
||||
);
|
||||
const [expandedDomain, setExpandedDomain] = useState<LifeDomain | null>(null);
|
||||
|
||||
const handleScoreChange = useCallback((updatedScore: LifeDomainScore) => {
|
||||
setScores((prev) =>
|
||||
prev.map((s) => (s.domain === updatedScore.domain ? updatedScore : s))
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleToggleExpand = useCallback((domain: LifeDomain) => {
|
||||
setExpandedDomain((prev) => (prev === domain ? null : domain));
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave(scores);
|
||||
};
|
||||
|
||||
const highPriorityCount = scores.filter((s) => s.priority === 'hoog').length;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span>📊</span>
|
||||
Leefgebieden Assessment
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Beoordeel de 7 leefgebieden van de cliënt. Klik op een gebied om scores
|
||||
en prioriteiten aan te passen.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{/* Summary Stats */}
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-4">
|
||||
<span>
|
||||
Klik op een leefgebied om de scores aan te passen
|
||||
</span>
|
||||
{highPriorityCount > 0 && (
|
||||
<span className="text-orange-600 font-medium">
|
||||
{highPriorityCount} hoge prioriteit{highPriorityCount > 1 ? 'en' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Domain Rows */}
|
||||
<div className="space-y-2">
|
||||
{LIFE_DOMAINS.map((domain) => {
|
||||
const score = scores.find((s) => s.domain === domain)!;
|
||||
return (
|
||||
<DomainFormRow
|
||||
key={domain}
|
||||
score={score}
|
||||
onChange={handleScoreChange}
|
||||
expanded={expandedDomain === domain}
|
||||
onToggleExpand={() => handleToggleExpand(domain)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2 pt-4 border-t">
|
||||
{onCancel && (
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Annuleren
|
||||
</Button>
|
||||
)}
|
||||
<Button type="submit" disabled={isSaving}>
|
||||
{isSaving ? 'Opslaan...' : 'Leefgebieden opslaan'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeefgebiedenQuickFormProps {
|
||||
initialScores?: LifeDomainScore[];
|
||||
onChange: (scores: LifeDomainScore[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact version of the form for inline editing
|
||||
*/
|
||||
export function LeefgebiedenQuickForm({
|
||||
initialScores,
|
||||
onChange,
|
||||
}: LeefgebiedenQuickFormProps) {
|
||||
const [scores, setScores] = useState<LifeDomainScore[]>(
|
||||
initialScores || createDefaultLifeDomainScores()
|
||||
);
|
||||
|
||||
const handleScoreChange = useCallback(
|
||||
(domain: LifeDomain, value: number) => {
|
||||
const updated = scores.map((s) =>
|
||||
s.domain === domain ? { ...s, baseline: value, current: value } : s
|
||||
);
|
||||
setScores(updated);
|
||||
onChange(updated);
|
||||
},
|
||||
[scores, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{LIFE_DOMAINS.map((domain) => {
|
||||
const score = scores.find((s) => s.domain === domain)!;
|
||||
const meta = LIFE_DOMAIN_META[domain];
|
||||
return (
|
||||
<div key={domain} className="flex items-center gap-3">
|
||||
<div className="w-8 text-center text-lg">{meta.emoji}</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span>{meta.shortLabel}</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ color: getScoreColor(score.baseline) }}
|
||||
>
|
||||
{score.baseline}
|
||||
</span>
|
||||
</div>
|
||||
<Slider
|
||||
value={[score.baseline]}
|
||||
onValueChange={(v) => handleScoreChange(domain, v[0])}
|
||||
min={1}
|
||||
max={5}
|
||||
step={1}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
186
components/behandelplan/leefgebieden-scores.tsx
Normal file
186
components/behandelplan/leefgebieden-scores.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
'use client';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
type LifeDomainScore,
|
||||
type LifeDomain,
|
||||
LIFE_DOMAIN_META,
|
||||
LIFE_DOMAINS,
|
||||
getScoreColor,
|
||||
getAverageScore,
|
||||
} from '@/lib/types/leefgebieden';
|
||||
import { LeefgebiedenBadge } from './leefgebieden-badge';
|
||||
|
||||
interface LeefgebiedenScoreBarProps {
|
||||
score: LifeDomainScore;
|
||||
showTarget?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single life domain score as a progress bar
|
||||
*/
|
||||
export function LeefgebiedenScoreBar({
|
||||
score,
|
||||
showTarget = true,
|
||||
compact = false,
|
||||
}: LeefgebiedenScoreBarProps) {
|
||||
const meta = LIFE_DOMAIN_META[score.domain];
|
||||
const percentage = (score.current / 5) * 100;
|
||||
const targetPercentage = (score.target / 5) * 100;
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-1', compact ? 'py-1' : 'py-2')}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg">{meta.emoji}</span>
|
||||
<span className={cn('font-medium', compact ? 'text-sm' : 'text-base')}>
|
||||
{meta.shortLabel}
|
||||
</span>
|
||||
{score.priority === 'hoog' && (
|
||||
<span className="text-xs text-orange-500 font-medium">
|
||||
Prioriteit
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span className="font-medium" style={{ color: getScoreColor(score.current) }}>
|
||||
{score.current}
|
||||
</span>
|
||||
{showTarget && (
|
||||
<>
|
||||
<span>→</span>
|
||||
<span className="font-medium text-foreground">{score.target}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
{/* Custom progress bar with domain-specific color */}
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-primary/20">
|
||||
<div
|
||||
className="h-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: meta.color,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{showTarget && (
|
||||
<div
|
||||
className="absolute top-0 h-2 w-0.5 bg-foreground/50"
|
||||
style={{ left: `${targetPercentage}%` }}
|
||||
title={`Doel: ${score.target}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeefgebiedenScoresProps {
|
||||
scores: LifeDomainScore[];
|
||||
showTarget?: boolean;
|
||||
compact?: boolean;
|
||||
showSummary?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display all 7 life domain scores
|
||||
*/
|
||||
export function LeefgebiedenScores({
|
||||
scores,
|
||||
showTarget = true,
|
||||
compact = false,
|
||||
showSummary = true,
|
||||
className,
|
||||
}: LeefgebiedenScoresProps) {
|
||||
// Ensure we have all 7 domains in the correct order
|
||||
const orderedScores = LIFE_DOMAINS.map((domain) => {
|
||||
const found = scores.find((s) => s.domain === domain);
|
||||
return (
|
||||
found || {
|
||||
domain,
|
||||
baseline: 3,
|
||||
current: 3,
|
||||
target: 4,
|
||||
notes: '',
|
||||
priority: 'middel' as const,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const avgCurrent = getAverageScore(orderedScores, 'current');
|
||||
const avgTarget = getAverageScore(orderedScores, 'target');
|
||||
const highPriority = orderedScores.filter((s) => s.priority === 'hoog');
|
||||
|
||||
return (
|
||||
<div className={cn('space-y-4', className)}>
|
||||
{showSummary && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-muted-foreground">
|
||||
Gemiddelde score:{' '}
|
||||
<span className="font-medium text-foreground">{avgCurrent}</span>
|
||||
{showTarget && (
|
||||
<span className="text-muted-foreground"> → {avgTarget}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
{highPriority.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-muted-foreground">Prioriteiten:</span>
|
||||
{highPriority.map((s) => (
|
||||
<LeefgebiedenBadge key={s.domain} domain={s.domain} size="sm" />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
{orderedScores.map((score) => (
|
||||
<LeefgebiedenScoreBar
|
||||
key={score.domain}
|
||||
score={score}
|
||||
showTarget={showTarget}
|
||||
compact={compact}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LeefgebiedenScoresCardProps {
|
||||
scores: LifeDomainScore[];
|
||||
title?: string;
|
||||
showTarget?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Life domain scores in a Card wrapper
|
||||
*/
|
||||
export function LeefgebiedenScoresCard({
|
||||
scores,
|
||||
title = 'Leefgebieden',
|
||||
showTarget = true,
|
||||
className,
|
||||
}: LeefgebiedenScoresCardProps) {
|
||||
return (
|
||||
<Card className={className}>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
<span>📊</span>
|
||||
{title}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LeefgebiedenScores scores={scores} showTarget={showTarget} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
36
components/ui/badge.tsx
Normal file
36
components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
76
components/ui/card.tsx
Normal file
76
components/ui/card.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("font-semibold leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
26
components/ui/label.tsx
Normal file
26
components/ui/label.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
28
components/ui/progress.tsx
Normal file
28
components/ui/progress.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
28
components/ui/slider.tsx
Normal file
28
components/ui/slider.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-1.5 w-full grow overflow-hidden rounded-full bg-primary/20">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-4 w-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
55
components/ui/tabs.tsx
Normal file
55
components/ui/tabs.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
22
components/ui/textarea.tsx
Normal file
22
components/ui/textarea.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Textarea = React.forwardRef<
|
||||
HTMLTextAreaElement,
|
||||
React.ComponentProps<"textarea">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Textarea.displayName = "Textarea"
|
||||
|
||||
export { Textarea }
|
||||
Reference in New Issue
Block a user