1
+ import React , { useState , useEffect } from "react" ;
2
+ import { motion , useMotionValue , useTransform } from "framer-motion" ;
3
+ import Image from "next/image" ;
4
+
5
+ interface BlogCardData {
6
+ id : number ;
7
+ src : string ;
8
+ title : string ;
9
+ description : string ;
10
+ link : string ;
11
+ }
12
+
13
+ const blogCards : BlogCardData [ ] = [
14
+ {
15
+ id : 1 ,
16
+ src : "/camel-ai-agent-mcp-integration.png" ,
17
+ title : "How to Connect Your CAMEL-AI Agent to External Tools via MCP" ,
18
+ description : "Seamlessly integrate databases, APIs, and web services into your CAMEL-AI agents using the Model Context Protocol." ,
19
+ link : "https://www.camel-ai.org/blogs/camel-ai-agent-mcp-integration" ,
20
+ } ,
21
+ {
22
+ id : 2 ,
23
+ src : "/connect-your-owl-agent-to-notion-via-the-mcp-server.png" ,
24
+ title : "How to Connect Your OWL Agent to Notion via the MCP Server" ,
25
+ description : "Empower your CAMEL-AI OWL agent to interact with Notion using the MCP server." ,
26
+ link : "https://www.camel-ai.org/blogs/connect-your-owl-agent-to-notion-via-the-mcp-server" ,
27
+ } ,
28
+ {
29
+ id : 3 ,
30
+ src : "/camel-mcp-servers-model-context-protocol-ai-agents.png" ,
31
+ title : "CAMEL x MCP: Making AI Agents Accessible to All Tools" ,
32
+ description : "A lightweight server that exports Camel framework toolkits as MCP-compatible tools." ,
33
+ link : "https://www.camel-ai.org/blogs/camel-mcp-servers-model-context-protocol-ai-agents" ,
34
+ } ,
35
+ ] ;
36
+
37
+ const ArrowButton = ( { direction, onClick } : { direction : 'left' | 'right' , onClick : ( ) => void } ) => (
38
+ < button
39
+ onClick = { onClick }
40
+ aria-label = { direction === 'left' ? 'Previous card' : 'Next card' }
41
+ className = "mx-2 p-2 rounded-full bg-card border shadow hover:bg-muted transition-colors focus:outline-none focus:ring-2 focus:ring-primary"
42
+ type = "button"
43
+ >
44
+ { direction === 'left' ? (
45
+ < svg width = "24" height = "24" fill = "none" stroke = "currentColor" strokeWidth = "2" viewBox = "0 0 24 24" > < path d = "M15 19l-7-7 7-7" /> </ svg >
46
+ ) : (
47
+ < svg width = "24" height = "24" fill = "none" stroke = "currentColor" strokeWidth = "2" viewBox = "0 0 24 24" > < path d = "M9 5l7 7-7 7" /> </ svg >
48
+ ) }
49
+ </ button >
50
+ ) ;
51
+
52
+ const SwipeCards = ( ) => {
53
+ const [ cards , setCards ] = useState < BlogCardData [ ] > ( blogCards ) ;
54
+
55
+ // Auto-rotate cards effect
56
+ useEffect ( ( ) => {
57
+ const interval = setInterval ( ( ) => {
58
+ if ( cards . length > 0 ) {
59
+ setCards ( prevCards => {
60
+ const newCards = [ ...prevCards ] ;
61
+ const lastCard = newCards . pop ( ) ;
62
+ if ( lastCard ) {
63
+ newCards . unshift ( lastCard ) ;
64
+ }
65
+ return newCards ;
66
+ } ) ;
67
+ }
68
+ } , 10000 ) ;
69
+
70
+ return ( ) => clearInterval ( interval ) ;
71
+ } , [ cards ] ) ;
72
+
73
+ // Manual navigation handlers
74
+ const handlePrev = ( ) => {
75
+ setCards ( prevCards => {
76
+ if ( prevCards . length === 0 ) return prevCards ;
77
+ const newCards = [ ...prevCards ] ;
78
+ const firstCard = newCards . shift ( ) ;
79
+ if ( firstCard ) {
80
+ newCards . push ( firstCard ) ;
81
+ }
82
+ return newCards ;
83
+ } ) ;
84
+ } ;
85
+
86
+ const handleNext = ( ) => {
87
+ setCards ( prevCards => {
88
+ if ( prevCards . length === 0 ) return prevCards ;
89
+ const newCards = [ ...prevCards ] ;
90
+ const lastCard = newCards . pop ( ) ;
91
+ if ( lastCard ) {
92
+ newCards . unshift ( lastCard ) ;
93
+ }
94
+ return newCards ;
95
+ } ) ;
96
+ } ;
97
+
98
+ return (
99
+ < div className = "relative w-full h-[350px] flex flex-col items-center justify-center" >
100
+ < div className = "relative w-full h-full flex items-center justify-center" >
101
+ { cards . map ( ( card ) => (
102
+ < BlogSwipeCard key = { card . id } cards = { cards } setCards = { setCards } { ...card } />
103
+ ) ) }
104
+ </ div >
105
+ < div className = "flex items-center justify-center mt-12" >
106
+ < ArrowButton direction = "left" onClick = { handlePrev } />
107
+ < ArrowButton direction = "right" onClick = { handleNext } />
108
+ </ div >
109
+ </ div >
110
+ ) ;
111
+ } ;
112
+
113
+ const BlogSwipeCard = ( { id, src, title, description, link, setCards, cards } : BlogCardData & {
114
+ setCards : React . Dispatch < React . SetStateAction < BlogCardData [ ] > > ;
115
+ cards : BlogCardData [ ] ;
116
+ } ) => {
117
+ const x = useMotionValue ( 0 ) ;
118
+ const rotateRaw = useTransform ( x , [ - 150 , 150 ] , [ - 18 , 18 ] ) ;
119
+ const opacity = useTransform ( x , [ - 150 , 0 , 150 ] , [ 0 , 1 , 0 ] ) ;
120
+
121
+ const isFront = id === cards [ cards . length - 1 ] . id ;
122
+
123
+ const rotate = useTransform ( ( ) => {
124
+ const offset = isFront ? 0 : id % 2 ? 6 : - 6 ;
125
+ return `${ rotateRaw . get ( ) + offset } deg` ;
126
+ } ) ;
127
+
128
+ const handleDragEnd = ( ) => {
129
+ if ( Math . abs ( x . get ( ) ) > 100 ) {
130
+ setCards ( ( prevCards : BlogCardData [ ] ) => {
131
+ const filtered = prevCards . filter ( ( card : BlogCardData ) => card . id !== id ) ;
132
+ filtered . unshift ( { id, src, title, description, link } ) ;
133
+ return filtered ;
134
+ } ) ;
135
+ }
136
+ } ;
137
+
138
+ return (
139
+ < motion . div
140
+ className = "absolute w-full h-full flex items-center justify-center"
141
+ style = { {
142
+ x,
143
+ opacity,
144
+ rotate,
145
+ zIndex : isFront ? 1 : 0 ,
146
+ transition : "0.125s transform" ,
147
+ } }
148
+ animate = { {
149
+ scale : isFront ? 1 : 0.95 ,
150
+ } }
151
+ drag = { isFront ? "x" : false }
152
+ dragConstraints = { {
153
+ left : 0 ,
154
+ right : 0 ,
155
+ } }
156
+ onDragEnd = { handleDragEnd }
157
+ >
158
+ < div className = "flex-shrink-0 w-full max-w-xs lg:max-w-sm" >
159
+ < a
160
+ href = { link }
161
+ className = "block bg-card rounded-xl shadow-lg overflow-hidden border p-4 transition-all duration-300 hover:border-secondary focus:border-primary outline-none"
162
+ tabIndex = { 0 }
163
+ target = "_blank"
164
+ rel = "noopener noreferrer"
165
+ >
166
+ < div className = "relative w-full h-48 rounded-lg" >
167
+ < Image
168
+ src = { src }
169
+ alt = { title }
170
+ fill
171
+ style = { { objectFit : 'cover' , borderRadius : '0.5rem' } }
172
+ priority = { isFront }
173
+ />
174
+ </ div >
175
+ < div className = "mt-4" >
176
+ < h3 className = "text-xl font-semibold mb-2" > { title } </ h3 >
177
+ </ div >
178
+ </ a >
179
+ </ div >
180
+ </ motion . div >
181
+ ) ;
182
+ } ;
183
+
184
+ export default SwipeCards ;
0 commit comments