@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.
@@ -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.26",
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",