From 9a1ae7c0ee7b0579d6df6d699da93f67794b5d2f Mon Sep 17 00:00:00 2001 From: Coffee In Date: Sat, 13 Dec 2025 12:58:19 +0000 Subject: [PATCH] Added builder bridge and dynamic menus --- package.json | 3 +- scripts/build.js | 51 +++++++++ src/.DS_Store | Bin 0 -> 6148 bytes src/apps/.gitignore | 24 +++++ src/apps/bridge-builder-math/metadata.json | 10 +- src/apps/bridge-builder-math/vite.config.ts | 1 + src/apps/components/Button.tsx | 37 ------- src/apps/components/Confetti.tsx | 72 ------------- src/apps/components/ControlPanel.tsx | 105 ------------------ src/apps/components/GameScene.tsx | 113 -------------------- src/apps/components/TutorModal.tsx | 72 ------------- src/apps/german-trainer/metadata.json | 7 ++ src/apps/hexarow/metadata.json | 7 ++ src/apps/services/geminiService.ts | 55 ---------- src/apps/services/mathEngine.ts | 92 ---------------- src/apps/snapmark/metadata.json | 10 +- src/landing/index.html | 2 +- 17 files changed, 105 insertions(+), 556 deletions(-) create mode 100755 src/.DS_Store create mode 100644 src/apps/.gitignore delete mode 100644 src/apps/components/Button.tsx delete mode 100644 src/apps/components/Confetti.tsx delete mode 100644 src/apps/components/ControlPanel.tsx delete mode 100644 src/apps/components/GameScene.tsx delete mode 100644 src/apps/components/TutorModal.tsx create mode 100644 src/apps/german-trainer/metadata.json create mode 100644 src/apps/hexarow/metadata.json delete mode 100644 src/apps/services/geminiService.ts delete mode 100644 src/apps/services/mathEngine.ts diff --git a/package.json b/package.json index 371914c..1614471 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "version": "0.0.1", "private": true, "scripts": { - "deploy": "wrangler pages deploy dist --project-name cffyin", + "deploy": "wrangler pages deploy dist --project-name cffyin --branch main --commit-dirty=true", + "preview": "wrangler pages deploy dist --project-name cffyin --branch master --commit-dirty=true", "dev": "wrangler pages dev dist", "start": "wrangler pages dev dist", "cf-typegen": "wrangler types", diff --git a/scripts/build.js b/scripts/build.js index ca78784..dc00306 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -48,10 +48,61 @@ function processHtml(filePath, outPath) { content = NAV_TEMPLATE + content; } + // Inject Apps Grid if placeholder exists + if (content.includes('')) { + const appsGridHtml = generateAppsGridHtml(); + content = content.replace('', appsGridHtml); + } + fs.writeFileSync(outPath, content); console.log(`Processed ${filePath} -> ${outPath}`); } +function generateAppsGridHtml() { + if (!fs.existsSync(APPS_DIR)) return ''; + + const apps = fs.readdirSync(APPS_DIR).filter(item => { + const itemPath = path.join(APPS_DIR, item); + return !item.startsWith('.') && fs.statSync(itemPath).isDirectory(); + }); + + return apps.map(app => { + const metadataPath = path.join(APPS_DIR, app, 'metadata.json'); + let metadata = { + title: app, + description: 'A cool app.', + emoji: '📱', + color: 'gray', + path: `/${app}/` + }; + + if (fs.existsSync(metadataPath)) { + try { + const data = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')); + metadata = { ...metadata, ...data }; + } catch (e) { + console.error(`Error reading metadata for ${app}:`, e); + } + } + + return ` +
+ +
+
${metadata.emoji}
+

${metadata.title}

+

${metadata.description}

+
+
+
ONLINE
+
+
+
+ `; + }).join('\n'); +} + + function buildLanding() { const landingSrc = path.join(SRC_DIR, 'landing', 'index.html'); const landingDist = path.join(DIST_DIR, 'index.html'); diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100755 index 0000000000000000000000000000000000000000..3d8319d6882fa46dfc2fb15ee9b018fd9c704b3d GIT binary patch literal 6148 zcmeHK!D<^Z5FI&Ay~Zv0&_D?U7J7AB*9n1;%f?L)xn@gxXp6hsHR+Pg%6RSgU<~^K z?YHFK59#kTZzRRUc6+T&nK3g@8qG-P$w)gyq`Kqe4N;SbJSbzO4)X`$an=>7IL|gv z@f_!w{u*aGMxQ4X*?b20?5Y&e0Ka1C{7y0U>bDWIk$GG^DF*JGf5w9>Ek^D3FICy9 zZa=AcHLu})4!`M1IEqH&q8klAa__y?S^UtB;>)x@={29f)%hq&^Zw8zq)8tkpFX5{ zq9fX-F*C!Xf>+B}i z-!1tH|4bv#96rDgH0C?H4TpKA^DFo`zD5~1$tj@}_G*cjJ48I@ { const env = loadEnv(mode, '.', ''); return { + base: './', server: { port: 3000, host: '0.0.0.0', diff --git a/src/apps/components/Button.tsx b/src/apps/components/Button.tsx deleted file mode 100644 index a31fe38..0000000 --- a/src/apps/components/Button.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; - -interface ButtonProps extends React.ButtonHTMLAttributes { - variant?: 'primary' | 'secondary' | 'danger'; - size?: 'sm' | 'md' | 'lg'; -} - -export const Button: React.FC = ({ - children, - variant = 'primary', - size = 'md', - className = '', - ...props -}) => { - const baseStyles = "font-bold rounded-xl shadow-[0_4px_0_0_rgba(0,0,0,0.2)] active:shadow-none active:translate-y-[4px] transition-all flex items-center justify-center"; - - const variants = { - primary: "bg-green-500 text-white hover:bg-green-400 border-2 border-green-600", - secondary: "bg-sky-500 text-white hover:bg-sky-400 border-2 border-sky-600", - danger: "bg-red-500 text-white hover:bg-red-400 border-2 border-red-600", - }; - - const sizes = { - sm: "px-4 py-2 text-sm", - md: "px-6 py-3 text-lg", - lg: "px-8 py-4 text-xl", - }; - - return ( - - ); -}; diff --git a/src/apps/components/Confetti.tsx b/src/apps/components/Confetti.tsx deleted file mode 100644 index ee5c160..0000000 --- a/src/apps/components/Confetti.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useEffect, useRef } from 'react'; -import { COLORS } from '../constants'; - -export const Confetti: React.FC = () => { - const canvasRef = useRef(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - - // Set dimensions - canvas.width = window.innerWidth; - canvas.height = window.innerHeight; - - const particles: any[] = []; - // Bright celebratory colors - const colors = [COLORS.grass, COLORS.bridge, COLORS.character, '#ef4444', '#3b82f6', '#8b5cf6', '#ec4899']; - - // Create particles - for (let i = 0; i < 200; i++) { - particles.push({ - x: canvas.width / 2, - y: canvas.height * 0.3, - vx: (Math.random() - 0.5) * 25, - vy: (Math.random() - 0.5) * 25 - 5, - size: Math.random() * 8 + 4, - color: colors[Math.floor(Math.random() * colors.length)], - rotation: Math.random() * 360, - rotationSpeed: (Math.random() - 0.5) * 15, - gravity: 0.6, - drag: 0.95 - }); - } - - let animationId: number; - const animate = () => { - ctx.clearRect(0, 0, canvas.width, canvas.height); - - let activeParticles = 0; - particles.forEach((p) => { - if (p.y < canvas.height + 20) { - activeParticles++; - p.x += p.vx; - p.y += p.vy; - p.vy += p.gravity; - p.vx *= p.drag; - p.y *= 1.0; - p.rotation += p.rotationSpeed; - - ctx.save(); - ctx.translate(p.x, p.y); - ctx.rotate((p.rotation * Math.PI) / 180); - ctx.fillStyle = p.color; - ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); - ctx.restore(); - } - }); - - if (activeParticles > 0) { - animationId = requestAnimationFrame(animate); - } - }; - - animate(); - - return () => cancelAnimationFrame(animationId); - }, []); - - return ; -}; \ No newline at end of file diff --git a/src/apps/components/ControlPanel.tsx b/src/apps/components/ControlPanel.tsx deleted file mode 100644 index 732d17d..0000000 --- a/src/apps/components/ControlPanel.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Problem, GameStatus } from '../types'; -import { Delete, ArrowRight } from 'lucide-react'; - -interface ControlPanelProps { - problem: Problem | null; - status: GameStatus; - onAnswer: (answer: number) => void; -} - -export const ControlPanel: React.FC = ({ problem, status, onAnswer }) => { - const [input, setInput] = useState(''); - - // Clear input when problem changes - useEffect(() => { - setInput(''); - }, [problem?.id]); - - const handleNumClick = (num: number) => { - if (input.length < 3) { // Max 3 digits (answer < 200 usually) - setInput(prev => prev + num.toString()); - } - }; - - const handleDelete = () => { - setInput(prev => prev.slice(0, -1)); - }; - - const handleSubmit = (e?: React.FormEvent) => { - e?.preventDefault(); - if (input === '') return; - onAnswer(parseInt(input, 10)); - }; - - if (status !== GameStatus.PLAYING || !problem) { - return
; - } - - // Button style helper - const btnClass = "bg-slate-100 hover:bg-slate-200 active:bg-slate-300 text-slate-700 font-bold text-2xl md:text-3xl rounded-lg shadow-sm border-b-4 border-slate-300 active:border-b-0 active:translate-y-1 transition-all h-full flex items-center justify-center touch-manipulation select-none"; - - return ( -
- {/* Question Display - Smaller on mobile */} -
-
Build Segment
-
- {problem.a} - {problem.operator} - {problem.b} - = - - {input || '?'} - -
-
- - {/* Numpad - Takes more space on mobile */} -
-
- {/* Row 1: 1, 2, 3 */} - {[1, 2, 3].map((num) => ( - - ))} - - {/* Row 2: 4, 5, 6 */} - {[4, 5, 6].map((num) => ( - - ))} - - {/* Row 3: 7, 8, 9 */} - {[7, 8, 9].map((num) => ( - - ))} - - {/* Row 4: Del, 0, Enter */} - - - - - -
-
-
- ); -}; \ No newline at end of file diff --git a/src/apps/components/GameScene.tsx b/src/apps/components/GameScene.tsx deleted file mode 100644 index e509a7a..0000000 --- a/src/apps/components/GameScene.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import { GameState } from '../types'; -import { GAME_CONFIG, COLORS } from '../constants'; -import { HardHat, Mountain } from 'lucide-react'; - -interface GameSceneProps { - gameState: GameState; -} - -export const GameScene: React.FC = ({ gameState }) => { - const { characterX, bridgeEndX } = gameState; - - // Calculate view offset to keep character somewhat centered but moving right - // We want the character to be at roughly 30% of the screen width initially - // As character moves, we scroll the 'camera' - const viewOffset = Math.max(0, characterX - GAME_CONFIG.CANVAS_WIDTH * 0.3); - - return ( -
- {/* Background Decor */} -
- -
-
- -
- - {/* Game World Container with transform for scrolling */} -
- {/* Ground (Starting Platform) */} -
- - {/* The Abyss (Visual only, behind bridge) */} -
- - {/* Bridge Segments */} -
- - {/* Character */} -
bridgeEndX ? '200px' : '0px'}) rotate(${gameState.status === 'GAME_OVER' && characterX > bridgeEndX ? '90deg' : '0deg'})`, - transition: 'transform 0.5s ease-in' - }} - > - -
- {/* Legs animation simulation */} -
-
-
-
-
- - {/* Next Platform Marker (Visual Goal) */} -
- GOAL -> -
-
-
- ); -}; \ No newline at end of file diff --git a/src/apps/components/TutorModal.tsx b/src/apps/components/TutorModal.tsx deleted file mode 100644 index d47dac5..0000000 --- a/src/apps/components/TutorModal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Problem, TutorResponse } from '../types'; -import { getTutorExplanation } from '../services/geminiService'; -import { Bot, Loader2, X } from 'lucide-react'; -import { Button } from './Button'; - -interface TutorModalProps { - problem: Problem; - onClose: () => void; - isOpen: boolean; -} - -export const TutorModal: React.FC = ({ problem, onClose, isOpen }) => { - const [loading, setLoading] = useState(false); - const [data, setData] = useState(null); - - useEffect(() => { - if (isOpen && problem) { - setLoading(true); - getTutorExplanation(problem) - .then(setData) - .finally(() => setLoading(false)); - } - }, [isOpen, problem]); - - if (!isOpen) return null; - - return ( -
-
-
-
- - Math Coach -
- -
- -
-
-

The problem was

-
- {problem.a} {problem.operator} {problem.b} = {problem.answer} -
-
- - {loading ? ( -
- -

Thinking...

-
- ) : data ? ( -
-
-

{data.explanation}

-
-

"{data.encouragement}"

-
- ) : ( -

Could not load explanation.

- )} - -
- -
-
-
-
- ); -}; diff --git a/src/apps/german-trainer/metadata.json b/src/apps/german-trainer/metadata.json new file mode 100644 index 0000000..2cde10b --- /dev/null +++ b/src/apps/german-trainer/metadata.json @@ -0,0 +1,7 @@ +{ + "title": "Lern mit mir!", + "description": "A simple, focused tool for drilling German vocabulary and grammar.", + "emoji": "🇩🇪", + "color": "cyan", + "path": "/german-trainer/" +} diff --git a/src/apps/hexarow/metadata.json b/src/apps/hexarow/metadata.json new file mode 100644 index 0000000..80340b0 --- /dev/null +++ b/src/apps/hexarow/metadata.json @@ -0,0 +1,7 @@ +{ + "title": "6-in-a-Line", + "description": "A classic board game to play against a surprisingly clever AI. Good luck.", + "emoji": "🎮", + "color": "lime", + "path": "/hexarow/" +} diff --git a/src/apps/services/geminiService.ts b/src/apps/services/geminiService.ts deleted file mode 100644 index a7f8d68..0000000 --- a/src/apps/services/geminiService.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { GoogleGenAI, Type } from "@google/genai"; -import { Problem, TutorResponse } from "../types"; - -// Helper to check for API key availability -export const hasApiKey = (): boolean => { - return !!process.env.API_KEY; -}; - -export const getTutorExplanation = async (problem: Problem): Promise => { - if (!process.env.API_KEY) { - return { - explanation: "Practice makes perfect! Try visualizing the numbers as blocks.", - encouragement: "You can do it next time!" - }; - } - - const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); - - const prompt = ` - Explain how to solve the math problem ${problem.a} ${problem.operator} ${problem.b} to a 7-year-old child. - The answer is ${problem.answer}. - If it involves regrouping (carrying/borrowing), explicitly explain that step simply. - Keep the explanation under 3 sentences. - Also provide a short encouraging phrase. - `; - - try { - const response = await ai.models.generateContent({ - model: "gemini-2.5-flash", - contents: prompt, - config: { - responseMimeType: "application/json", - responseSchema: { - type: Type.OBJECT, - properties: { - explanation: { type: Type.STRING }, - encouragement: { type: Type.STRING }, - }, - required: ["explanation", "encouragement"], - }, - }, - }); - - const text = response.text; - if (!text) throw new Error("No response from AI"); - - return JSON.parse(text) as TutorResponse; - } catch (error) { - console.error("Gemini API Error:", error); - return { - explanation: "Remember to check if you need to carry over a ten or borrow one!", - encouragement: "Keep building!" - }; - } -}; diff --git a/src/apps/services/mathEngine.ts b/src/apps/services/mathEngine.ts deleted file mode 100644 index 71a8d83..0000000 --- a/src/apps/services/mathEngine.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Problem, Operator } from '../types'; - -/** - * Generates a random integer between min and max (inclusive) - */ -const randomInt = (min: number, max: number): number => { - return Math.floor(Math.random() * (max - min + 1)) + min; -}; - -/** - * Checks if an addition problem requires regrouping (carrying). - * e.g., 18 + 5 (8+5 > 9) - */ -const isRegroupingAdd = (a: number, b: number): boolean => { - return (a % 10) + (b % 10) >= 10; -}; - -/** - * Checks if a subtraction problem requires regrouping (borrowing). - * e.g., 22 - 5 (2 < 5) - */ -const isRegroupingSub = (a: number, b: number): boolean => { - return (a % 10) < (b % 10); -}; - -export const generateProblem = (level: number, forceRegrouping: boolean = false): Problem => { - // Level 1: Single digits (0-9). Level 2+: Standard constraints (<100). - const operator: Operator = Math.random() > 0.5 ? '+' : '-'; - - let a = 0; - let b = 0; - let answer = 0; - let requiresRegrouping = false; - let valid = false; - - // Safety break counter - let attempts = 0; - - while (!valid && attempts < 100) { - attempts++; - - if (level === 1) { - // Single Digit Mode - if (operator === '+') { - a = randomInt(0, 9); - b = randomInt(0, 9); - answer = a + b; - requiresRegrouping = isRegroupingAdd(a, b); - } else { - a = randomInt(0, 9); - b = randomInt(0, a); // Result >= 0 - answer = a - b; - requiresRegrouping = isRegroupingSub(a, b); - } - valid = true; - } else { - // Standard Mode (Level 2+) - // Constraint: Operands >= 0 - // Constraint: Result < 100 (and >= 0 for sub) - - if (operator === '+') { - // a + b = c, c < 100 - a = randomInt(0, 89); // leave room for b - b = randomInt(1, 99 - a); - answer = a + b; - requiresRegrouping = isRegroupingAdd(a, b); - } else { - // a - b = c, a < 100, c >= 0 - a = randomInt(1, 99); - b = randomInt(1, a); - answer = a - b; - requiresRegrouping = isRegroupingSub(a, b); - } - - // Higher levels or 'forceRegrouping' prefer regrouping problems - if (forceRegrouping) { - if (requiresRegrouping) valid = true; - } else { - valid = true; - } - } - } - - return { - id: Math.random().toString(36).substr(2, 9), - a, - b, - operator, - answer, - requiresRegrouping, - }; -}; \ No newline at end of file diff --git a/src/apps/snapmark/metadata.json b/src/apps/snapmark/metadata.json index 9816c6c..8bb0210 100644 --- a/src/apps/snapmark/metadata.json +++ b/src/apps/snapmark/metadata.json @@ -1,5 +1,7 @@ { - "name": "SnapMark", - "description": "A vector-based image annotation tool. Paste images, add editable highlights, arrows, and text, then export as PNG.", - "requestFramePermissions": [] -} \ No newline at end of file + "title": "SnapMark", + "description": "Highlight, annotate, and export images in a snap.", + "emoji": "🖍️", + "color": "pink", + "path": "/snapmark/" +} diff --git a/src/landing/index.html b/src/landing/index.html index 4070260..da5f9c9 100644 --- a/src/landing/index.html +++ b/src/landing/index.html @@ -1 +1 @@ -cffyin | cffy.in
☕
✨
🎨

CFFYIN

☕ ✨ 🎨

Welcome to the Playground

A collection of small, delightful web experiments where education meets fun meets art.
No ads. No tracking. Just vibes.

☕

→ THE SITES ←

Pick your adventure!

☕ ❤️ ✨

Remember when the internet was fun? We do.
No tracking, no ads, no BS. Just pure, caffeinated creativity.

© 2025 cffy.in • Best viewed with coffee in hand ☕

\ No newline at end of file +cffyin | cffy.in
☕
✨
🎨

CFFYIN

☕ ✨ 🎨

Welcome to the Playground

A collection of small, delightful web experiments where education meets fun meets art.
No ads. No tracking. Just vibes.

☕

→ THE SITES ←

Pick your adventure!

\ No newline at end of file