Dup Goto 📝

Source

TIM/type-show 07-01 11:48:59
To Pop
348 lines, 934 words, 11103 chars Wednesday 2026-07-01 11:48:59

Type Show - A Simple Vibecoded App by Gemini

To Use CLICK HERE

This was interspersed with a more complex conversation about stuff like iterative convergence. What is below is precised to what will generate the html at the bottom.

I want a simple html page with javascript. It has a two-line textarea at the top for input (2em font size for readability). Below, it fills the window with the text as large as it can make it. This is to enable me to type and show across a videoconferencing link.

Pressing escape should clear the box.

Pressing Ctrl-Shift-D duplicates the current tab in a new tab.

This works so well I'm hitting scale issues. Having a dozen tabs with messages. I need a way to dump messages to local storage and recall them, and later (when I get ssh access to allsup.co again) a way of backing this all into a mysql database of messages. Thus once a message is written, it can be recalled and shown.

I needs to work on mobile in portrait orientation full stop that last version of the text goes off the side of the screen

It must support multiple simultaneous tabs when saving. And should, say, every second, see if a new message has been added in another tab.

The app needs a 'clear' button, along with save as I have to 'select alll backspace' on the iiPad.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Video Share Text</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body, html {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: row; 
            font-family: sans-serif;
            overflow: hidden;
            background-color: #f8f9fa;
        }

        /* Sidebar Layout (Desktop) */
        #sidebar {
            width: 260px;
            background: #222;
            color: #fff;
            display: flex;
            flex-direction: column;
            border-right: 1px solid #444;
            flex-shrink: 0;
        }
        #sidebar-header {
            padding: 15px 10px;
            font-weight: bold;
            border-bottom: 1px solid #444;
        }
        #history-list {
            flex: 1;
            overflow-y: auto;
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .history-item {
            padding: 12px 10px;
            border-bottom: 1px solid #333;
            cursor: pointer;
            font-size: 0.9em;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .history-item:hover {
            background: #333;
        }
        .delete-btn {
            background: none;
            border: none;
            color: #ff5c5c;
            cursor: pointer;
            font-weight: bold;
            padding: 0 5px;
            font-size: 1.2em;
        }

        /* Main Workspace */
        #main-content {
            flex: 1;
            display: flex;
            flex-direction: column;
            height: 100%;
            min-width: 0; 
        }
        #input-container {
            padding: 10px;
            background: #ffffff;
            border-bottom: 2px solid #ccc;
            display: flex;
            gap: 10px;
            align-items: center;
        }
        textarea {
            flex: 1;
            height: 2.5em; 
            font-size: 1.5em; 
            font-weight: bold;
            padding: 5px;
            resize: none;
            border: 1px solid #aaa;
            border-radius: 4px;
        }
        .button-group {
            display: flex;
            flex-direction: column;
            gap: 5px;
        }
        .action-btn {
            padding: 8px 15px;
            font-size: 1.0em;
            font-weight: bold;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            white-space: nowrap;
        }
        #save-btn {
            background: #007bff;
        }
        #save-btn:hover {
            background: #0056b3;
        }
        #clear-btn {
            background: #dc3545;
        }
        #clear-btn:hover {
            background: #bd2130;
        }
        #output-container {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 15px;
            overflow: hidden;
            background: #111111; 
        }
        #output-text {
            color: #ffffff;
            font-weight: bold;
            white-space: pre-wrap; 
            text-align: center;
            line-height: 1.1;
            word-break: break-word; 
            width: 100%;
        }

        /* MOBILE PORTRAIT OPTIMIZATION */
        @media (max-width: 600px) {
            body, html {
                flex-direction: column; 
            }
            #sidebar {
                width: 100%;
                height: 160px; 
                border-right: none;
                border-top: 2px solid #444;
            }
            #sidebar-header {
                padding: 8px 10px;
                font-size: 0.85em;
            }
            .history-item {
                padding: 10px;
            }
            textarea {
                font-size: 1.2em; 
            }
            .button-group {
                flex-direction: row;
            }
        }
    </style>
