Navigation
Standalone Crossword App - Complete Starter

Copy these files to create your external crossword app

Quick Start:

  1. Create a new folder: mkdir crossword-app && cd crossword-app
  2. Copy all files below into the folder structure
  3. Run: npm install && npm run dev
  4. Deploy to Vercel: vercel --prod
  5. Update CROSSWORD_APP_URL in StandaloneCrosswordHub.js
package.json
{
  "name": "crossword-standalone",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev -p 3001",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "14.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
}
app/page.js
'use client';
import { useState, useEffect } from 'react';
import CrosswordGrid from '../components/CrosswordGrid';
import { sendToParent, listenFromParent } from '../lib/postMessage';
import { SAMPLE_PUZZLE } from '../lib/puzzleData';

export default function CrosswordApp() {
  const [user, setUser] = useState(null);
  const [puzzle] = useState(SAMPLE_PUZZLE);
  const [startTime] = useState(Date.now());
  const [completed, setCompleted] = useState(false);

  useEffect(() => {
    // Announce ready to parent
    sendToParent('CROSSWORD_READY', {});
    
    // Listen for auth token from parent
    listenFromParent((message) => {
      if (message.type === 'AUTH_TOKEN') {
        try {
          const userData = JSON.parse(atob(message.token));
          setUser(userData);
        } catch (error) {
          console.error('Invalid token:', error);
        }
      }
    });
  }, []);

  const handleComplete = () => {
    const timeSpent = Math.floor((Date.now() - startTime) / 1000);
    setCompleted(true);
    
    // Send completion event to parent with pearls
    sendToParent('PUZZLE_COMPLETED', {
      pearlsEarned: 10,
      completionTime: timeSpent
    });
  };

  return (
    <div className="min-h-screen bg-gradient-to-br from-orange-50 to-yellow-50 p-4">
      <div className="max-w-4xl mx-auto">
        <div className="bg-white rounded-xl shadow-lg p-6 mb-4">
          <div className="flex justify-between items-center mb-4">
            <h1 className="text-2xl font-bold text-orange-900">
              Daily Idiom Crossword
            </h1>
            {user && (
              <div className="text-sm text-gray-600">
                Welcome, {user.name}!
              </div>
            )}
          </div>
          
          {completed ? (
            <div className="text-center py-12">
              <div className="text-6xl mb-4">🎉</div>
              <h2 className="text-3xl font-bold text-green-600 mb-2">
                Puzzle Complete!
              </h2>
              <p className="text-gray-600">
                Great job! Pearls syncing to your account...
              </p>
            </div>
          ) : (
            <CrosswordGrid 
              puzzle={puzzle} 
              onComplete={handleComplete}
            />
          )}
        </div>
      </div>
    </div>
  );
}
components/CrosswordGrid.js
'use client';
import { useState } from 'react';

