initial commit

This commit is contained in:
Fatih Kadir Akın
2026-01-14 21:09:10 +03:00
parent 1c421bb66d
commit 33f55d70c2
4 changed files with 600 additions and 31 deletions

View File

@@ -495,6 +495,89 @@
animation: float 4s ease-in-out infinite;
}
/* Flying plane animation for kids map */
@keyframes flyPlane {
0% {
left: -200px;
}
100% {
left: 100%;
}
}
/* Flying plane animation across full map width */
@keyframes flyPlaneMap {
0% {
left: -250px;
}
100% {
left: var(--map-width, 100%);
}
}
/* Engine vibration animation for vehicles */
@keyframes engineVibrate {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-1px);
}
}
/* Propeller spin animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Car driving animations */
@keyframes driveLeft {
0% {
left: var(--map-width, 100%);
}
100% {
left: -80px;
}
}
@keyframes driveRight {
0% {
left: -80px;
}
100% {
left: var(--map-width, 100%);
}
}
/* Pixelated flag wave animation - broken wave effect */
@keyframes flagWave {
0% {
transform: skewX(0deg) scaleX(1);
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 4px 50%);
}
25% {
transform: skewX(-2deg) scaleX(0.98);
clip-path: polygon(0 2px, calc(100% - 10px) 0, 100% 45%, calc(100% - 6px) 100%, 0 calc(100% - 2px), 6px 50%);
}
50% {
transform: skewX(0deg) scaleX(1);
clip-path: polygon(0 0, calc(100% - 6px) 2px, 100% 55%, calc(100% - 10px) calc(100% - 2px), 0 100%, 4px 45%);
}
75% {
transform: skewX(2deg) scaleX(0.98);
clip-path: polygon(0 2px, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 calc(100% - 2px), 4px 55%);
}
100% {
transform: skewX(0deg) scaleX(1);
clip-path: polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 4px 50%);
}
}
/* Cloud animations for kids background */
@keyframes cloud-drift {
0% {

View File

@@ -360,3 +360,255 @@ export function PixelPath({ width = 100, className }: { width?: number; classNam
</svg>
);
}
// Pixel Art Sports Car
export function PixelCar({ className, color = "#E74C3C" }: { className?: string; color?: string }) {
// Darker shade for details
const darkColor = color === "#E74C3C" ? "#C0392B" : color === "#27AE60" ? "#1E8449" : "#2471A3";
return (
<svg
viewBox="0 0 36 14"
className={cn("w-28 h-11", className)}
style={{ imageRendering: "pixelated" }}
>
{/* Shadow */}
<rect x="4" y="12" width="28" height="2" fill="#00000033" />
{/* Car body - low sleek profile */}
<rect x="2" y="6" width="32" height="6" fill={color} />
{/* Sloped hood */}
<rect x="26" y="5" width="8" height="2" fill={color} />
{/* Low cabin */}
<rect x="10" y="3" width="14" height="4" fill={color} />
{/* Body details/shading */}
<rect x="2" y="10" width="32" height="2" fill={darkColor} />
{/* Windshield - angled */}
<rect x="20" y="3" width="4" height="3" fill="#87CEEB" />
{/* Rear window */}
<rect x="12" y="4" width="4" height="2" fill="#87CEEB" />
{/* Wheels - larger sporty */}
<rect x="4" y="10" width="6" height="4" fill="#222" />
<rect x="5" y="11" width="4" height="2" fill="#555" />
<rect x="26" y="10" width="6" height="4" fill="#222" />
<rect x="27" y="11" width="4" height="2" fill="#555" />
{/* Headlights */}
<rect x="32" y="6" width="2" height="2" fill="#FFD700" />
{/* Taillights */}
<rect x="2" y="7" width="2" height="2" fill="#DC143C" />
{/* Spoiler */}
<rect x="2" y="4" width="6" height="2" fill={darkColor} />
<rect x="4" y="2" width="2" height="2" fill={darkColor} />
</svg>
);
}
// Pixel Art Old Classic Car (Beetle style)
export function PixelOldCar({ className, color = "#27AE60" }: { className?: string; color?: string }) {
const darkColor = color === "#27AE60" ? "#1E8449" : "#1a5276";
return (
<svg
viewBox="0 0 28 14"
className={cn("w-22 h-11", className)}
style={{ imageRendering: "pixelated" }}
>
{/* Shadow */}
<rect x="4" y="12" width="20" height="2" fill="#00000033" />
{/* Car body - rounded beetle shape */}
<rect x="2" y="6" width="24" height="6" fill={color} />
{/* Rounded roof */}
<rect x="6" y="2" width="16" height="6" fill={color} />
<rect x="8" y="1" width="12" height="2" fill={color} />
{/* Body shading */}
<rect x="2" y="10" width="24" height="2" fill={darkColor} />
{/* Front fender bump */}
<rect x="20" y="5" width="6" height="2" fill={color} />
{/* Rear fender bump */}
<rect x="2" y="5" width="6" height="2" fill={color} />
{/* Windshield - rounded */}
<rect x="16" y="3" width="4" height="3" fill="#87CEEB" />
{/* Rear window */}
<rect x="8" y="3" width="4" height="3" fill="#87CEEB" />
{/* Wheels */}
<rect x="4" y="10" width="5" height="4" fill="#222" />
<rect x="5" y="11" width="3" height="2" fill="#555" />
<rect x="19" y="10" width="5" height="4" fill="#222" />
<rect x="20" y="11" width="3" height="2" fill="#555" />
{/* Headlights - round */}
<rect x="24" y="7" width="2" height="2" fill="#FFD700" />
{/* Taillights */}
<rect x="2" y="7" width="2" height="2" fill="#DC143C" />
{/* Chrome bumpers */}
<rect x="24" y="10" width="2" height="2" fill="#C0C0C0" />
<rect x="2" y="10" width="2" height="2" fill="#C0C0C0" />
</svg>
);
}
// Pixel Art A-Team style Van with optional text
export function PixelVan({ className, text }: { className?: string; text?: string }) {
return (
<div className={cn("relative", className)}>
<svg
viewBox="0 0 40 20"
className="w-32 h-16"
style={{ imageRendering: "pixelated" }}
>
{/* Shadow */}
<rect x="4" y="18" width="32" height="2" fill="#00000033" />
{/* Van body - white */}
<rect x="2" y="4" width="34" height="12" fill="#F5F5F5" />
{/* Roof */}
<rect x="4" y="2" width="30" height="4" fill="#E8E8E8" />
{/* Windshield */}
<rect x="28" y="4" width="6" height="6" fill="#87CEEB" />
{/* Wheels */}
<rect x="6" y="16" width="6" height="4" fill="#222" />
<rect x="7" y="17" width="4" height="2" fill="#555" />
<rect x="28" y="16" width="6" height="4" fill="#222" />
<rect x="29" y="17" width="4" height="2" fill="#555" />
{/* Headlights */}
<rect x="34" y="8" width="2" height="2" fill="#FFD700" />
{/* Taillights */}
<rect x="2" y="12" width="2" height="2" fill="#DC143C" />
{/* Front bumper */}
<rect x="34" y="14" width="4" height="2" fill="#333" />
</svg>
{/* Text overlay on van side */}
{text && (
<span
className="absolute top-[40%] left-[40%] -translate-x-1/2 -translate-y-1/2 text-gray-800 font-bold text-[11px] whitespace-nowrap"
style={{ textShadow: "0.5px 0.5px 0 #fff", transform: "scaleX(-1)" }}
>
{text}
</span>
)}
</div>
);
}
// Pixel Art Lake
export function PixelLake({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 32 16"
className={cn("w-16 h-8", className)}
style={{ imageRendering: "pixelated" }}
>
{/* Lake water - multiple shades of blue */}
<rect x="4" y="4" width="24" height="2" fill="#5DADE2" />
<rect x="2" y="6" width="28" height="2" fill="#3498DB" />
<rect x="0" y="8" width="32" height="4" fill="#2980B9" />
<rect x="2" y="12" width="28" height="2" fill="#3498DB" />
<rect x="6" y="14" width="20" height="2" fill="#5DADE2" />
{/* Water sparkles */}
<rect x="8" y="8" width="2" height="2" fill="#AED6F1" />
<rect x="18" y="10" width="2" height="2" fill="#AED6F1" />
<rect x="24" y="8" width="2" height="2" fill="#D4E6F1" />
{/* Shore/edge */}
<rect x="4" y="2" width="6" height="2" fill="#C4A574" />
<rect x="22" y="2" width="6" height="2" fill="#C4A574" />
</svg>
);
}
// Pixel Art Small Pond
export function PixelPond({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 20 12"
className={cn("w-10 h-6", className)}
style={{ imageRendering: "pixelated" }}
>
{/* Pond water */}
<rect x="4" y="2" width="12" height="2" fill="#5DADE2" />
<rect x="2" y="4" width="16" height="4" fill="#3498DB" />
<rect x="4" y="8" width="12" height="2" fill="#2980B9" />
{/* Water sparkle */}
<rect x="8" y="4" width="2" height="2" fill="#AED6F1" />
{/* Lily pad */}
<rect x="12" y="6" width="4" height="2" fill="#27AE60" />
</svg>
);
}
// Pixel Art Cessna-style Plane with Banner (banner trails behind)
export function PixelPlaneWithBanner({ className, bannerText = "LEARN AI!" }: { className?: string; bannerText?: string }) {
return (
<div className={cn("flex items-center", className)}>
{/* Banner trailing behind - wavy flag style with wave animation */}
<div
className="bg-[#FFD700] border-2 border-[#DAA520] px-4 py-1.5 -mr-1"
style={{
clipPath: "polygon(0 0, calc(100% - 8px) 0, 100% 50%, calc(100% - 8px) 100%, 0 100%, 4px 50%)",
animation: "flagWave 0.4s steps(2) infinite",
}}
>
<span className="text-[#8B4513] font-bold text-sm whitespace-nowrap">{bannerText}</span>
</div>
{/* Banner rope */}
<svg viewBox="0 0 24 8" className="w-6 h-2 -mr-1" style={{ imageRendering: "pixelated" }}>
<rect x="0" y="3" width="24" height="2" fill="#8B4513" />
</svg>
{/* Cessna-style Plane */}
<svg
viewBox="0 0 48 28"
className="w-24 h-14"
style={{ imageRendering: "pixelated" }}
>
{/* Fuselage (body) - white/cream */}
<rect x="8" y="12" width="28" height="8" fill="#F5F5F5" />
<rect x="6" y="14" width="4" height="4" fill="#E8E8E8" />
<rect x="34" y="12" width="6" height="6" fill="#F5F5F5" />
{/* Nose cone */}
<rect x="38" y="14" width="4" height="4" fill="#E8E8E8" />
<rect x="40" y="15" width="3" height="2" fill="#D0D0D0" />
{/* Cockpit windows - blue */}
<rect x="30" y="13" width="6" height="3" fill="#87CEEB" />
<rect x="31" y="13" width="2" height="2" fill="#5CB8E8" />
<rect x="34" y="13" width="2" height="2" fill="#5CB8E8" />
{/* High wing (Cessna style - wing on top) */}
<rect x="16" y="8" width="20" height="4" fill="#F5F5F5" />
<rect x="14" y="9" width="4" height="2" fill="#E8E8E8" />
<rect x="34" y="9" width="4" height="2" fill="#E8E8E8" />
{/* Wing strut */}
<rect x="22" y="12" width="2" height="2" fill="#888" />
<rect x="28" y="12" width="2" height="2" fill="#888" />
{/* Red stripe on fuselage */}
<rect x="10" y="16" width="26" height="2" fill="#E74C3C" />
{/* Tail section */}
<rect x="4" y="10" width="6" height="2" fill="#F5F5F5" />
<rect x="2" y="8" width="4" height="4" fill="#F5F5F5" />
<rect x="0" y="6" width="4" height="4" fill="#E8E8E8" />
{/* Horizontal stabilizer */}
<rect x="2" y="18" width="8" height="2" fill="#F5F5F5" />
{/* Propeller hub */}
<rect x="43" y="14" width="3" height="4" fill="#444" />
{/* Animated propeller */}
<g style={{ transformOrigin: "45px 16px", animation: "spin 0.1s linear infinite" }}>
<rect x="44" y="8" width="2" height="6" fill="#666" />
<rect x="44" y="18" width="2" height="6" fill="#666" />
</g>
{/* Landing gear */}
<rect x="18" y="20" width="2" height="4" fill="#555" />
<rect x="16" y="24" width="6" height="2" fill="#333" />
<rect x="30" y="20" width="2" height="4" fill="#555" />
<rect x="28" y="24" width="6" height="2" fill="#333" />
</svg>
</div>
);
}

View File

@@ -17,7 +17,13 @@ import {
PixelFlower,
PixelStar,
PixelRobot,
PixelLevelNode
PixelLevelNode,
PixelPlaneWithBanner,
PixelLake,
PixelPond,
PixelCar,
PixelOldCar,
PixelVan
} from "./pixel-art";
export function ProgressMap() {
@@ -56,14 +62,22 @@ export function ProgressMap() {
<div className="relative h-full flex flex-col">
{/* Scroll controls for desktop */}
<div className="hidden md:flex absolute left-2 top-1/2 -translate-y-1/2 z-20">
<Button variant="secondary" size="icon" onClick={scrollLeft} className="rounded-full shadow-lg">
<ChevronLeft className="h-5 w-5" />
</Button>
<button
onClick={scrollLeft}
className="w-10 h-10 bg-[#FFD700] border-3 border-[#DAA520] flex items-center justify-center hover:bg-[#FFC000] transition-colors shadow-lg"
style={{ clipPath: "polygon(0 4px, 4px 4px, 4px 0, calc(100% - 4px) 0, calc(100% - 4px) 4px, 100% 4px, 100% calc(100% - 4px), calc(100% - 4px) calc(100% - 4px), calc(100% - 4px) 100%, 4px 100%, 4px calc(100% - 4px), 0 calc(100% - 4px))" }}
>
<ChevronLeft className="h-5 w-5 text-[#8B4513]" />
</button>
</div>
<div className="hidden md:flex absolute right-2 top-1/2 -translate-y-1/2 z-20">
<Button variant="secondary" size="icon" onClick={scrollRight} className="rounded-full shadow-lg">
<ChevronRight className="h-5 w-5" />
</Button>
<button
onClick={scrollRight}
className="w-10 h-10 bg-[#FFD700] border-3 border-[#DAA520] flex items-center justify-center hover:bg-[#FFC000] transition-colors shadow-lg"
style={{ clipPath: "polygon(0 4px, 4px 4px, 4px 0, calc(100% - 4px) 0, calc(100% - 4px) 4px, 100% 4px, 100% calc(100% - 4px), calc(100% - 4px) calc(100% - 4px), calc(100% - 4px) 100%, 4px 100%, 4px calc(100% - 4px), 0 calc(100% - 4px))" }}
>
<ChevronRight className="h-5 w-5 text-[#8B4513]" />
</button>
</div>
{/* Horizontal scrolling map container */}
@@ -83,32 +97,48 @@ export function ProgressMap() {
<div className="absolute bottom-12 left-0 right-0 h-4 bg-[#228B22]" style={{ minWidth: `${mapWidth}px` }} />
<div className="absolute bottom-16 left-0 right-0 h-2 bg-[#32CD32]" style={{ minWidth: `${mapWidth}px` }} />
{/* Road with dashed center line */}
<div
className="absolute bottom-0 left-0 right-0 h-8 bg-[#4A4A4A]"
style={{ minWidth: `${mapWidth}px` }}
>
{/* Road edges */}
<div className="absolute top-0 left-0 right-0 h-1 bg-[#6B6B6B]" />
<div className="absolute bottom-0 left-0 right-0 h-1 bg-[#3A3A3A]" />
{/* Dashed center line */}
<div
className="absolute top-1/2 -translate-y-1/2 left-0 right-0 h-1.5 bg-repeat-x"
style={{
backgroundImage: "repeating-linear-gradient(90deg, #FFD700 0px, #FFD700 20px, transparent 20px, transparent 40px)",
minWidth: `${mapWidth}px`,
}}
/>
</div>
{/* Pixel art path connecting all levels */}
<svg
className="absolute inset-0 w-full h-full pointer-events-none z-0"
style={{ width: `${mapWidth}px` }}
style={{ width: `${mapWidth}px`, imageRendering: "pixelated" }}
preserveAspectRatio="none"
shapeRendering="crispEdges"
>
{/* Draw dotted path between levels - pixel style */}
{/* Path border for depth */}
<path
d={generatePathD(levelPositions)}
d={generatePixelPathD(levelPositions)}
fill="none"
stroke="#D4A574"
stroke="#8B5A2B"
strokeWidth="12"
strokeLinecap="square"
strokeLinejoin="miter"
strokeDasharray="16 8"
/>
{/* Path border for depth */}
{/* Draw path between levels - pixel style */}
<path
d={generatePathD(levelPositions)}
d={generatePixelPathD(levelPositions)}
fill="none"
stroke="#8B5A2B"
strokeWidth="16"
stroke="#D4A574"
strokeWidth="8"
strokeLinecap="square"
strokeLinejoin="miter"
strokeDasharray="16 8"
className="opacity-30"
/>
</svg>
@@ -205,7 +235,60 @@ function generatePathD(positions: { x: number; y: number }[]): string {
return d;
}
// Generate pixelated path using straight line segments (stair-step pattern)
function generatePixelPathD(positions: { x: number; y: number }[]): string {
if (positions.length === 0) return "";
let d = `M ${positions[0].x} ${positions[0].y}`;
for (let i = 1; i < positions.length; i++) {
const prev = positions[i - 1];
const curr = positions[i];
const midX = (prev.x + curr.x) / 2;
// Create stair-step pattern for pixelated look
d += ` H ${midX}`; // Horizontal to midpoint
d += ` V ${curr.y}`; // Vertical to new Y
d += ` H ${curr.x}`; // Horizontal to destination
}
return d;
}
function MapDecorations({ mapWidth }: { mapWidth: number }) {
const [planeY, setPlaneY] = useState(15); // percentage from top
const [isDragging, setIsDragging] = useState(false);
const planeRef = useRef<HTMLDivElement>(null);
// Handle plane dragging
useEffect(() => {
if (!isDragging) return;
const handleMove = (clientY: number) => {
const container = planeRef.current?.parentElement;
if (!container) return;
const rect = container.getBoundingClientRect();
const y = ((clientY - rect.top) / rect.height) * 100;
// Constrain between 8% and 35% from top
setPlaneY(Math.max(8, Math.min(35, y)));
};
const onMouseMove = (e: MouseEvent) => handleMove(e.clientY);
const onTouchMove = (e: TouchEvent) => handleMove(e.touches[0].clientY);
const onEnd = () => setIsDragging(false);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('touchmove', onTouchMove);
document.addEventListener('mouseup', onEnd);
document.addEventListener('touchend', onEnd);
return () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('touchmove', onTouchMove);
document.removeEventListener('mouseup', onEnd);
document.removeEventListener('touchend', onEnd);
};
}, [isDragging]);
// Generate decorations along the map - using seeded positions for consistency
const decorations = [];
const spacing = 100;
@@ -221,7 +304,7 @@ function MapDecorations({ mapWidth }: { mapWidth: number }) {
className="absolute pointer-events-none"
style={{
left: `${x + (i % 3) * 15}px`,
bottom: `${55 + yOffset + (i % 4) * 5}px`
bottom: `${50 + yOffset + (i % 4) * 5}px`
}}
>
{type === 0 && <PixelTree />}
@@ -233,7 +316,30 @@ function MapDecorations({ mapWidth }: { mapWidth: number }) {
);
}
// Add castle at the end
// Add lakes and ponds scattered on the ground
const lakePositions = [
{ x: 210, type: 'lake' },
{ x: 1000, type: 'pond' },
{ x: 2180, type: 'lake' },
{ x: 2920, type: 'pond' },
{ x: 3680, type: 'lake' },
];
lakePositions.forEach((lake, i) => {
if (lake.x < mapWidth - 100) {
decorations.push(
<div
key={`lake-${i}`}
className="absolute pointer-events-none"
style={{ left: `${lake.x}px`, bottom: "50px" }}
>
{lake.type === 'lake' ? <PixelLake /> : <PixelPond />}
</div>
);
}
});
// Add castle at the end
decorations.push(
<div
key="castle"
@@ -244,6 +350,116 @@ function MapDecorations({ mapWidth }: { mapWidth: number }) {
</div>
);
// Add cars on the road with hover traffic light
// Car 1 - going left (top lane)
decorations.push(
<div
key="car1"
className="absolute z-[2] group"
style={{
bottom: "7px",
transform: "scaleX(-1)", // Flip to face left
animation: `driveLeft ${Math.max(10, mapWidth / 200)}s linear infinite`,
["--map-width" as string]: `${mapWidth}px`,
}}
onMouseEnter={(e) => e.currentTarget.style.animationPlayState = "paused"}
onMouseLeave={(e) => e.currentTarget.style.animationPlayState = "running"}
>
{/* Traffic light - appears on hover, in front of car */}
<div className="absolute -right-8 -bottom-2 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center" style={{ transform: "scaleX(-1)" }}>
{/* Light box */}
<div className="w-4 h-10 bg-gray-700 flex flex-col items-center justify-center gap-0.5 p-1">
<div className="w-2 h-2 bg-red-500 shadow-[0_0_6px_#ef4444]" />
<div className="w-2 h-2 bg-yellow-800" />
<div className="w-2 h-2 bg-green-800" />
</div>
{/* Pole */}
<div className="w-1.5 h-8 bg-gray-500" />
</div>
<PixelCar color="#E74C3C" />
</div>
);
// Van - going left (top lane, different timing)
decorations.push(
<div
key="van1"
className="absolute z-[2] group"
style={{
bottom: "0px",
transform: "scaleX(-1)", // Flip to face left
animation: `driveLeft ${Math.max(18, mapWidth / 70)}s linear infinite`,
animationDelay: "-8s",
["--map-width" as string]: `${mapWidth}px`,
}}
onMouseEnter={(e) => e.currentTarget.style.animationPlayState = "paused"}
onMouseLeave={(e) => e.currentTarget.style.animationPlayState = "running"}
>
{/* Traffic light - appears on hover, in front of van */}
<div className="absolute -right-8 -bottom-2 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center" style={{ transform: "scaleX(-1)" }}>
{/* Light box */}
<div className="w-4 h-10 bg-gray-700 flex flex-col items-center justify-center gap-0.5 p-1">
<div className="w-2 h-2 bg-red-500 shadow-[0_0_6px_#ef4444]" />
<div className="w-2 h-2 bg-yellow-800" />
<div className="w-2 h-2 bg-green-800" />
</div>
{/* Pole */}
<div className="w-1.5 h-8 bg-gray-500" />
</div>
<div style={{ animation: "engineVibrate 0.1s steps(2) infinite" }}>
<PixelVan text="prompts.chat" />
</div>
</div>
);
// Car 2 - going right (bottom lane)
decorations.push(
<div
key="car2"
className="absolute z-[1] group"
style={{
bottom: "22px",
animation: `driveRight ${Math.max(12, mapWidth / 90)}s linear infinite`,
["--map-width" as string]: `${mapWidth}px`,
}}
onMouseEnter={(e) => e.currentTarget.style.animationPlayState = "paused"}
onMouseLeave={(e) => e.currentTarget.style.animationPlayState = "running"}
>
{/* Traffic light - appears on hover, on right side of car (going right) */}
<div className="absolute -right-8 bottom-2 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center">
{/* Light box */}
<div className="w-4 h-10 bg-gray-700 flex flex-col items-center justify-center gap-0.5 p-1">
<div className="w-2 h-2 bg-red-500 shadow-[0_0_6px_#ef4444]" />
<div className="w-2 h-2 bg-yellow-800" />
<div className="w-2 h-2 bg-green-800" />
</div>
{/* Pole */}
<div className="w-1.5 h-8 bg-gray-500" />
</div>
<div style={{ animation: "engineVibrate 0.1s steps(2) infinite" }}>
<PixelOldCar />
</div>
</div>
);
// Add flying plane with banner - flies across full map width, draggable up/down
decorations.push(
<div
key="plane"
ref={planeRef}
className="absolute z-10 cursor-grab active:cursor-grabbing select-none"
style={{
top: `${planeY}%`,
animation: `flyPlaneMap ${Math.max(20, mapWidth / 50)}s linear infinite`,
["--map-width" as string]: `${mapWidth}px`,
}}
onMouseDown={() => setIsDragging(true)}
onTouchStart={() => setIsDragging(true)}
>
<PixelPlaneWithBanner bannerText="prompts.chat" />
</div>
);
return <>{decorations}</>;
}

View File

@@ -50,6 +50,10 @@ export function SettingsButton() {
);
}
// Pixel clip-paths for consistent styling
const pixelClipPath = "polygon(0 8px, 8px 8px, 8px 0, calc(100% - 8px) 0, calc(100% - 8px) 8px, 100% 8px, 100% calc(100% - 8px), calc(100% - 8px) calc(100% - 8px), calc(100% - 8px) 100%, 8px 100%, 8px calc(100% - 8px), 0 calc(100% - 8px))";
const smallPixelClipPath = "polygon(0 4px, 4px 4px, 4px 0, calc(100% - 4px) 0, calc(100% - 4px) 4px, 100% 4px, 100% calc(100% - 4px), calc(100% - 4px) calc(100% - 4px), calc(100% - 4px) 100%, 4px 100%, 4px calc(100% - 4px), 0 calc(100% - 4px))";
function SettingsModal({ onClose }: { onClose: () => void }) {
const t = useTranslations("kids.settings");
const currentLocale = useLocale();
@@ -95,11 +99,15 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
/>
{/* Modal */}
<div className="relative bg-[#FEF3C7] border-4 border-[#8B4513] rounded-xl p-6 w-full max-w-md max-h-[90vh] overflow-y-auto animate-in zoom-in-95 fade-in duration-200">
<div
className="relative bg-[#FEF3C7] border-4 border-[#8B4513] p-6 w-full max-w-md max-h-[90vh] overflow-y-auto animate-in zoom-in-95 fade-in duration-200"
style={{ clipPath: pixelClipPath }}
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-3 right-3 p-2 text-[#8B4513] hover:bg-[#8B4513]/10 rounded-lg"
className="absolute top-3 right-3 p-2 text-[#8B4513] hover:bg-[#8B4513]/10"
style={{ clipPath: smallPixelClipPath }}
>
<X className="w-5 h-5" />
</button>
@@ -145,27 +153,31 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
<Globe className="w-5 h-5" />
{t("language")}
</h3>
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-2 gap-2">
{SUPPORTED_LOCALES.map((locale) => (
<button
key={locale.code}
onClick={() => handleLanguageChange(locale.code)}
className={cn(
"p-2 rounded-lg border-2 text-sm font-medium transition-all",
"p-2 border-2 text-sm font-medium transition-all flex items-center gap-2",
currentLocale === locale.code
? "bg-[#8B4513] border-[#5D4037] text-white"
: "bg-white border-[#D4A574] text-[#5D4037] hover:border-[#8B4513]"
)}
style={{ clipPath: smallPixelClipPath }}
>
<span className="text-lg">{locale.flag}</span>
<div className="text-xs mt-1">{locale.label}</div>
<span className="text-xs">{locale.label}</span>
</button>
))}
</div>
</div>
{/* Progress Info */}
<div className="mb-6 p-4 bg-white/50 rounded-lg border-2 border-[#D4A574]">
<div
className="mb-6 p-4 bg-white/50 border-2 border-[#D4A574]"
style={{ clipPath: smallPixelClipPath }}
>
<h3 className="text-lg font-bold text-[#5D4037] mb-2">
{t("progress")}
</h3>
@@ -189,7 +201,10 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
</h3>
{resetComplete ? (
<div className="p-3 bg-green-100 border-2 border-green-500 rounded-lg text-green-700 font-medium flex items-center gap-2">
<div
className="p-3 bg-green-100 border-2 border-green-500 text-green-700 font-medium flex items-center gap-2"
style={{ clipPath: smallPixelClipPath }}
>
<Check className="w-5 h-5" />
{t("resetComplete")}
</div>
@@ -198,16 +213,18 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
<p className="text-red-600 font-medium text-sm">
{t("resetWarning")}
</p>
<div className="flex gap-2">
<div className="flex flex-col gap-2">
<button
onClick={handleResetProgress}
className="flex-1 py-2 px-4 bg-red-500 hover:bg-red-600 text-white font-bold rounded-lg"
className="w-full py-2 px-4 bg-red-500 hover:bg-red-600 text-white font-bold"
style={{ clipPath: smallPixelClipPath }}
>
{t("resetConfirm")}
</button>
<button
onClick={() => setShowResetConfirm(false)}
className="flex-1 py-2 px-4 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold rounded-lg"
className="w-full py-2 px-4 bg-gray-200 hover:bg-gray-300 text-gray-700 font-bold"
style={{ clipPath: smallPixelClipPath }}
>
{t("cancel")}
</button>
@@ -216,7 +233,8 @@ function SettingsModal({ onClose }: { onClose: () => void }) {
) : (
<button
onClick={handleResetProgress}
className="w-full py-2 px-4 bg-red-100 hover:bg-red-200 text-red-700 font-bold rounded-lg border-2 border-red-300"
className="w-full py-2 px-4 bg-red-100 hover:bg-red-200 text-red-700 font-bold border-2 border-red-300"
style={{ clipPath: smallPixelClipPath }}
>
{t("resetButton")}
</button>