Skip to content

Commit e2b509d

Browse files
committed
feat: add pattern assistant
1 parent 789b550 commit e2b509d

33 files changed

+2157
-10
lines changed

app/[lang]/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ const fontMono = FontMono({
3434

3535
export const metadata = metadataSEO
3636

37-
export default async function RootLayout({ children, params }) {
37+
export default async function RootLayout({ children, params }: {
38+
children: React.ReactNode
39+
params: Promise<{ lang: string }>
40+
}) {
3841
const { lang } = await params
3942
const dictionary = await getDictionary(lang)
4043

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { DecisionEdge, DecisionNode } from '../types'
2+
3+
export const nodes: DecisionNode[] = [
4+
{
5+
id: '1',
6+
type: 'question',
7+
data: {
8+
label: 'What is your primary content type?',
9+
description: 'Consider how users typically consume your content'
10+
},
11+
position: { x: 0, y: 0 }
12+
},
13+
{
14+
id: '2',
15+
type: 'question',
16+
data: {
17+
label: 'Is SEO critical for your content?',
18+
description: 'Consider if content needs to be easily discoverable and indexed'
19+
},
20+
position: { x: -200, y: 100 }
21+
},
22+
{
23+
id: '3',
24+
type: 'question',
25+
data: {
26+
label: 'Do users need to bookmark/share specific items?',
27+
description: 'Consider if users need to return to specific content'
28+
},
29+
position: { x: 200, y: 100 }
30+
},
31+
{
32+
id: '4',
33+
type: 'pattern',
34+
data: {
35+
label: 'Pagination',
36+
description: 'Best for structured, searchable content with clear navigation',
37+
patternLink: '/patterns/navigation/pagination#implementation'
38+
},
39+
position: { x: -300, y: 200 }
40+
},
41+
{
42+
id: '5',
43+
type: 'pattern',
44+
data: {
45+
label: 'Infinite Scroll',
46+
description: 'Ideal for continuous browsing and discovery',
47+
patternLink: '/patterns/navigation/infinite-scroll#implementation'
48+
},
49+
position: { x: 300, y: 200 }
50+
},
51+
{
52+
id: '6',
53+
type: 'consideration',
54+
data: {
55+
label: 'Consider Hybrid Approach',
56+
description: 'Load More button with limited infinite scroll',
57+
patternLink: '/patterns/navigation/infinite-scroll#hybrid-approach'
58+
},
59+
position: { x: 0, y: 300 }
60+
},
61+
{
62+
id: '7',
63+
type: 'question',
64+
data: {
65+
label: 'Is mobile the primary platform?',
66+
description: 'Consider touch interfaces and screen size'
67+
},
68+
position: { x: 100, y: 150 }
69+
},
70+
{
71+
id: '8',
72+
type: 'consideration',
73+
data: {
74+
label: 'Performance Considerations',
75+
description: 'Memory usage, loading states, and scroll position management',
76+
patternLink: '/patterns/navigation/pagination#performance'
77+
},
78+
position: { x: -100, y: 250 }
79+
}
80+
]
81+
82+
export const edges: DecisionEdge[] = [
83+
// Content type decisions
84+
{
85+
id: 'e1-2',
86+
source: '1',
87+
target: '2',
88+
label: 'Structured Content'
89+
},
90+
{
91+
id: 'e1-3',
92+
source: '1',
93+
target: '3',
94+
label: 'Social/Feed Content'
95+
},
96+
97+
// SEO path
98+
{
99+
id: 'e2-4',
100+
source: '2',
101+
target: '4',
102+
label: 'Yes'
103+
},
104+
{
105+
id: 'e2-8',
106+
source: '2',
107+
target: '8',
108+
label: 'No'
109+
},
110+
111+
// Bookmarking path
112+
{
113+
id: 'e3-7',
114+
source: '3',
115+
target: '7',
116+
label: 'No'
117+
},
118+
{
119+
id: 'e3-4',
120+
source: '3',
121+
target: '4',
122+
label: 'Yes'
123+
},
124+
125+
// Mobile consideration
126+
{
127+
id: 'e7-5',
128+
source: '7',
129+
target: '5',
130+
label: 'Yes'
131+
},
132+
{
133+
id: 'e7-6',
134+
source: '7',
135+
target: '6',
136+
label: 'No'
137+
},
138+
139+
// Performance to hybrid
140+
{
141+
id: 'e8-6',
142+
source: '8',
143+
target: '6',
144+
label: 'Consider'
145+
}
146+
]
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client'
2+
3+
import dagre from 'dagre'
4+
import { useCallback } from 'react'
5+
import ReactFlow, {
6+
Background,
7+
ConnectionMode,
8+
Controls,
9+
Edge,
10+
Node,
11+
useEdgesState,
12+
useNodesState,
13+
} from 'reactflow'
14+
import 'reactflow/dist/style.css'
15+
16+
import { ConsiderationNode } from './nodes/ConsiderationNode'
17+
import { PatternNode } from './nodes/PatternNode'
18+
import { QuestionNode } from './nodes/QuestionNode'
19+
import type { DecisionFlowProps } from './types'
20+
21+
const nodeTypes = {
22+
question: QuestionNode,
23+
pattern: PatternNode,
24+
consideration: ConsiderationNode,
25+
}
26+
27+
// Custom edge style
28+
const edgeStyles = {
29+
stroke: 'var(--edge-stroke)',
30+
strokeWidth: 2,
31+
}
32+
33+
// Custom edge label style
34+
const edgeLabelStyles = {
35+
fill: 'var(--edge-label-text)',
36+
fontWeight: 500,
37+
fontSize: 12,
38+
}
39+
40+
// Helper function to layout nodes using dagre
41+
const getLayoutedElements = (nodes: Node[], edges: Edge[]) => {
42+
const dagreGraph = new dagre.graphlib.Graph()
43+
dagreGraph.setDefaultEdgeLabel(() => ({}))
44+
45+
// Increase spacing between nodes
46+
dagreGraph.setGraph({
47+
rankdir: 'TB',
48+
ranksep: 150, // Increased vertical spacing
49+
nodesep: 200, // Increased horizontal spacing
50+
edgesep: 100, // Increased edge spacing
51+
})
52+
53+
// Increase node size estimates for better spacing
54+
nodes.forEach((node) => {
55+
dagreGraph.setNode(node.id, { width: 300, height: 120 })
56+
})
57+
58+
edges.forEach((edge) => {
59+
dagreGraph.setEdge(edge.source, edge.target)
60+
})
61+
62+
dagre.layout(dagreGraph)
63+
64+
const layoutedNodes = nodes.map((node) => {
65+
const nodeWithPosition = dagreGraph.node(node.id)
66+
return {
67+
...node,
68+
position: {
69+
x: nodeWithPosition.x - 150, // Center node by subtracting half the width
70+
y: nodeWithPosition.y,
71+
},
72+
}
73+
})
74+
75+
// Add styling to edges
76+
const styledEdges = edges.map((edge) => ({
77+
...edge,
78+
style: edgeStyles,
79+
labelStyle: edgeLabelStyles,
80+
labelBgStyle: {
81+
fill: 'var(--edge-label-bg)',
82+
rx: 4, // rounded corners
83+
ry: 4
84+
},
85+
labelBgPadding: [8, 4],
86+
labelBgBorderRadius: 4,
87+
type: 'default', // Use smooth default edges instead of smoothstep
88+
})) as Edge[]
89+
90+
return { nodes: layoutedNodes, edges: styledEdges }
91+
}
92+
93+
export function DecisionFlow({ nodes: initialNodes, edges: initialEdges, className = '' }: DecisionFlowProps) {
94+
// Layout the nodes on first render
95+
const { nodes: layoutedNodes, edges: layoutedEdges } = getLayoutedElements(
96+
initialNodes,
97+
initialEdges
98+
)
99+
100+
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes)
101+
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges)
102+
103+
const onInit = useCallback(() => {
104+
// Any initialization logic
105+
}, [])
106+
107+
return (
108+
<div className={`w-full h-[800px] ${className}`}>
109+
<ReactFlow
110+
nodes={nodes}
111+
edges={edges}
112+
onNodesChange={onNodesChange}
113+
onEdgesChange={onEdgesChange}
114+
onInit={onInit}
115+
nodeTypes={nodeTypes}
116+
connectionMode={ConnectionMode.Strict}
117+
fitView
118+
minZoom={0.5}
119+
maxZoom={1.5}
120+
defaultViewport={{ x: 0, y: 0, zoom: 0.7 }} // Reduced default zoom
121+
attributionPosition="bottom-left"
122+
>
123+
<Background color="var(--flow-background-dots)" gap={16} />
124+
<Controls className="bg-white dark:bg-gray-800" />
125+
</ReactFlow>
126+
</div>
127+
)
128+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client'
2+
3+
import { AlertCircle } from 'lucide-react'
4+
import Link from 'next/link'
5+
import { Handle, Position } from 'reactflow'
6+
import type { NodeData } from '../types'
7+
8+
interface ConsiderationNodeProps {
9+
data: NodeData
10+
isConnectable: boolean
11+
}
12+
13+
export function ConsiderationNode({ data, isConnectable }: ConsiderationNodeProps) {
14+
return (
15+
<div className="px-4 py-2 shadow-lg rounded-md border border-yellow-200 dark:border-yellow-500/30 bg-yellow-50/80 dark:bg-yellow-900/20 backdrop-blur-sm">
16+
<Handle
17+
type="target"
18+
position={Position.Top}
19+
isConnectable={isConnectable}
20+
className="w-2 h-2 !bg-yellow-500"
21+
/>
22+
23+
<div className="flex items-center gap-2">
24+
<AlertCircle className="w-4 h-4 text-yellow-600 dark:text-yellow-400" />
25+
{data.patternLink ? (
26+
<Link
27+
href={data.patternLink}
28+
className="text-sm font-medium text-yellow-700 hover:text-yellow-800 dark:text-yellow-200 dark:hover:text-yellow-100 transition-colors"
29+
>
30+
{data.label}
31+
</Link>
32+
) : (
33+
<div className="text-sm font-medium text-yellow-700 dark:text-yellow-200">{data.label}</div>
34+
)}
35+
</div>
36+
37+
{data.description && (
38+
<div className="mt-2 text-xs text-yellow-600/80 dark:text-yellow-200/80">
39+
{data.description}
40+
</div>
41+
)}
42+
43+
<Handle
44+
type="source"
45+
position={Position.Bottom}
46+
isConnectable={isConnectable}
47+
className="w-2 h-2 !bg-yellow-500"
48+
/>
49+
</div>
50+
)
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use client'
2+
3+
import { Component } from 'lucide-react'
4+
import Link from 'next/link'
5+
import { Handle, Position } from 'reactflow'
6+
import type { NodeData } from '../types'
7+
8+
interface PatternNodeProps {
9+
data: NodeData
10+
isConnectable: boolean
11+
}
12+
13+
export function PatternNode({ data, isConnectable }: PatternNodeProps) {
14+
return (
15+
<div className="px-4 py-2 shadow-lg rounded-md border border-indigo-200 dark:border-indigo-500/30 bg-indigo-50/80 dark:bg-indigo-900/30 backdrop-blur-sm">
16+
<Handle
17+
type="target"
18+
position={Position.Top}
19+
isConnectable={isConnectable}
20+
className="w-2 h-2 !bg-indigo-500"
21+
/>
22+
23+
<div className="flex items-center gap-2">
24+
<Component className="w-4 h-4 text-indigo-600 dark:text-indigo-400" />
25+
{data.patternLink ? (
26+
<Link
27+
href={data.patternLink}
28+
className="text-sm font-medium text-indigo-700 hover:text-indigo-800 dark:text-indigo-300 dark:hover:text-indigo-200 transition-colors"
29+
>
30+
{data.label}
31+
</Link>
32+
) : (
33+
<div className="text-sm font-medium text-indigo-700 dark:text-indigo-300">{data.label}</div>
34+
)}
35+
</div>
36+
37+
{data.description && (
38+
<div className="mt-2 text-xs text-indigo-600/80 dark:text-indigo-300/80">
39+
{data.description}
40+
</div>
41+
)}
42+
43+
<Handle
44+
type="source"
45+
position={Position.Bottom}
46+
isConnectable={isConnectable}
47+
className="w-2 h-2 !bg-indigo-500"
48+
/>
49+
</div>
50+
)
51+
}

0 commit comments

Comments
 (0)