export default function CrosswordGrid({ puzzle, onComplete }) {
  const [userAnswers, setUserAnswers] = useState({});
  const [selectedCell, setSelectedCell] = useState(null);

  const handleCellChange = (row, col, value) => {
    const key = row + '-' + col;
    const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };
    setUserAnswers(newAnswers);

    // Check if puzzle is complete
    checkCompletion(newAnswers);
  };

  const checkCompletion = (answers) => {
    const allFilled = puzzle.grid.every((row, rowIdx) =>
      row.every((cell, colIdx) => {
        if (!cell) return true; // Black square
        const key = rowIdx + '-' + colIdx;
        return answers[key] === cell;
      })
    );

    if (allFilled) {
      setTimeout(() => onComplete(), 500);
    }
  };

  return (
    <div>
      {/* Grid */}
      <div className="grid gap-1 mb-6" style={{
        gridTemplateColumns: 'repeat(' + puzzle.size + ', 1fr)',
        maxWidth: '500px',
        margin: '0 auto'
      }}>
        {puzzle.grid.map((row, rowIdx) =>
          row.map((cell, colIdx) => {
            const key = rowIdx + '-' + colIdx;
            const userValue = userAnswers[key] || '';
            const isCorrect = userValue && userValue === cell;
            
            return (
              <div key={key} className="relative">
                {cell ? (
                  <input
                    type="text"
                    maxLength={1}
                    value={userValue}
                    onChange={(e) => handleCellChange(rowIdx, colIdx, e.target.value)}
                    className={`w-12 h-12 border-2 text-center font-bold text-lg uppercase focus:outline-none focus:ring-2 focus:ring-orange-400 ${
                      isCorrect ? 'bg-green-100 border-green-400' : 'bg-white border-gray-300'
                    }`}
                  />
                ) : (
                  <div className="w-12 h-12 bg-gray-900" />
                )}
              </div>
            );
          })
        )}
      </div>

      {/* Clues */}
      <div className="grid md:grid-cols-2 gap-6">
        <div>
          <h3 className="font-bold text-lg mb-2">Across</h3>
          <div className="space-y-2">
            {puzzle.clues.across.map((clue, idx) => (
              <div key={idx} className="text-sm">
                <span className="font-semibold">{clue.number}.</span> {clue.text}
              </div>
            ))}
          </div>
        </div>
        <div>
          <h3 className="font-bold text-lg mb-2">Down</h3>
          <div className="space-y-2">
            {puzzle.clues.down.map((clue, idx) => (
              <div key={idx} className="text-sm">
                <span className="font-semibold">{clue.number}.</span> {clue.text}
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}
lib/postMessage.js
/**
 * PostMessage Communication Layer
 * Handles messages between standalone app and parent app
 */

export const sendToParent = (type, data) => {
  if (window.parent && window.parent !== window) {
    window.parent.postMessage({ type, data }, '*');
    console.log('📤 Sent to parent:', type, data);
  }
};

export const listenFromParent = (callback) => {
  const handler = (event) => {
    // Security: Verify origin
    const allowedOrigins = [
      'http://localhost',
      'https://snappyenglish.com',
      'https://app.snappyenglish.com'
    ];
    
    const isAllowed = allowedOrigins.some(origin => 
      event.origin.includes(origin)
    );
    
    if (isAllowed && event.data.type) {
      console.log('📥 Received from parent:', event.data);
      callback(event.data);
    }
  };

  window.addEventListener('message', handler);
  
  return () => window.removeEventListener('message', handler);
};
lib/puzzleData.js
/**
 * Sample Puzzle Data
 * In production, fetch from your own API or hardcode multiple puzzles
 */

export const SAMPLE_PUZZLE = {
  size: 5,
  grid: [
    ['C', 'A', 'T', null, 'D'],
    ['A', null, 'O', null, 'O'],
    ['R', 'U', 'N', null, 'G'],
    [null, null, null, null, null],
    ['B', 'I', 'T', 'E', null]
  ],
  clues: {
    across: [
      { number: 1, text: 'Feline pet' },
      { number: 3, text: 'Move quickly' },
      { number: 5, text: 'Use teeth' }
    ],
    down: [
      { number: 1, text: 'Automobile' },
      { number: 2, text: 'Top of building' },
      { number: 4, text: 'Canine animal' }
    ]
  }
};

// Add more puzzles for different days
export const PUZZLES = {
  '2025-01-18': SAMPLE_PUZZLE,
  // Add more dates...
};
app/globals.css
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}

input:focus {
  outline: none;
}
app/layout.js
import './globals.css'

export const metadata = {
  title: 'Crossword - Snappy English',
  description: 'Daily crossword puzzle'
}

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
.gitignore
node_modules/
.next/
out/
.DS_Store
*.log
.env*.local
Deploy to Vercel

1. Install Vercel CLI

npm install -g vercel

2. Deploy

vercel login
vercel --prod

3. Update Main App

In pages/StandaloneCrosswordHub.js, change:

const CROSSWORD_APP_URL = 'https://your-vercel-url.vercel.app';

✅ That's it! Your crossword now runs independently and syncs pearls back to the main app.

Testing Checklist