commit bf1d29e70481498853b8ff6a994a4f4beb3901a8 Author: Paul Butler Date: Sun Jan 26 08:16:00 2025 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/app/emoji.ts b/app/emoji.ts new file mode 100644 index 0000000..7be6ee8 --- /dev/null +++ b/app/emoji.ts @@ -0,0 +1,24 @@ +export const EMOJI_LIST = [ + "πŸ˜€", + "πŸ˜‚", + "πŸ₯°", + "😎", + "πŸ€”", + "πŸ‘", + "πŸ‘Ž", + "πŸ‘", + "πŸ™Œ", + "🀝", + "πŸŽ‰", + "πŸŽ‚", + "πŸ•", + "🌈", + "🌞", + "πŸŒ™", + "πŸ”₯", + "πŸ’―", + "πŸš€", + "πŸ‘€", + "πŸ’€", + "πŸ₯Ή", +] diff --git a/app/encoding.test.ts b/app/encoding.test.ts new file mode 100644 index 0000000..66ac0fa --- /dev/null +++ b/app/encoding.test.ts @@ -0,0 +1,29 @@ +import { expect, test, describe } from 'vitest' +import { encode, decode } from './encoding' +import { EMOJI_LIST } from './emoji' + +describe('emoji encoder/decoder', () => { + test('should correctly encode and decode strings', () => { + const testStrings = [ + 'Hello, World!', + 'Testing 123', + 'Special chars: !@#$%^&*()', + 'Unicode: δ½ ε₯½οΌŒδΈ–η•Œ', + '', // empty string + ' ' // space only + ] + + for (const emoji of EMOJI_LIST) { + for (const str of testStrings) { + const encoded = encode(emoji, str) + const decoded = decode(encoded) + + // Ensure decoding returns the original string + expect(decoded).toBe(str) + + // Ensure encoded string only contains emojis (optional test) + // expect(encoded).toMatch(/^[\p{Emoji}]+$/u) + } + } + }) +}) diff --git a/app/encoding.ts b/app/encoding.ts new file mode 100644 index 0000000..23f9792 --- /dev/null +++ b/app/encoding.ts @@ -0,0 +1,31 @@ +export function encode(emoji: string, text: string): string { + // convert the string to utf-8 bytes + const bytes = new TextEncoder().encode(text) + let encoded = emoji + + for (const char of bytes) { + const upper = char >> 4 + const lower = char & 0b1111 + encoded += String.fromCodePoint(upper + 0xfe00, lower + 0xfe00) + } + + return encoded +} + +export function decode(text: string): string { + let decoded = [] + + for (let i = 2; i < text.length; i += 2) { + const upper = text.charCodeAt(i) - 0xfe00 + const lower = text.charCodeAt(i + 1) - 0xfe00 + + if (upper < 0 || upper > 15 || lower < 0 || lower > 15) { + break; + } + + decoded.push((upper << 4) | lower) + } + + let decodedArray = new Uint8Array(decoded) + return new TextDecoder().decode(decodedArray) +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..ac68442 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,94 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 0 0% 3.9%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary: 0 0% 96.1%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent: 0 0% 96.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; + } + .dark { + --background: 0 0% 3.9%; + --foreground: 0 0% 98%; + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary: 0 0% 14.9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent: 0 0% 14.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..2489707 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' +import './globals.css' + +export const metadata: Metadata = { + title: 'Encode anything as an emoji', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + {children} + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..470c6da --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,88 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Switch } from "@/components/ui/switch" +import { Label } from "@/components/ui/label" +import { decode, encode } from "./encoding" +import { EmojiSelector } from "@/components/emoji-selector" +import { EMOJI_LIST } from "./emoji" + +export default function Base64EncoderDecoder() { + const [input, setInput] = useState("") + const [emoji, setEmoji] = useState("πŸ˜€") + const [output, setOutput] = useState("") + const [isEncoding, setIsEncoding] = useState(true) + const [error, setError] = useState("") + + const handleConvert = () => { + try { + if (isEncoding) { + setOutput(encode(emoji, input)) + } else { + setOutput(decode(input)) + } + setError("") + } catch (e) { + setError(`Error ${isEncoding ? "encoding" : "decoding"}: Invalid input`) + } + } + + const handleModeToggle = (checked: boolean) => { + setIsEncoding(checked) + setInput("") + setOutput("") + setError("") + } + + const handleEmojiSelect = (emoji: string) => { + setEmoji(emoji) + } + + return ( +
+ + + Encode Anything as an Emoji + + +

This tool allows you to encode a hidden message into an emoji. You can copy and paste an emoji with a hidden message in it to decode the message.

+ +
+ + + +
+ +