</head>
<body>

    <div id="sidebar">
        <div id="sidebar-header">Saved History Log</div>
        <ul id="history-list"></ul>
    </div>

    <div id="main-content">
        <div id="input-container">
            <textarea id="text-input" placeholder="Type here..." autofocus></textarea>
            <div class="button-group">
                <button id="save-btn" class="action-btn">Save</button>
                <button id="clear-btn" class="action-btn">Clear</button>
            </div>
        </div>

        <div id="output-container">
            <div id="output-text"></div>
        </div>
    </div>

    <script>
        const input = document.getElementById('text-input');
        const output = document.getElementById('output-text');
        const container = document.getElementById('output-container');
        const historyList = document.getElementById('history-list');
        const saveBtn = document.getElementById('save-btn');
        const clearBtn = document.getElementById('clear-btn');

        let messages = [];

        function resizeText() {
            const text = input.value || "Type something...";
            output.textContent = text;

            let low = 10;
            let high = 500; 
            let optimalSize = low;

            while (low <= high) {
                let mid = Math.floor((low + high) / 2);
                output.style.fontSize = mid + 'px';

                if (output.scrollWidth <= container.clientWidth && output.scrollHeight <= container.clientHeight) {
                    optimalSize = mid;
                    low = mid + 1; 
                } else {
                    high = mid - 1; 
                }
            }
            output.style.fontSize = optimalSize + 'px';
        }

        // --- LOCAL STORAGE MANAGER ---
        function loadMessages() {
            messages = JSON.parse(localStorage.getItem('shared_messages')) || [];
        }

        function renderHistory() {
            historyList.innerHTML = '';
            messages.forEach((msg, index) => {
                const li = document.createElement('li');
                li.className = 'history-item';
                const displaySnippet = msg.replace(/\n/g, " ");

                li.innerHTML = `
                    <span style="flex:1; overflow:hidden; text-overflow:ellipsis;">${displaySnippet}</span>
                    <button class="delete-btn" data-index="${index}">&times;</button>
                `;

                li.addEventListener('click', (e) => {
                    if (!e.target.classList.contains('delete-btn')) {
                        input.value = msg;
                        resizeText();
                        input.focus();
                    }
                });
                historyList.appendChild(li);
            });

            document.querySelectorAll('.delete-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    loadMessages(); 
                    messages.splice(btn.getAttribute('data-index'), 1);
                    localStorage.setItem('shared_messages', JSON.stringify(messages));
                    renderHistory();
                });
            });
        }

        function saveMessage() {
            const val = input.value.trim();
            if (!val) return;

            loadMessages(); 
            messages = messages.filter(m => m !== val);
            messages.unshift(val);

            localStorage.setItem('shared_messages', JSON.stringify(messages));
            renderHistory();

            if (typeof syncToExternalDatabase === "function") syncToExternalDatabase(val);
        }

        function wipeInput() {
            input.value = '';
            resizeText();
            input.focus();
        }

        function syncToExternalDatabase(msg) {
            console.log("MySQL ready output payload:", msg);
        }

        // --- CROSS-TAB REALTIME SYNC ---
        window.addEventListener('storage', function(event) {
            if (event.key === 'shared_messages') {
                loadMessages();
                renderHistory();

                if (messages.length > 0 && input.value !== messages[0]) {
                    input.value = messages[0];
                    resizeText();
                }
            }
        });

        // Event triggers
        input.addEventListener('input', resizeText);
        window.addEventListener('resize', resizeText);
        saveBtn.addEventListener('click', saveMessage);
        clearBtn.addEventListener('click', wipeInput);

        window.addEventListener('keydown', function(event) {
            if (event.key === 'Escape') {
                wipeInput();
            }
            if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === 'd') {
                event.preventDefault(); 
                window.open(window.location.href, '_blank');
            }
            if (event.key === 'Enter' && !event.shiftKey && document.activeElement === input) {
                event.preventDefault(); 
                saveMessage();
            }
        });

        // Init layout
        loadMessages();
        renderHistory();
        resizeText();
    </script>
</body>
</html>