Skip to content

Commit ad2cfc3

Browse files
committed
feat: ui good. functionality not yet
1 parent ab4f414 commit ad2cfc3

5 files changed

Lines changed: 324 additions & 60 deletions

File tree

package-lock.json

Lines changed: 55 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
"lint": "eslint"
1010
},
1111
"dependencies": {
12+
"@dnd-kit/core": "^6.3.1",
13+
"@dnd-kit/sortable": "^10.0.0",
1214
"next": "16.0.7",
1315
"react": "19.2.0",
1416
"react-dom": "19.2.0"

src/app/components/CssRush.tsx

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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;

src/app/components/targets.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
export const targets = [
2+
{
3+
id: 'target-1',
4+
html: `
5+
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
6+
<p style="font-size: 2.25rem; font-weight: 700; color: #3b82f6;">Hello, World!</p>
7+
</div>
8+
`,
9+
solution: [
10+
'<div>',
11+
'<p>',
12+
'Hello, World!',
13+
'</p>',
14+
'</div>',
15+
'<style>',
16+
'div { display: flex; justify-content: center; align-items: center; height: 100%; }',
17+
'p { font-size: 2.25rem; font-weight: 700; color: #3b82f6; }',
18+
'</style>',
19+
]
20+
},
21+
{
22+
id: 'target-2',
23+
html: `
24+
<div style="background-color: #ef4444; width: 8rem; height: 8rem;"></div>
25+
`,
26+
solution: [
27+
'<div>',
28+
'</div>',
29+
'<style>',
30+
'div { background-color: #ef4444; width: 8rem; height: 8rem; }',
31+
'</style>',
32+
]
33+
},
34+
{
35+
id: 'target-3',
36+
html: `
37+
<div style="display: flex; justify-content: space-around; align-items: center; width: 100%; height: 100%; background-color: #f3f4f6;">
38+
<div style="background-color: #3b82f6; width: 4rem; height: 4rem; border-radius: 9999px;"></div>
39+
<div style="background-color: #ef4444; width: 4rem; height: 4rem;"></div>
40+
<div style="background-color: #10b981; width: 4rem; height: 4rem; transform: rotate(45deg);"></div>
41+
</div>
42+
`,
43+
solution: [
44+
'<div>',
45+
'<div></div>',
46+
'<div></div>',
47+
'<div></div>',
48+
'</div>',
49+
'<style>',
50+
'div:first-child { display: flex; justify-content: space-around; align-items: center; width: 100%; height: 100%; background-color: #f3f4f6; }',
51+
'div:first-child div:nth-child(1) { background-color: #3b82f6; width: 4rem; height: 4rem; border-radius: 9999px; }',
52+
'div:first-child div:nth-child(2) { background-color: #ef4444; width: 4rem; height: 4rem; }',
53+
'div:first-child div:nth-child(3) { background-color: #10b981; width: 4rem; height: 4rem; transform: rotate(45deg); }',
54+
'</style>',
55+
]
56+
}
57+
];

0 commit comments

Comments
 (0)