Standalone Crossword App - Complete Starter
Copy these files to create your external crossword app
Quick Start:
- Create a new folder:
mkdir crossword-app && cd crossword-app - Copy all files below into the folder structure
- Run:
npm install && npm run dev - Deploy to Vercel:
vercel --prod - 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*.localDeploy 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