]> OzVa Git service - shopping-channel/commitdiff
Added shout and rearranged panels
authorMax Value <greenwoodw50@gmail.com>
Mon, 7 Apr 2025 00:37:41 +0000 (01:37 +0100)
committerMax Value <greenwoodw50@gmail.com>
Mon, 7 Apr 2025 00:37:41 +0000 (01:37 +0100)
+ added shout on the timer panel with sound on activation
+ star for shout display
+ number redact switch to admin panels (not implemented into GFX yet)
~ changed discount system to automatically set the 4 discounts
(now on the timer pannel)
~ moved some other panels
- removed database specified currency in favor of item specified

data.db
schema
static/assets/star5.svg [new file with mode: 0644]
static/sounds/shout.wav [new file with mode: 0644]
static/static.json
teleshopping.py
templates/gfx.html
templates/price.html
templates/sounds.html
templates/text.html
templates/timer.html

diff --git a/data.db b/data.db
index 2f81fe0370a2d08d9af26fc199e8724981754747..22d712eda31fe60646298e90c750dba3c7f09e60 100755 (executable)
Binary files a/data.db and b/data.db differ
diff --git a/schema b/schema
index 5869f54383c4da3585c142142ce6bc5e387f43aa..c117b71527cf7a2aeecdca7e6ffb49c06c319933 100644 (file)
--- a/schema
+++ b/schema
@@ -1,17 +1,16 @@
 CREATE TABLE state (
        id INTEGER PRIMARY KEY,
        item_id INTEGER,
+       discount INTEGER,
        discount_1 INTEGER,
        discount_2 INTEGER,
        discount_3 INTEGER,
        discount_4 INTEGER,
        discount_change INTEGER,
        percent_remaining INTEGER,
-       currency_symbol TEXT,
        crawler_top_index INTEGER,
        list_crawler_bottom TEXT,
        banner_index INTEGER,
-       bool_prefix INTEGER,
        bool_rounding INTEGER,
        bool_all INTEGER,
        bool_product INTEGER,
@@ -29,6 +28,7 @@ CREATE TABLE state (
        end_timer_5 INTEGER,
        end_timer_6 INTEGER,
        end_timer_main INTEGER,
+       end_timer_shout INTEGER,
        focus_timer_1 INTEGER,
        focus_timer_2 INTEGER,
        focus_timer_3 INTEGER,
@@ -39,23 +39,23 @@ CREATE TABLE state (
        current_position INTEGER,
        movement_speed INTEGER,
        movement_function TEXT,
+       shout TEXT,
        note TEXT
 );
 
 INSERT INTO state (
        id,
        item_id,
+       discount,
        discount_1,
        discount_2,
        discount_3,
        discount_4,
        discount_change,
        percent_remaining,
-       currency_symbol,
        crawler_top_index,
        list_crawler_bottom,
        banner_index,
-       bool_prefix,
        bool_rounding,
        bool_all,
        bool_product,
@@ -72,6 +72,8 @@ INSERT INTO state (
        end_timer_4,
        end_timer_5,
        end_timer_6,
+       end_timer_main,
+       end_timer_shout,
        focus_timer_1,
        focus_timer_2,
        focus_timer_3,
@@ -82,4 +84,5 @@ INSERT INTO state (
        current_position,
        movement_speed,
        movement_function,
-       note) VALUES (1, 0, 0.0, 0.0, 0.0, 0.0, 50, 100, '£', 0, '[0,1]', 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 40, 'linear', 'test note');
+       shout,
+       note) VALUES (1, 0, 0.0, 0.0, 0.0, 0.0, 0.0, 50, 100, 0, '[0,1]', 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 2, 2, 0, 0, 40, 'linear', 'test shout', 'test note');
diff --git a/static/assets/star5.svg b/static/assets/star5.svg
new file mode 100644 (file)
index 0000000..7ad77ec
--- /dev/null
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="400"
+   height="400"
+   viewBox="0 0 105.83333 105.83333"
+   version="1.1"
+   id="svg1"
+   sodipodi:docname="star5.svg"
+   inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#696969"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:zoom="0.49132984"
+     inkscape:cx="177.07046"
+     inkscape:cy="193.3528"
+     inkscape:window-width="1366"
+     inkscape:window-height="704"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg1" />
+  <defs
+     id="defs1" />
+  <metadata
+     id="metadata1">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <path
+     sodipodi:type="star"
+     style="fill:#ff4eac;fill-opacity:1;stroke:#ffffff;stroke-width:3.36072;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
+     id="path1"
+     inkscape:flatsided="false"
+     sodipodi:sides="9"
+     sodipodi:cx="26.30571"
+     sodipodi:cy="27.046715"
+     sodipodi:r1="53.991825"
+     sodipodi:r2="37.25436"
+     sodipodi:arg1="0.81451614"
+     sodipodi:arg2="1.163582"
+     inkscape:rounded="0"
+     inkscape:randomized="0"
+     d="M 63.356003,66.320025 41.06041,61.254699 29.443484,80.947286 15.619998,62.73569 -5.9372351,70.35387 -4.8204511,47.517412 -26.231122,39.496555 -10.696624,22.720668 -21.942441,2.8138212 0.74100701,-0.05187047 4.9220858,-22.530072 24.140646,-10.14468 41.792248,-24.676436 48.553343,-2.8352227 71.416087,-2.6209626 62.556124,18.456325 79.932279,33.316347 59.596934,43.767412 Z"
+     inkscape:transform-center-x="-0.51152643"
+     inkscape:transform-center-y="1.0220882"
+     transform="matrix(0.93880616,0,0,0.93880616,27.68061,26.445875)" />
+</svg>
diff --git a/static/sounds/shout.wav b/static/sounds/shout.wav
new file mode 100644 (file)
index 0000000..a760531
Binary files /dev/null and b/static/sounds/shout.wav differ
index 42b280b25e4c9122a6828feeaf08a3f0125af8af..1b2598dd7833821ae2693f2d6e554eb7d7d6ab03 100755 (executable)
                        "rating":"&starf;&starf;&starf;&starf;&starf;",
                        "subtext":"Starting soon!",
                        "description":"The show will be starting soon!",
-                       "currency":"£",
-                       "prefix":true,
+                       "currency":" Units",
+                       "prefix":false,
                        "origional_price":1,
                        "gallery_price":1,
                        "cost_price":1,
index 5ea67435e02ad1219e0ed4e46eec88f68b092446..cd4b51f5987f9a055507220027e975497d5c92f3 100755 (executable)
@@ -132,6 +132,8 @@ def admin_page(page):
                                        WHERE id = 1;
                                        """
 
+                               print(query)
+
                                cursor.execute(query)
                                connection.commit()
 
@@ -265,7 +267,7 @@ def check_database():
                                cursor.execute(load)
                        connection.commit()
 
-               generate_docs() # This might be a bad idea
+               generate_docs() # This might be a bad idea
 
                first_request = False
 
index ccb8e65bdbb628864823edcc574f930cb58caffb..e61f0938c2672fdcf21c103897f10aad00b07219 100644 (file)
@@ -16,6 +16,8 @@ body {
 
     opacity: 0;
     transition: opacity 1.5s;
+
+    overflow: hidden;
 }
 @keyframes spin {
     0% {transform: rotate(0turn) scale(1.2);}
@@ -24,11 +26,46 @@ body {
     75% {transform: rotate(0.75turn) scale(1);}
     100% {transform: rotate(1turn) scale(1.2);}
 }
+@keyframes spinOffset {
+    0% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0turn) scale(1.2);}
+    25% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.25turn) scale(1);}
+    50% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.5turn) scale(1.2);}
+    75% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(0.75turn) scale(1);}
+    100% {transform: perspective(300px) translate(-50%, -50%) rotateY( 15deg ) rotateZ(1turn) scale(1.2);}
+}
 @keyframes spinText {
     0% {transform: rotate(-0.01turn);}
     50% {transform: rotate(0.01turn);}
     100% {transform: rotate(-0.01turn);}
 }
+@keyframes spinTextOffset {
+    0% {transform: perspective(400px) translateX(-50%) rotateY( -15deg ) rotateZ(-0.01turn);}
+    50% {transform: perspective(400px) translateX(-50%) rotateY( -15deg) rotateZ(0.01turn);}
+    100% {transform: perspective(400px) translateX(-50%) rotateY( -15deg ) rotateZ(-0.01turn);}
+}
+@keyframes flicker {
+    0% {
+        color: black;
+        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+    }
+    49.999% {
+        color: black;
+        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+    }
+    50% {
+        color: white;
+        text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
+    }
+    99.999% {
+        color: white;
+        text-shadow: -2px -2px 0 #000, 2px -2px 0 #000, -2px 2px 0 #000, 2px 2px 0 #000;
+    }
+    100% {
+        color: black;
+        text-shadow: -2px -2px 0 #FFF, 2px -2px 0 #FFF, -2px 2px 0 #FFF, 2px 2px 0 #FFF;
+    }
+}
+
 .container {
     position: absolute;
 }
@@ -267,6 +304,49 @@ body {
     text-align: center;
     /*text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;*/
 }
+#shoutContainer {
+    position: absolute;
+    top: 50vh;
+    left: 50vw;
+    padding: 0;
+}
+#shoutBadgeContainer {
+    opacity: 0;
+    transform: translateX(-30vw);
+    transition: opacity 2s ease, transform 2s ease;
+}
+#shoutContainer.show > #shoutBadgeContainer {
+    opacity: 1;
+    transform: translateX(0vw);
+}
+#shoutTextContainer {
+    position: absolute;
+    top: 0vh;
+    opacity: 0;
+    transform: translateX(30vw);
+    transition: opacity 2s ease, transform 2s ease;
+    animation: flicker 1s linear 0s infinite;
+}
+#shoutContainer.show > #shoutTextContainer {
+    opacity: 1;
+    transform: translateX(0vw);
+}
+#shoutBadgeContainer > img {
+    width: 40vh;
+    height: 40vh;
+    padding: 0;
+    animation: spinOffset 10s linear 0s infinite;
+}
+#shout {
+    position: absolute;
+    top: -1em;
+    width: 100vw;
+    perspective: 400px;
+    animation: spinTextOffset 10s linear 0s infinite;
+    rotate: 0.025turn;
+    font-size: 6em;
+    text-align: center;
+}
 #currentPrice {
     line-height: 0.5em;
 }
@@ -457,6 +537,14 @@ circle {
                </div>
                <div id="timer2" class="box"></div>
                <div id="timer3"></div>
+               <div id="shoutContainer">
+            <div id="shoutBadgeContainer">
+                <img class="badge" src="./static/assets/star5.svg"></img>
+            </div>
+            <div id="shoutTextContainer">
+                <h1 id="shout">SHOUT TEXT</h1>
+            </div>
+               </div>
 
                <div class="container box sigilBox" id="sigil1">
                        <svg class="sigil" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
@@ -612,6 +700,7 @@ function update() {
             topText = dataStatic.text.crawler_top[data.crawler_top_index]
             bottomText = [];
             for (let i of data.list_crawler_bottom) {bottomText.push(dataStatic.text.crawler_bottom[i])}
+            shout
 
             // handle all optional elements showing/hiding
             if (data.bool_all) {all.classList.add("show");}
@@ -710,6 +799,16 @@ function update() {
                 badgeContainer.classList.remove("show");
             }
 
+            // Shout
+            document.getElementById("shout").innerHTML = data.shout;
+            let current = Math.round((Date.now() + data['timer_offset']) / 1000);
+            var time = data[`end_timer_shout`] - current;
+            if (time > 0) {
+                document.getElementById("shoutContainer").classList.add("show");
+            } else {
+                document.getElementById("shoutContainer").classList.remove("show");
+            }
+
             // Sigil handling
             for (let s = 0; s < 4; s++) {
                 if (data[`bool_sigil_${s+1}`]) {
@@ -749,13 +848,16 @@ function update() {
                 price = Math.round(price * 100) / 100;
             }
 
+            console.log(item.currency);
+            console.log(item.prefix);
+
             let priceString;
             let ezString;
-            if (data.bool_prefix) {priceString = `${data.currency_symbol}${price}`}
-            else {priceString = `${price}${data.currency_symbol}`};
+            if (item.prefix) {priceString = `${item.currency}${price}`}
+            else {priceString = `${price}${item.currency}`};
             let ezPrice = Math.round((price * 1.1) / 12);
-            if (data.bool_prefix) {ezString = `${data.currency_symbol}${ezPrice}`}
-            else {ezString = `${ezPrice}${data.currency_symbol}`};
+            if (item.prefix) {ezString = `${item.currency}${ezPrice}`}
+            else {ezString = `${ezPrice}${item.currency}`};
 
             // set discount, pricing and ez pay
             document.getElementById("currentPrice").innerHTML = `<em>Now only:</em> ${priceString}`;
@@ -769,8 +871,8 @@ function update() {
 
             if (discount.reduce((a, b)=> a*b, 1) <= 0.99) {
                 let origionalPrice = Math.round(item.origional_price);
-                if (data.bool_prefix) {origionalString = `${data.currency_symbol}${origionalPrice}`}
-                else {origionalString = `${origionalPrice}${data.currency_symbol}`};
+                if (item.prefix) {origionalString = `${item.currency}${origionalPrice}`}
+                else {origionalString = `${origionalPrice}${item.currency}`};
                 origional.innerHTML = `<s><em>WAS:</em> ${origionalString}</s>`;
             } else {
                 origional.innerHTML = `<em>NOW:</em> ${priceString}`;
index c4b7d4e15937ba7d8edb2363dd1f54d13be5ee1b..f456b01cedc075f86849f24219cc3e9a218a3240 100644 (file)
 
                        <fieldset>
                                <legend>Pricing</legend>
-                               <input type='text' name='currency_symbol' value='{{data.currency_symbol}}'>
-                               <label>Currency</label>
-                               {% if data.bool_prefix %}
-                                       <input type='radio' name='bool_prefix' value='0'>
-                                       <label>Postfix</label>
-                                       <input type='radio' name='bool_prefix' value='1' checked='checked'>
-                                       <label style='color: red;'>Prefix</label>
-                               {% else %}
-                                       <input type='radio' name='bool_prefix' value='0' checked='checked'>
-                                       <label style='color: red;'>Postfix</label>
-                                       <input type='radio' name='bool_prefix' value='1'>
-                                       <label>Prefix</label>
-                               {% endif %}
-                               <br>
 
                                <input type='number' name='percent_remaining' value='{{data.percent_remaining}}'>
                                <label>Stock left (%)</label> <label style='color: red;'>was {{data.percent_remaining}}</label><br>
 
                                <br>
+
+                               <input type='number' name='discount' step='1' value='{{data.discount}}' id="discount">
+                               <label>Discount</label> <label style='color: red;'>was {{data.discount}}</label><br>
+                               <em>Individual discounts (automatic)</em><br>
                                {% for d in (1,2,3,4) %}
-                                       <input type='number' name='discount_{{d}}' step='1' value='{{data['discount_' ~ d]}}'>
-                                       <label>Discount {{d}} (%)</label> <label style='color: red;'>was {{data['discount_' ~ d]}}</label><br>
+                                       <label>Discount {{d}}</label>
+                                       <input type='text' name='discount_{{d}}_display' value='{{data["discount_" ~ d]}}' disabled>
+                                       <br>
                                {% endfor %}
+
                                <br>
 
                                <input type='number' name='discount_change' value='{{data.discount_change}}'>
                                {% endif %}
                        </fieldset>
 
-                       <input type="submit" name="update" value="Update">
+                       <div style="display: none;">
+                               {% for d in (1,2,3,4) %}
+                                       <input type='text' name='discount_{{d}}' value='{{data["discount_" ~ d]}}' id="{{d}}">
+                               {% endfor %}
+                       </div>
+
+                       <input type="button" value="Update" onclick="submitForm();">
                </form>
+
+               <script>
+
+const deviation = 0.05;
+
+async function submitForm() {
+
+       if (document.getElementById("discount").value != {{data.discount}}) {
+
+               const totalDiscount = document.getElementById("discount").value / 100;
+               const discount1 = document.getElementById("1");
+               const discount2 = document.getElementById("2");
+               const discount3 = document.getElementById("3");
+               const discount4 = document.getElementById("4");
+
+               discount1.value = 0;
+               discount2.value = 0;
+               discount3.value = 0;
+               discount4.value = 0;
+
+               let activeDiscounts;
+               let targetValue;
+               switch (Math.floor(totalDiscount * 4)) {
+                       case 0:
+                               activeDiscounts = [discount1];
+                               break;
+                       case 1:
+                               activeDiscounts = [discount1, discount2];
+                               targetLeftValue = Math.sqrt(1 - totalDiscount);
+                               break;
+                       case 2:
+                               activeDiscounts = [discount1, discount2, discount3];
+                               targetLeftValue = Math.cbrt(1 - totalDiscount);
+                               break;
+                       default:
+                               activeDiscounts = [discount1, discount2, discount3, discount4];
+                               targetLeftValue = Math.pow(1 - totalDiscount, 0.25);
+                               break;
+               }
+
+               let discountRemaining = 1 - totalDiscount;
+               const lastDiscount = activeDiscounts.pop();
+
+               for (discount of activeDiscounts) {
+                       const u = 1 - Math.random();
+                       const v = Math.random();
+                       const z = Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v );
+                       discountLeft = z * deviation + targetLeftValue;
+
+                       discount.value = Math.round((1 - discountLeft) * 100);
+                       discountRemaining /= discountLeft;
+               }
+
+               lastDiscount.value = Math.round((1 - discountRemaining) * 100);
+       }
+
+       document.getElementsByTagName("form")[0].submit();
+}
+
+               </script>
        </body>
 </html>
index fbbd440394c631c6664e8acdc72365800d40728a..04206817d89799595c20510e47a8cd60de68ae6c 100644 (file)
                        </tr>
                        <tr>
                                <td><audio id="timer2" src=/static/sounds/timer2.wav controls></audio></td>
-                               <td>Timer 1</td>
+                               <td>Timer 2</td>
                        </tr>
                        <tr>
                                <td><audio id="timer3" src=/static/sounds/timer3.wav controls></audio></td>
-                               <td>Timer 1</td>
+                               <td>Timer 3</td>
                        </tr>
                        <tr>
                                <td><audio id="timer4" src=/static/sounds/timer4.wav controls></audio></td>
-                               <td>Timer 1</td>
+                               <td>Timer 4</td>
                        </tr>
                        <tr>
                                <td><audio id="timer5" src=/static/sounds/timer5.wav controls></audio></td>
-                               <td>Timer 1</td>
+                               <td>Timer 5</td>
                        </tr>
                        <tr>
                                <td><audio id="timer6" src=/static/sounds/timer6.wav controls></audio></td>
-                               <td>Timer 1</td>
+                               <td>Timer 6</td>
                        </tr>
                </table>
                <h2>Doomsday</h2>
                <audio id="clock" src=/static/sounds/clock.wav controls></audio>
+               <h2>Shout</h2>
+               <audio id="shout" src=/static/sounds/shout.wav controls></audio>
                <br>
                <input type="button" onclick="start();" value="Start listen" id="start" /> <em style="color:green" id="state"></em>
 
@@ -61,6 +63,7 @@ let pastTime = [
                0
        ];
 let pastClock = 0;
+let pastShout = 0;
 let timersPlaying = 0;
 
 const context = new AudioContext();
@@ -96,6 +99,7 @@ const timers = [
        document.getElementById("clock")
        ];
 const clock = document.getElementById("clock");
+const shout = document.getElementById("shout");
 
 for (let t = 0; t < 6; t++) {
        let timer = timers[t];
@@ -132,6 +136,17 @@ function getTimes() {
                                clock.play();
                        }
 
+                       if (pastShout != data["end_timer_shout"]) {
+                               pastShout = data["end_timer_shout"]
+                               if (context.state === "suspended") {
+                                       context.resume();
+                               }
+
+                               console.log(`Shout!`);
+                               shout.currentTime = 0.0;
+                               shout.play();
+                       }
+
                        timersPlaying = 0;
                        for (let t = 0; t < 6; t++) {
                                // if the timer is playing
@@ -187,9 +202,11 @@ function updateLeveler() {
                targetLevel = 1 / timersPlaying;
        }
        if (clock.currentTime > 0 && !clock.paused && !clock.ended) {
-               //targetLevel *= 0.75;
                targetWet = 0.75;
        }
+       if (shout.currentTime > 0 && !shout.paused && !shout.ended) {
+               targetLevel *= 0.75;
+       }
 
        if (Math.abs(targetLevel - amp.gain.value) < 0.1 / levelLength) {
                amp.gain.value = targetLevel;
index 25218a7e12c40f6a4601a5c93efb44ecfb60c405..71140a503f19c01647329ef4c098c870b159e1c1 100644 (file)
                                {% endfor %}
                        </fieldset>
 
+                       <fieldset>
+                               <legend>Number</legend>
+                               {% if data.bool_number %}
+                                       <input type='radio' name='bool_number' value='0'>
+                                       <label>Redact number</label>
+                                       <input type='radio' name='bool_number' value='1' checked='checked'>
+                                       <label style='color: red;'>Show number</label>
+                               {% else %}
+                                       <input type='radio' name='bool_number' value='0' checked='checked'>
+                                       <label style='color: red;'>Redact number</label>
+                                       <input type='radio' name='bool_number' value='1'>
+                                       <label>Show number</label>
+                               {% endif %}
+                       </fieldset>
+
                        <fieldset>
                                <legend>Note</legend>
                                <textarea name='note' rows='10' style='width: 100%;'>{{data.note|safe}}</textarea>
index efe7ae617dd8f41706c5d1e609bc6ac887dfef93..a91a98bf7b91f04fc0d74f30b26e5ad3b80ba6a9 100644 (file)
                        <input type="submit" name="update" value="Update">
                </form>
 
+               <br>
+
+               <form action="/admin/timer" method="POST">
+                       <fieldset>
+                               <legend>Shout text</legend>
+                               <textarea name='shout' rows='2' style='width: 100%;'></textarea>
+                               <label>Time to live </label>
+                               <input type="number" value="10" name="end_timer_shout"></input>
+                               <label> seconds (this defaults to 10s)</label>
+                       </fieldset>
+                       <input type="submit" name="update" value="Shout">
+               </form>
+
                <script>
 
 function update() {