@fastnd/components 1.0.26 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +54 -0
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +120 -0
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +123 -0
- package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +70 -0
- package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +51 -0
- package/dist/examples/dashboard/constants.ts +18 -0
- package/dist/examples/dashboard/hooks/use-dashboard-state.ts +98 -0
- package/dist/examples/dashboard/index.ts +6 -0
- package/dist/examples/dashboard/types.ts +19 -0
- package/package.json +2 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import { StatusOverview } from '../StatusOverview/StatusOverview'
|
|
4
|
+
import { ProjectList } from '../ProjectList/ProjectList'
|
|
5
|
+
import { useDashboardState } from '../hooks/use-dashboard-state'
|
|
6
|
+
|
|
7
|
+
interface DashboardPageProps extends React.ComponentProps<'main'> {}
|
|
8
|
+
|
|
9
|
+
const DashboardPage = React.forwardRef<HTMLElement, DashboardPageProps>(
|
|
10
|
+
({ className, ...props }, ref) => {
|
|
11
|
+
const {
|
|
12
|
+
statusCounts,
|
|
13
|
+
total,
|
|
14
|
+
activeFilter,
|
|
15
|
+
setActiveFilter,
|
|
16
|
+
filteredProjects,
|
|
17
|
+
filterLabel,
|
|
18
|
+
searchQuery,
|
|
19
|
+
setSearchQuery,
|
|
20
|
+
toggleFavorite,
|
|
21
|
+
} = useDashboardState()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<main
|
|
25
|
+
ref={ref}
|
|
26
|
+
data-slot="dashboard-page"
|
|
27
|
+
className={cn(
|
|
28
|
+
'grid grid-cols-1 lg:grid-cols-[350px_1fr] gap-6 max-w-[1400px] mx-auto p-8',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
<StatusOverview
|
|
34
|
+
data={statusCounts}
|
|
35
|
+
total={total}
|
|
36
|
+
activeFilter={activeFilter}
|
|
37
|
+
onFilterChange={setActiveFilter}
|
|
38
|
+
/>
|
|
39
|
+
<ProjectList
|
|
40
|
+
projects={filteredProjects}
|
|
41
|
+
filterLabel={filterLabel}
|
|
42
|
+
searchQuery={searchQuery}
|
|
43
|
+
onSearchChange={setSearchQuery}
|
|
44
|
+
onToggleFavorite={toggleFavorite}
|
|
45
|
+
/>
|
|
46
|
+
</main>
|
|
47
|
+
)
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
DashboardPage.displayName = 'DashboardPage'
|
|
52
|
+
|
|
53
|
+
export { DashboardPage }
|
|
54
|
+
export type { DashboardPageProps }
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Star } from 'lucide-react'
|
|
3
|
+
import { Card, CardContent } from '@/components/ui/card'
|
|
4
|
+
import { Input } from '@/components/ui/input'
|
|
5
|
+
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from '@/components/ui/table'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
import { FavoriteButton } from '@/components/FavoriteButton/FavoriteButton'
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
9
|
+
import type { Project, ProjectStatus } from '../types'
|
|
10
|
+
import { STATUS_CONFIG } from '../constants'
|
|
11
|
+
|
|
12
|
+
const STATUS_BADGE_CLASSES: Record<ProjectStatus, string> = {
|
|
13
|
+
'neu': 'bg-primary/10 text-primary border-transparent',
|
|
14
|
+
'offen': 'bg-muted-foreground/10 text-muted-foreground border-transparent',
|
|
15
|
+
'in-prufung': 'bg-[#ebbe0d]/15 text-[#8a7100] border-transparent',
|
|
16
|
+
'validierung': 'bg-[#e8a026]/15 text-[#8a5a00] border-transparent',
|
|
17
|
+
'abgeschlossen': 'bg-[#1ec489]/15 text-[#0a6e44] border-transparent',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ProjectListProps extends React.ComponentProps<'section'> {
|
|
21
|
+
projects: Project[]
|
|
22
|
+
filterLabel: string
|
|
23
|
+
searchQuery: string
|
|
24
|
+
onSearchChange: (query: string) => void
|
|
25
|
+
onToggleFavorite: (projectId: string) => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ProjectList = React.forwardRef<HTMLElement, ProjectListProps>(
|
|
29
|
+
(
|
|
30
|
+
{
|
|
31
|
+
projects,
|
|
32
|
+
filterLabel,
|
|
33
|
+
searchQuery,
|
|
34
|
+
onSearchChange,
|
|
35
|
+
onToggleFavorite,
|
|
36
|
+
className,
|
|
37
|
+
...props
|
|
38
|
+
},
|
|
39
|
+
ref,
|
|
40
|
+
) => {
|
|
41
|
+
return (
|
|
42
|
+
<section
|
|
43
|
+
ref={ref}
|
|
44
|
+
data-slot="project-list"
|
|
45
|
+
aria-label="Projektliste"
|
|
46
|
+
className={cn('', className)}
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<Card>
|
|
50
|
+
<div className="flex justify-between items-center flex-wrap gap-4 px-6 pt-6">
|
|
51
|
+
<h2
|
|
52
|
+
className="text-2xl font-medium"
|
|
53
|
+
style={{ fontFamily: 'var(--font-clash)' }}
|
|
54
|
+
>
|
|
55
|
+
{filterLabel}
|
|
56
|
+
</h2>
|
|
57
|
+
<Input
|
|
58
|
+
type="search"
|
|
59
|
+
aria-label="Projekte durchsuchen"
|
|
60
|
+
placeholder="Suchen..."
|
|
61
|
+
className="w-[250px]"
|
|
62
|
+
value={searchQuery}
|
|
63
|
+
onChange={e => onSearchChange(e.target.value)}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
<CardContent>
|
|
67
|
+
<Table>
|
|
68
|
+
<TableHeader>
|
|
69
|
+
<TableRow>
|
|
70
|
+
<TableHead scope="col">Projektname</TableHead>
|
|
71
|
+
<TableHead scope="col">Kunde</TableHead>
|
|
72
|
+
<TableHead scope="col">Applikation</TableHead>
|
|
73
|
+
<TableHead scope="col">Status</TableHead>
|
|
74
|
+
<TableHead scope="col" aria-label="Favorit">
|
|
75
|
+
<Star className="size-3.5" />
|
|
76
|
+
</TableHead>
|
|
77
|
+
</TableRow>
|
|
78
|
+
</TableHeader>
|
|
79
|
+
<TableBody>
|
|
80
|
+
{projects.map(project => (
|
|
81
|
+
<TableRow key={project.id}>
|
|
82
|
+
<TableCell>
|
|
83
|
+
<span className="font-medium">{project.name}</span>
|
|
84
|
+
</TableCell>
|
|
85
|
+
<TableCell>
|
|
86
|
+
<span className="text-muted-foreground text-[0.8125rem]">
|
|
87
|
+
{project.client}
|
|
88
|
+
</span>
|
|
89
|
+
</TableCell>
|
|
90
|
+
<TableCell>{project.application}</TableCell>
|
|
91
|
+
<TableCell>
|
|
92
|
+
<Badge
|
|
93
|
+
variant="secondary"
|
|
94
|
+
className={cn(STATUS_BADGE_CLASSES[project.status])}
|
|
95
|
+
>
|
|
96
|
+
{STATUS_CONFIG[project.status].label}
|
|
97
|
+
</Badge>
|
|
98
|
+
</TableCell>
|
|
99
|
+
<TableCell>
|
|
100
|
+
<FavoriteButton
|
|
101
|
+
pressed={project.isFavorite}
|
|
102
|
+
projectName={project.name}
|
|
103
|
+
onPressedChange={() => onToggleFavorite(project.id)}
|
|
104
|
+
/>
|
|
105
|
+
</TableCell>
|
|
106
|
+
</TableRow>
|
|
107
|
+
))}
|
|
108
|
+
</TableBody>
|
|
109
|
+
</Table>
|
|
110
|
+
</CardContent>
|
|
111
|
+
</Card>
|
|
112
|
+
</section>
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
ProjectList.displayName = 'ProjectList'
|
|
118
|
+
|
|
119
|
+
export { ProjectList }
|
|
120
|
+
export type { ProjectListProps }
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Pie, PieChart, Label, Sector } from 'recharts'
|
|
3
|
+
import type { PieSectorShapeProps } from 'recharts/types/polar/Pie'
|
|
4
|
+
import {
|
|
5
|
+
ChartContainer,
|
|
6
|
+
ChartTooltip,
|
|
7
|
+
ChartTooltipContent,
|
|
8
|
+
type ChartConfig,
|
|
9
|
+
} from '@/components/ui/chart'
|
|
10
|
+
import { cn } from '@/lib/utils'
|
|
11
|
+
import type { StatusCount, StatusFilter } from '../types'
|
|
12
|
+
|
|
13
|
+
interface StatusDonutChartProps extends React.ComponentProps<'div'> {
|
|
14
|
+
data: StatusCount[]
|
|
15
|
+
total: number
|
|
16
|
+
activeFilter?: StatusFilter
|
|
17
|
+
onFilterChange?: (filter: StatusFilter) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const StatusDonutChart = React.forwardRef<HTMLDivElement, StatusDonutChartProps>(
|
|
21
|
+
({ data, total, activeFilter = 'all', onFilterChange, className, ...props }, ref) => {
|
|
22
|
+
const chartConfig = React.useMemo<ChartConfig>(() => {
|
|
23
|
+
return data.reduce<ChartConfig>((acc, item) => {
|
|
24
|
+
acc[item.status] = { label: item.label, color: item.color }
|
|
25
|
+
return acc
|
|
26
|
+
}, {})
|
|
27
|
+
}, [data])
|
|
28
|
+
|
|
29
|
+
const chartData = React.useMemo(() => {
|
|
30
|
+
return data.map((item) => ({
|
|
31
|
+
status: item.status,
|
|
32
|
+
count: item.count,
|
|
33
|
+
fill: `var(--color-${item.status})`,
|
|
34
|
+
}))
|
|
35
|
+
}, [data])
|
|
36
|
+
|
|
37
|
+
const activeIndex = activeFilter === 'all'
|
|
38
|
+
? -1
|
|
39
|
+
: chartData.findIndex((d) => d.status === activeFilter)
|
|
40
|
+
|
|
41
|
+
const handlePieClick = React.useCallback(
|
|
42
|
+
(_: unknown, index: number) => {
|
|
43
|
+
if (!onFilterChange) return
|
|
44
|
+
const clickedStatus = chartData[index]?.status
|
|
45
|
+
if (!clickedStatus) return
|
|
46
|
+
onFilterChange(clickedStatus === activeFilter ? 'all' : clickedStatus)
|
|
47
|
+
},
|
|
48
|
+
[onFilterChange, chartData, activeFilter],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
ref={ref}
|
|
54
|
+
data-slot="status-donut-chart"
|
|
55
|
+
role="img"
|
|
56
|
+
aria-label="Kreisdiagramm der Projektstatus-Verteilung"
|
|
57
|
+
className={cn(className)}
|
|
58
|
+
{...props}
|
|
59
|
+
>
|
|
60
|
+
<ChartContainer config={chartConfig} className="mx-auto aspect-square max-h-[200px]">
|
|
61
|
+
<PieChart>
|
|
62
|
+
<ChartTooltip cursor={false} content={<ChartTooltipContent hideLabel />} />
|
|
63
|
+
<Pie
|
|
64
|
+
data={chartData}
|
|
65
|
+
dataKey="count"
|
|
66
|
+
nameKey="status"
|
|
67
|
+
innerRadius={60}
|
|
68
|
+
outerRadius={85}
|
|
69
|
+
strokeWidth={5}
|
|
70
|
+
className="cursor-pointer"
|
|
71
|
+
onClick={handlePieClick}
|
|
72
|
+
shape={({
|
|
73
|
+
index,
|
|
74
|
+
outerRadius: or = 0,
|
|
75
|
+
...sectorProps
|
|
76
|
+
}: PieSectorShapeProps) => (
|
|
77
|
+
<Sector
|
|
78
|
+
{...sectorProps}
|
|
79
|
+
outerRadius={index === activeIndex ? or + 10 : or}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<Label
|
|
84
|
+
content={({ viewBox }) => {
|
|
85
|
+
if (viewBox && 'cx' in viewBox && 'cy' in viewBox) {
|
|
86
|
+
return (
|
|
87
|
+
<text
|
|
88
|
+
x={viewBox.cx}
|
|
89
|
+
y={viewBox.cy}
|
|
90
|
+
textAnchor="middle"
|
|
91
|
+
dominantBaseline="middle"
|
|
92
|
+
>
|
|
93
|
+
<tspan
|
|
94
|
+
x={viewBox.cx}
|
|
95
|
+
y={viewBox.cy}
|
|
96
|
+
className="fill-foreground text-3xl font-bold"
|
|
97
|
+
style={{ fontFamily: 'var(--font-clash)' }}
|
|
98
|
+
>
|
|
99
|
+
{total.toLocaleString()}
|
|
100
|
+
</tspan>
|
|
101
|
+
<tspan
|
|
102
|
+
x={viewBox.cx}
|
|
103
|
+
y={(viewBox.cy || 0) + 24}
|
|
104
|
+
className="fill-muted-foreground text-xs uppercase tracking-wider"
|
|
105
|
+
>
|
|
106
|
+
Gesamt
|
|
107
|
+
</tspan>
|
|
108
|
+
</text>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
</Pie>
|
|
114
|
+
</PieChart>
|
|
115
|
+
</ChartContainer>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
StatusDonutChart.displayName = 'StatusDonutChart'
|
|
122
|
+
|
|
123
|
+
export { StatusDonutChart }
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
import type { StatusCount, StatusFilter } from '../types'
|
|
4
|
+
|
|
5
|
+
interface StatusFilterLegendProps extends React.ComponentProps<'div'> {
|
|
6
|
+
items: StatusCount[]
|
|
7
|
+
total: number
|
|
8
|
+
activeFilter: StatusFilter
|
|
9
|
+
onFilterChange: (filter: StatusFilter) => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const StatusFilterLegend = React.forwardRef<HTMLDivElement, StatusFilterLegendProps>(
|
|
13
|
+
({ items, total, activeFilter, onFilterChange, className, ...props }, ref) => {
|
|
14
|
+
const allItem = { filter: 'all' as StatusFilter, label: 'Alle', count: total, color: null }
|
|
15
|
+
|
|
16
|
+
const entries = [
|
|
17
|
+
allItem,
|
|
18
|
+
...items.map((item) => ({
|
|
19
|
+
filter: item.status as StatusFilter,
|
|
20
|
+
label: item.label,
|
|
21
|
+
count: item.count,
|
|
22
|
+
color: item.color,
|
|
23
|
+
})),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
ref={ref}
|
|
29
|
+
role="listbox"
|
|
30
|
+
aria-label="Nach Status filtern"
|
|
31
|
+
data-slot="status-filter-legend"
|
|
32
|
+
className={cn('flex flex-col gap-1', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{entries.map(({ filter, label, count, color }) => {
|
|
36
|
+
const isActive = activeFilter === filter
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<button
|
|
40
|
+
key={filter}
|
|
41
|
+
role="option"
|
|
42
|
+
aria-selected={isActive}
|
|
43
|
+
data-filter={filter}
|
|
44
|
+
onClick={() => onFilterChange(filter)}
|
|
45
|
+
className={cn(
|
|
46
|
+
'flex items-center justify-between w-full px-3 py-2 rounded-md border border-transparent transition-colors cursor-pointer text-sm outline-none focus-visible:bg-muted',
|
|
47
|
+
isActive ? 'bg-muted border-border' : 'hover:bg-muted',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<span className="flex items-center gap-2">
|
|
51
|
+
<span
|
|
52
|
+
className={cn('shrink-0 rounded-full', color === null && 'bg-border')}
|
|
53
|
+
style={color !== null ? { backgroundColor: color, width: 12, height: 12 } : { width: 12, height: 12 }}
|
|
54
|
+
/>
|
|
55
|
+
<span>{label}</span>
|
|
56
|
+
</span>
|
|
57
|
+
<span className="font-[var(--font-grotesk)] text-sm text-muted-foreground font-semibold bg-secondary px-2 py-0.5 rounded-full">
|
|
58
|
+
{count}
|
|
59
|
+
</span>
|
|
60
|
+
</button>
|
|
61
|
+
)
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
StatusFilterLegend.displayName = 'StatusFilterLegend'
|
|
69
|
+
|
|
70
|
+
export { StatusFilterLegend }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
import { StatusDonutChart } from '../StatusDonutChart/StatusDonutChart'
|
|
5
|
+
import { StatusFilterLegend } from '../StatusFilterLegend/StatusFilterLegend'
|
|
6
|
+
import type { StatusCount, StatusFilter } from '../types'
|
|
7
|
+
|
|
8
|
+
interface StatusOverviewProps extends React.ComponentProps<'section'> {
|
|
9
|
+
data: StatusCount[]
|
|
10
|
+
total: number
|
|
11
|
+
activeFilter: StatusFilter
|
|
12
|
+
onFilterChange: (filter: StatusFilter) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const StatusOverview = React.forwardRef<HTMLElement, StatusOverviewProps>(
|
|
16
|
+
({ data, total, activeFilter, onFilterChange, className, ...props }, ref) => {
|
|
17
|
+
return (
|
|
18
|
+
<section
|
|
19
|
+
ref={ref}
|
|
20
|
+
data-slot="status-overview"
|
|
21
|
+
aria-label="Projektstatus Übersicht"
|
|
22
|
+
className={cn(className)}
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<Card>
|
|
26
|
+
<CardHeader>
|
|
27
|
+
<CardTitle
|
|
28
|
+
className="text-2xl font-medium"
|
|
29
|
+
style={{ fontFamily: 'var(--font-clash)' }}
|
|
30
|
+
>
|
|
31
|
+
Projekte
|
|
32
|
+
</CardTitle>
|
|
33
|
+
</CardHeader>
|
|
34
|
+
<CardContent className="flex flex-col gap-4">
|
|
35
|
+
<StatusDonutChart data={data} total={total} activeFilter={activeFilter} onFilterChange={onFilterChange} />
|
|
36
|
+
<StatusFilterLegend
|
|
37
|
+
items={data}
|
|
38
|
+
total={total}
|
|
39
|
+
activeFilter={activeFilter}
|
|
40
|
+
onFilterChange={onFilterChange}
|
|
41
|
+
/>
|
|
42
|
+
</CardContent>
|
|
43
|
+
</Card>
|
|
44
|
+
</section>
|
|
45
|
+
)
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
StatusOverview.displayName = 'StatusOverview'
|
|
50
|
+
|
|
51
|
+
export { StatusOverview }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ProjectStatus, Project } from './types'
|
|
2
|
+
|
|
3
|
+
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: string }> = {
|
|
4
|
+
'neu': { label: 'Neu', color: '#0073e6' },
|
|
5
|
+
'offen': { label: 'Offen', color: '#576472' },
|
|
6
|
+
'in-prufung': { label: 'In Prüfung', color: '#ebbe0d' },
|
|
7
|
+
'validierung': { label: 'Validierung', color: '#e8a026' },
|
|
8
|
+
'abgeschlossen': { label: 'Abgeschlossen', color: '#1ec489' },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const MOCK_PROJECTS: Project[] = [
|
|
12
|
+
{ id: '1', name: 'E-Commerce Relaunch', client: 'Müller GmbH', application: 'Shopify CMS', status: 'neu', isFavorite: false },
|
|
13
|
+
{ id: '2', name: 'CRM Integration MVP', client: 'TechCorp Inc.', application: 'Salesforce API', status: 'offen', isFavorite: true },
|
|
14
|
+
{ id: '3', name: 'Data Analytics Dashboard', client: 'DataViz AG', application: 'React / Supabase', status: 'in-prufung', isFavorite: false },
|
|
15
|
+
{ id: '4', name: 'Mobile App Onboarding', client: 'FitHealth', application: 'Flutter', status: 'neu', isFavorite: false },
|
|
16
|
+
{ id: '5', name: 'Payment Gateway Update', client: 'FinTech Solutions', application: 'Stripe API', status: 'validierung', isFavorite: false },
|
|
17
|
+
{ id: '6', name: 'Legacy System Migration', client: 'OldBank Corp.', application: 'Node.js / PostgreSQL', status: 'abgeschlossen', isFavorite: true },
|
|
18
|
+
]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react'
|
|
2
|
+
import type { Project, StatusFilter, StatusCount } from '../types'
|
|
3
|
+
import { STATUS_CONFIG, MOCK_PROJECTS } from '../constants'
|
|
4
|
+
|
|
5
|
+
export interface DashboardState {
|
|
6
|
+
projects: Project[]
|
|
7
|
+
activeFilter: StatusFilter
|
|
8
|
+
searchQuery: string
|
|
9
|
+
statusCounts: StatusCount[]
|
|
10
|
+
total: number
|
|
11
|
+
filteredProjects: Project[]
|
|
12
|
+
filterLabel: string
|
|
13
|
+
setActiveFilter: (filter: StatusFilter) => void
|
|
14
|
+
setSearchQuery: (query: string) => void
|
|
15
|
+
toggleFavorite: (projectId: string) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const STATUS_ORDER: Array<keyof typeof STATUS_CONFIG> = [
|
|
19
|
+
'neu',
|
|
20
|
+
'offen',
|
|
21
|
+
'in-prufung',
|
|
22
|
+
'validierung',
|
|
23
|
+
'abgeschlossen',
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export function useDashboardState(initialProjects: Project[] = MOCK_PROJECTS): DashboardState {
|
|
27
|
+
const [projects, setProjects] = useState<Project[]>(initialProjects)
|
|
28
|
+
const [activeFilter, setActiveFilter] = useState<StatusFilter>('all')
|
|
29
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
30
|
+
|
|
31
|
+
const statusCounts = useMemo<StatusCount[]>(
|
|
32
|
+
() =>
|
|
33
|
+
STATUS_ORDER.map((status) => ({
|
|
34
|
+
status,
|
|
35
|
+
label: STATUS_CONFIG[status].label,
|
|
36
|
+
color: STATUS_CONFIG[status].color,
|
|
37
|
+
count: projects.filter((p) => p.status === status).length,
|
|
38
|
+
})),
|
|
39
|
+
[projects],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const total = useMemo(() => projects.length, [projects])
|
|
43
|
+
|
|
44
|
+
const filteredProjects = useMemo<Project[]>(() => {
|
|
45
|
+
const query = searchQuery.toLowerCase()
|
|
46
|
+
|
|
47
|
+
const matches = projects.filter((p) => {
|
|
48
|
+
const matchesFilter = activeFilter === 'all' || p.status === activeFilter
|
|
49
|
+
const matchesSearch =
|
|
50
|
+
query === '' ||
|
|
51
|
+
p.name.toLowerCase().includes(query) ||
|
|
52
|
+
p.client.toLowerCase().includes(query) ||
|
|
53
|
+
p.application.toLowerCase().includes(query)
|
|
54
|
+
return matchesFilter && matchesSearch
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
if (activeFilter === 'all') {
|
|
58
|
+
return [...matches].sort((a, b) => Number(b.isFavorite) - Number(a.isFavorite))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return matches
|
|
62
|
+
}, [projects, activeFilter, searchQuery])
|
|
63
|
+
|
|
64
|
+
const filterLabel = useMemo(
|
|
65
|
+
() =>
|
|
66
|
+
activeFilter === 'all'
|
|
67
|
+
? 'Alle Projekte'
|
|
68
|
+
: `Projekte: ${STATUS_CONFIG[activeFilter].label}`,
|
|
69
|
+
[activeFilter],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const handleSetActiveFilter = useCallback((filter: StatusFilter) => {
|
|
73
|
+
setActiveFilter(filter)
|
|
74
|
+
}, [])
|
|
75
|
+
|
|
76
|
+
const handleSetSearchQuery = useCallback((query: string) => {
|
|
77
|
+
setSearchQuery(query)
|
|
78
|
+
}, [])
|
|
79
|
+
|
|
80
|
+
const toggleFavorite = useCallback((projectId: string) => {
|
|
81
|
+
setProjects((prev) =>
|
|
82
|
+
prev.map((p) => (p.id === projectId ? { ...p, isFavorite: !p.isFavorite } : p)),
|
|
83
|
+
)
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
projects,
|
|
88
|
+
activeFilter,
|
|
89
|
+
searchQuery,
|
|
90
|
+
statusCounts,
|
|
91
|
+
total,
|
|
92
|
+
filteredProjects,
|
|
93
|
+
filterLabel,
|
|
94
|
+
setActiveFilter: handleSetActiveFilter,
|
|
95
|
+
setSearchQuery: handleSetSearchQuery,
|
|
96
|
+
toggleFavorite,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { DashboardPage } from './DashboardPage/DashboardPage'
|
|
2
|
+
export { StatusOverview } from './StatusOverview/StatusOverview'
|
|
3
|
+
export { StatusDonutChart } from './StatusDonutChart/StatusDonutChart'
|
|
4
|
+
export { StatusFilterLegend } from './StatusFilterLegend/StatusFilterLegend'
|
|
5
|
+
export { ProjectList } from './ProjectList/ProjectList'
|
|
6
|
+
export type { ProjectStatus, StatusFilter, Project, StatusCount } from './types'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type ProjectStatus = 'neu' | 'offen' | 'in-prufung' | 'validierung' | 'abgeschlossen'
|
|
2
|
+
|
|
3
|
+
export type StatusFilter = ProjectStatus | 'all'
|
|
4
|
+
|
|
5
|
+
export interface Project {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
client: string
|
|
9
|
+
application: string
|
|
10
|
+
status: ProjectStatus
|
|
11
|
+
isFavorite: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface StatusCount {
|
|
15
|
+
status: ProjectStatus
|
|
16
|
+
label: string
|
|
17
|
+
count: number
|
|
18
|
+
color: string
|
|
19
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fastnd/components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/fastnd-components.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "vite build && tsc -p tsconfig.build.json",
|
|
23
|
+
"postbuild": "node scripts/copy-examples.mjs",
|
|
23
24
|
"storybook": "storybook dev -p 6006",
|
|
24
25
|
"build-storybook": "storybook build",
|
|
25
26
|
"test": "vitest",
|