1+ "use client" ;
2+ import React , { useState , useEffect } from 'react' ;
3+ import { DndContext , closestCenter , KeyboardSensor , PointerSensor , useSensor , useSensors , useDraggable , useDroppable } from '@dnd-kit/core' ;
4+ import { SortableContext , sortableKeyboardCoordinates , arrayMove , useSortable } from '@dnd-kit/sortable' ;
5+ import { targets } from './targets' ;
6+
7+ // --- Draggable Block Component ---
8+ function DraggableBlock ( { id, content } : { id : string ; content : string } ) {
9+ const { attributes, listeners, setNodeRef, transform } = useDraggable ( {
10+ id : id ,
11+ data : { content : content }
12+ } ) ;
13+ const style = transform ? {
14+ transform : `translate3d(${ transform . x } px, ${ transform . y } px, 0)` ,
15+ } : undefined ;
16+
17+ return (
18+ < div ref = { setNodeRef } style = { style } { ...listeners } { ...attributes } className = "bg-white p-2 rounded shadow-md cursor-grab" >
19+ < code > { content } </ code >
20+ </ div >
21+ ) ;
22+ }
23+
24+ // --- Sortable Item Component ---
25+ function SortableItem ( { id, content } : { id : string ; content : string } ) {
26+ const {
27+ attributes,
28+ listeners,
29+ setNodeRef,
30+ transform,
31+ transition,
32+ } = useSortable ( { id : id } ) ;
33+
34+ const style = {
35+ transform : transform ? `translate3d(${ transform . x } px, ${ transform . y } px, 0)` : undefined ,
36+ transition,
37+ } ;
38+
39+ return (
40+ < div ref = { setNodeRef } style = { style } { ...attributes } { ...listeners } className = "p-2 mb-2 bg-green-100 rounded shadow-sm" >
41+ < code > { content } </ code >
42+ </ div >
43+ ) ;
44+ }
45+
46+
47+ const createBlock = ( content : string , id : string ) => ( { id, content } ) ;
48+
49+ const getInitialBlocks = ( ) => [
50+ createBlock ( '<div>' , 'block-1' ) ,
51+ createBlock ( '<p>' , 'block-2' ) ,
52+ createBlock ( '</p>' , 'block-3' ) ,
53+ createBlock ( '</div>' , 'block-4' ) ,
54+ createBlock ( '<style>' , 'block-5' ) ,
55+ createBlock ( '</style>' , 'block-6' ) ,
56+ createBlock ( 'display: flex;' , 'block-7' ) ,
57+ createBlock ( 'justify-content: center;' , 'block-8' ) ,
58+ createBlock ( 'align-items: center;' , 'block-9' ) ,
59+ createBlock ( 'background-color: #ef4444;' , 'block-10' ) ,
60+ createBlock ( 'width: 8rem;' , 'block-11' ) ,
61+ createBlock ( 'height: 8rem;' , 'block-12' ) ,
62+ createBlock ( 'color: #3b82f6;' , 'block-13' ) ,
63+ createBlock ( 'font-size: 2.25rem;' , 'block-14' ) ,
64+ createBlock ( 'font-weight: 700;' , 'block-15' ) ,
65+ createBlock ( 'Hello, World!' , 'block-16' ) ,
66+ ] ;
67+
68+ const CssRush = ( ) => {
69+ // --- State Variables ---
70+ const [ blocks , setBlocks ] = useState ( getInitialBlocks ( ) ) ;
71+ const [ code , setCode ] = useState < { id : string ; content : string } [ ] > ( [ ] ) ;
72+ const [ timer , setTimer ] = useState ( 180 ) ;
73+ const [ score , setScore ] = useState ( 0 ) ;
74+ const [ isClient , setIsClient ] = useState ( false ) ;
75+ const [ showTarget , setShowTarget ] = useState ( false ) ;
76+ const [ currentTarget , setCurrentTarget ] = useState ( targets [ 0 ] ) ;
77+
78+ // --- Effects ---
79+ useEffect ( ( ) => {
80+ setIsClient ( true ) ;
81+ } , [ ] ) ;
82+
83+ useEffect ( ( ) => {
84+ const interval = setInterval ( ( ) => {
85+ setTimer ( ( prev ) => ( prev > 0 ? prev - 1 : 0 ) ) ;
86+ } , 1000 ) ;
87+ return ( ) => clearInterval ( interval ) ;
88+ } , [ ] ) ;
89+
90+ useEffect ( ( ) => {
91+ if ( code . length > 0 ) {
92+ const userCode = code . map ( ( c ) => c . content ) . join ( '' ) ;
93+ const solution = currentTarget . solution . join ( '' ) ;
94+ if ( userCode === solution ) {
95+ setScore ( score + 1 ) ;
96+ const nextTargetIndex = ( targets . indexOf ( currentTarget ) + 1 ) % targets . length ;
97+ setCurrentTarget ( targets [ nextTargetIndex ] ) ;
98+ setCode ( [ ] ) ;
99+ }
100+ }
101+ } , [ code , score , currentTarget ] ) ;
102+
103+ // --- Drag and Drop Logic ---
104+ const sensors = useSensors (
105+ useSensor ( PointerSensor ) ,
106+ useSensor ( KeyboardSensor , {
107+ coordinateGetter : sortableKeyboardCoordinates ,
108+ } )
109+ ) ;
110+
111+ function handleDragEnd ( event : any ) {
112+ const { active, over } = event ;
113+
114+ if ( over ) {
115+ if ( over . id === 'code-space' ) {
116+ // Dragging from blocks to code space
117+ if ( active . data . current . content ) {
118+ const newId = `code-${ new Date ( ) . getTime ( ) } ` ;
119+ setCode ( ( oldCode ) => [ ...oldCode , { id : newId , content : active . data . current . content } ] ) ;
120+ }
121+ } else {
122+ // Reordering within code space
123+ const oldIndex = code . findIndex ( ( c ) => c . id === active . id ) ;
124+ const newIndex = code . findIndex ( ( c ) => c . id === over . id ) ;
125+
126+ if ( oldIndex !== - 1 && newIndex !== - 1 ) {
127+ setCode ( ( items ) => arrayMove ( items , oldIndex , newIndex ) ) ;
128+ }
129+ }
130+ }
131+ }
132+
133+ const { setNodeRef } = useDroppable ( {
134+ id : 'code-space' ,
135+ } ) ;
136+
137+
138+ // --- Render ---
139+ const userHtml = code . map ( ( c ) => c . content ) . join ( '' ) ;
140+
141+ return (
142+ < div className = "font-sans" >
143+ < div className = "grid grid-cols-2 gap-4 h-screen" >
144+ { /* Left Column */ }
145+ < div className = "flex flex-col p-4 bg-gray-50" >
146+ < h1 className = "text-3xl font-bold text-gray-800 mb-4" > CSS Rush</ h1 >
147+ < div className = "flex justify-between items-center mb-4 p-2 bg-white rounded-lg shadow-sm" >
148+ < div className = "text-lg font-semibold text-gray-700" >
149+ Time: < span className = "text-blue-500" > { timer } s</ span >
150+ </ div >
151+ < div className = "text-lg font-semibold text-gray-700" >
152+ Score: < span className = "text-green-500" > { score } </ span >
153+ </ div >
154+ </ div >
155+ { isClient && (
156+ < DndContext sensors = { sensors } collisionDetection = { closestCenter } onDragEnd = { handleDragEnd } >
157+ < div className = "flex flex-wrap gap-2 p-2 bg-gray-200 rounded-lg mb-4" >
158+ { blocks . map ( ( block ) => (
159+ < DraggableBlock key = { block . id } id = { block . id } content = { block . content } />
160+ ) ) }
161+ </ div >
162+ < h2 className = "text-2xl font-semibold text-gray-800 mb-2" > Code Space</ h2 >
163+ < div ref = { setNodeRef } className = "flex-grow p-4 bg-white rounded-lg shadow-inner border-2 border-gray-300" >
164+ < SortableContext items = { code . map ( i => i . id ) } >
165+ { code . map ( ( block ) => (
166+ < SortableItem key = { block . id } id = { block . id } content = { block . content } />
167+ ) ) }
168+ </ SortableContext >
169+ </ div >
170+ </ DndContext >
171+ ) }
172+ </ div >
173+
174+ { /* Right Column */ }
175+ < div className = "p-4 bg-gray-50 flex flex-col" >
176+ < div className = "flex-grow relative"
177+ onMouseEnter = { ( ) => setShowTarget ( true ) }
178+ onMouseLeave = { ( ) => setShowTarget ( false ) }
179+ >
180+ < div className = "w-full h-full bg-white rounded-lg shadow-inner border-2 border-gray-300" >
181+ < iframe
182+ title = "User Output"
183+ srcDoc = { userHtml }
184+ className = "w-full h-full rounded-lg"
185+ />
186+ </ div >
187+ { showTarget && (
188+ < div className = "absolute top-0 left-0 w-full h-full bg-white rounded-lg shadow-inner border-2 border-blue-400" >
189+ < iframe
190+ title = "Target Output"
191+ srcDoc = { currentTarget . html }
192+ className = "w-full h-full rounded-lg"
193+ />
194+ </ div >
195+ ) }
196+ </ div >
197+ < div className = "flex justify-center items-center h-10 mt-2 text-gray-600" >
198+ Hover to see the target design
199+ </ div >
200+ </ div >
201+ </ div >
202+ </ div >
203+ ) ;
204+ } ;
205+
206+ export default CssRush ;
0 commit comments