Finished cpp impl and made web version
This commit is contained in:
commit
868900e6d5
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
29
web/eslint.config.js
Normal file
29
web/eslint.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
15
web/index.html
Normal file
15
web/index.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DeckCrypt 🃏</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/libs/cardcode.js"></script>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
3369
web/package-lock.json
generated
Normal file
3369
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
web/package.json
Normal file
29
web/package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "web",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
6274
web/public/libs/cardcode.js
Normal file
6274
web/public/libs/cardcode.js
Normal file
File diff suppressed because it is too large
Load Diff
BIN
web/public/libs/cardcode.wasm
Executable file
BIN
web/public/libs/cardcode.wasm
Executable file
Binary file not shown.
336
web/src/App.jsx
Normal file
336
web/src/App.jsx
Normal file
@ -0,0 +1,336 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import Card from './components/Card';
|
||||
|
||||
const defaultDeck = Array.from({ length: 52 }, (_, i) => i);
|
||||
|
||||
const allowedChars = " .,-\"/abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
function sanitizeInput(value) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.split('')
|
||||
.filter((c) => allowedChars.includes(c))
|
||||
.join('')
|
||||
.slice(0, 45);
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [mode, setMode] = useState('encode');
|
||||
|
||||
// encode states
|
||||
const [text, setText] = useState('');
|
||||
const [deck, setDeck] = useState([]);
|
||||
|
||||
// decode states
|
||||
const [order, setOrder] = useState(Array.from({ length: 52 }, (_, i) => i));
|
||||
const [decodedText, setDecodedText] = useState('');
|
||||
const [dragIdx, setDragIdx] = useState(null);
|
||||
const [overIdx, setOverIdx] = useState(null);
|
||||
|
||||
// encryption
|
||||
const [useEncryption, setUseEncryption] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
// misc
|
||||
const [elapsedMs, setElapsedMs] = useState(null);
|
||||
const [wasm, setWasm] = useState(null);
|
||||
|
||||
// Load the WASM module once on mount
|
||||
useEffect(() => {
|
||||
if (!window.createModule) return;
|
||||
window.createModule().then((m) => setWasm(m));
|
||||
}, []);
|
||||
|
||||
// ENCODE: compute deck when text/key changes
|
||||
useEffect(() => {
|
||||
if (mode !== 'encode' || !wasm) return;
|
||||
|
||||
const sanitizedText = sanitizeInput(text);
|
||||
if (sanitizedText !== text) {
|
||||
setText(sanitizedText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (useEncryption && key.trim() === '') {
|
||||
setDeck(defaultDeck);
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitizedKey = sanitizeInput(key);
|
||||
|
||||
// If no text, simply use default deck without calling WASM to avoid errors
|
||||
if (sanitizedText.length === 0) {
|
||||
setDeck(defaultDeck);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
let wasmResult;
|
||||
try {
|
||||
wasmResult = useEncryption
|
||||
? wasm.textToPackOfCardsEncrypted(sanitizedText, sanitizedKey)
|
||||
: wasm.textToPackOfCards(sanitizedText);
|
||||
} catch {
|
||||
// fallback to default deck on any error
|
||||
wasmResult = defaultDeck;
|
||||
}
|
||||
const jsArray = Array.from(wasmResult);
|
||||
if (jsArray.length !== 52) {
|
||||
setDeck(defaultDeck);
|
||||
} else {
|
||||
setDeck(jsArray);
|
||||
}
|
||||
setElapsedMs(performance.now() - start);
|
||||
}, [mode, text, key, useEncryption, wasm]);
|
||||
|
||||
// DECODE: compute text when order/key changes
|
||||
useEffect(() => {
|
||||
if (mode !== 'decode' || !wasm) return;
|
||||
|
||||
const sanitizedKey = sanitizeInput(key);
|
||||
const start = performance.now();
|
||||
const deckArray = Int32Array.from(order.length === 52 ? order : defaultDeck);
|
||||
let result;
|
||||
if (useEncryption) {
|
||||
console.log("encrypted", deckArray, sanitizedKey);
|
||||
result = wasm.packOfCardsToTextEncrypted(deckArray, sanitizedKey);
|
||||
} else {
|
||||
console.log("unencrypted", deckArray);
|
||||
result = wasm.packOfCardsToText(deckArray);
|
||||
}
|
||||
setElapsedMs(performance.now() - start);
|
||||
setDecodedText(result);
|
||||
}, [mode, order, key, useEncryption, wasm]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setText(sanitizeInput(e.target.value));
|
||||
};
|
||||
|
||||
const handleKeyChange = (e) => {
|
||||
setKey(sanitizeInput(e.target.value));
|
||||
};
|
||||
|
||||
// drag handlers for decode reorder
|
||||
const handleDrop = useCallback(
|
||||
(targetIdx) => {
|
||||
setOrder((prev) => {
|
||||
if (dragIdx === null || dragIdx === targetIdx) return prev;
|
||||
const newOrder = [...prev];
|
||||
// Remove the dragged item from its current position
|
||||
const draggedItem = newOrder.splice(dragIdx, 1)[0];
|
||||
// Insert it at the target position
|
||||
newOrder.splice(targetIdx, 0, draggedItem);
|
||||
return newOrder;
|
||||
});
|
||||
setDragIdx(null);
|
||||
setOverIdx(null);
|
||||
},
|
||||
[dragIdx]
|
||||
);
|
||||
|
||||
const resetDecode = () => {
|
||||
setOrder(Array.from({ length: 52 }, (_, i) => i));
|
||||
setDecodedText('');
|
||||
setElapsedMs(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#1e1e2e] text-[#cdd6f4] p-4 flex flex-col">
|
||||
{/* Header with attribution */}
|
||||
<header className="mb-6">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h1 className="text-3xl font-bold text-[#89b4fa] text-center sm:text-left">DeckCrypt 🃏</h1>
|
||||
|
||||
<div className="text-sm text-[#a6adc8] text-center sm:text-right">
|
||||
<p>Made by <span className="text-[#89b4fa] font-medium">Asher Falcon</span></p>
|
||||
<p>
|
||||
<a
|
||||
href="#"
|
||||
className="text-[#74c7ec] hover:text-[#89dceb] underline transition-colors"
|
||||
>
|
||||
Read the blog post to learn how it works
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Toggle - Segmented Control */}
|
||||
<div className="mt-6 flex justify-center">
|
||||
<div className="relative bg-[#313244] p-1 rounded-lg inline-flex">
|
||||
<button
|
||||
onClick={() => setMode('encode')}
|
||||
className={`
|
||||
relative px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 min-w-[80px]
|
||||
${mode === 'encode'
|
||||
? 'bg-[#89b4fa] text-[#1e1e2e] shadow-lg'
|
||||
: 'text-[#bac2de] hover:text-[#cdd6f4] hover:bg-[#45475a]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Encode
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('decode')}
|
||||
className={`
|
||||
relative px-4 py-2 rounded-md text-sm font-medium transition-all duration-200 min-w-[80px]
|
||||
${mode === 'decode'
|
||||
? 'bg-[#89b4fa] text-[#1e1e2e] shadow-lg'
|
||||
: 'text-[#bac2de] hover:text-[#cdd6f4] hover:bg-[#45475a]'
|
||||
}
|
||||
`}
|
||||
>
|
||||
Decode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
{/* encryption toggle & key */}
|
||||
<div className="mb-6 p-4 bg-[#313244] rounded-lg border border-[#45475a]">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={useEncryption}
|
||||
onChange={(e) => setUseEncryption(e.target.checked)}
|
||||
className="sr-only"
|
||||
/>
|
||||
<div className={`
|
||||
w-11 h-6 rounded-full transition-colors duration-200
|
||||
${useEncryption ? 'bg-[#89b4fa]' : 'bg-[#585b70]'}
|
||||
`}>
|
||||
<div className={`
|
||||
w-5 h-5 bg-white rounded-full shadow-md transform transition-transform duration-200
|
||||
${useEncryption ? 'translate-x-5' : 'translate-x-0'}
|
||||
mt-0.5 ml-0.5
|
||||
`} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-[#cdd6f4] font-medium">Use encryption key</span>
|
||||
</label>
|
||||
|
||||
{useEncryption && (
|
||||
<input
|
||||
type="text"
|
||||
value={key}
|
||||
onChange={handleKeyChange}
|
||||
placeholder="Enter encryption key"
|
||||
className="flex-1 max-w-xs p-2 bg-[#45475a] border border-[#585b70] rounded-md text-[#cdd6f4] placeholder-[#9399b2] focus:border-[#89b4fa] focus:ring-1 focus:ring-[#89b4fa] focus:outline-none transition-colors"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MODE: ENCODE */}
|
||||
{mode === 'encode' && (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={handleChange}
|
||||
placeholder="Type here (max 45 chars)"
|
||||
className="w-full max-w-xl p-3 bg-[#45475a] border border-[#585b70] rounded-lg text-[#cdd6f4] placeholder-[#9399b2] focus:border-[#89b4fa] focus:ring-2 focus:ring-[#89b4fa] focus:ring-opacity-50 focus:outline-none transition-all"
|
||||
/>
|
||||
<p className="mt-2 text-sm text-[#a6adc8]">
|
||||
Allowed characters: space, period, comma, hyphen, double quote, slash, a-z.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#313244] p-4 rounded-lg border border-[#45475a]">
|
||||
<h3 className="text-lg font-semibold text-[#89b4fa] mb-3">Card Deck Output</h3>
|
||||
<div className="flex flex-wrap gap-1 max-w-full">
|
||||
{deck.map((idx, i) => (
|
||||
<Card key={i} index={idx} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* MODE: DECODE */}
|
||||
{mode === 'decode' && (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<button
|
||||
onClick={resetDecode}
|
||||
className="px-4 py-2 bg-[#585b70] hover:bg-[#6c7086] text-[#cdd6f4] rounded-lg transition-colors font-medium"
|
||||
>
|
||||
Reset order
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#313244] p-4 rounded-lg border border-[#45475a] mb-6">
|
||||
<h3 className="text-lg font-semibold text-[#89b4fa] mb-3">Drag Cards to Reorder</h3>
|
||||
{/* draggable card grid */}
|
||||
<div className="flex flex-wrap gap-1 max-w-full">
|
||||
{order.map((cardIdx, gridIdx) => (
|
||||
<Card
|
||||
key={gridIdx}
|
||||
index={cardIdx}
|
||||
draggable
|
||||
onDragStart={() => setDragIdx(gridIdx)}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setOverIdx(gridIdx);
|
||||
}}
|
||||
onDrop={() => handleDrop(gridIdx)}
|
||||
onDragLeave={() => setOverIdx(null)}
|
||||
dropTarget={overIdx === gridIdx && dragIdx !== gridIdx}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* decoded output */}
|
||||
{decodedText && (
|
||||
<div className="bg-[#313244] border border-[#45475a] rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-[#89b4fa] mb-3">Decoded Text</h3>
|
||||
<div className="p-3 bg-[#45475a] border border-[#585b70] rounded-md text-[#cdd6f4] whitespace-pre-wrap font-mono">
|
||||
{decodedText}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Performance info */}
|
||||
{elapsedMs !== null && (
|
||||
<div className="mt-8 text-center text-sm text-[#9399b2]">
|
||||
Last conversion took {elapsedMs.toFixed(1)} ms
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Credits section */}
|
||||
<footer className="mt-6 pt-6 border-t border-[#45475a] text-center">
|
||||
<div className="flex flex-col gap-3 text-sm">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-center gap-4">
|
||||
<p className="text-xs text-[#a6adc8]">
|
||||
Styled with <a href="https://catppuccin.com" className="text-[#f38ba8] hover:text-[#eba0ac] transition-colors" target="_blank" rel="noopener noreferrer">Catppuccin</a> theme
|
||||
</p>
|
||||
<p className="text-xs text-[#a6adc8]">
|
||||
Uses <a href="https://github.com/983" className="text-[#f9e2af] hover:text-[#f5e0a3] transition-colors" target="_blank" rel="noopener noreferrer">983</a>'s bignum library
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
className="inline-flex items-center justify-center gap-2 text-[#a6adc8] hover:text-[#cdd6f4] transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
50
web/src/components/Card.jsx
Normal file
50
web/src/components/Card.jsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
const cardList = [
|
||||
'A♠', '2♠', '3♠', '4♠', '5♠', '6♠', '7♠', '8♠', '9♠', '10♠', 'J♠', 'Q♠', 'K♠',
|
||||
'A♥', '2♥', '3♥', '4♥', '5♥', '6♥', '7♥', '8♥', '9♥', '10♥', 'J♥', 'Q♥', 'K♥',
|
||||
'A♦', '2♦', '3♦', '4♦', '5♦', '6♦', '7♦', '8♦', '9♦', '10♦', 'J♦', 'Q♦', 'K♦',
|
||||
'A♣', '2♣', '3♣', '4♣', '5♣', '6♣', '7♣', '8♣', '9♣', '10♣', 'J♣', 'Q♣', 'K♣',
|
||||
];
|
||||
|
||||
function getCardInfo(index) {
|
||||
const str = cardList[index];
|
||||
// Rank could be 1 or 2 characters (A vs 10)
|
||||
const suit = str.slice(-1);
|
||||
const rank = str.slice(0, -1);
|
||||
return { rank, suit };
|
||||
}
|
||||
|
||||
function Card({ index, onClick, selected = false, disabled = false, dropTarget = false, ...rest }) {
|
||||
const { rank, suit } = getCardInfo(index);
|
||||
const isRed = suit === '♥' || suit === '♦';
|
||||
|
||||
const baseClasses =
|
||||
'relative w-12 h-16 m-1 bg-white border rounded-lg shadow text-xs font-bold flex flex-col justify-between p-1 select-none';
|
||||
const colorClass = isRed ? 'text-red-600' : 'text-black';
|
||||
const selectedClass = selected ? 'ring-2 ring-blue-500' : '';
|
||||
const disabledClass = disabled ? 'opacity-50 cursor-default' : 'cursor-pointer hover:shadow-lg';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${baseClasses} ${selectedClass} ${disabledClass}`}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
{...rest}
|
||||
>
|
||||
{dropTarget && (
|
||||
<div className="absolute -left-1 top-0 h-full w-1 bg-blue-500"></div>
|
||||
)}
|
||||
<span className={colorClass}>
|
||||
{rank}
|
||||
{suit}
|
||||
</span>
|
||||
<span className={`self-end rotate-180 ${colorClass}`}>
|
||||
{rank}
|
||||
{suit}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { cardList };
|
||||
export default Card;
|
1
web/src/index.css
Normal file
1
web/src/index.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
13
web/src/main.jsx
Normal file
13
web/src/main.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<div className="h-screen bg-[#24273a">
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</div>
|
||||
|
||||
)
|
8
web/vite.config.js
Normal file
8
web/vite.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user