forro

Forro is a end-to-end encrypted contract form based on PGP.
git clone git://git.defalsify.org/forro.git
Info | Log | Files | Refs | README | LICENSE

commit ff50b613b651068652ba7d764f8017b73ebe962c
parent a7e9cfae214d1fe2fdb5d3ee217faadc64d97855
Author: AbdallahWario <abdallwario96@gmail.com>
Date:   Thu, 22 Aug 2024 03:17:13 -0400

ui made more intuitive and responsive

Diffstat:
Mindex.html | 274+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mstyle.css | 492++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 651 insertions(+), 115 deletions(-)

diff --git a/index.html b/index.html @@ -1,6 +1,8 @@ <html> <head> <title>Forro contact form</title> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <script defer src="node_modules/alpinejs/dist/cdn.min.js"></script> <script src="node_modules/openpgp/dist/openpgp.min.js"></script> <script src="node_modules/jssha/dist/sha256.js"></script> @@ -14,7 +16,9 @@ <link rel="stylesheet" type="text/css" href="style.css"></link> </head> - <body x-data="{ + <body + + x-data="{ unlock_set: false, message_status: '', message_count: g_counter, @@ -78,81 +82,16 @@ > - <h1><a href="https://defalsify.org/git/forro" - x-text="getTitle"></a> - </h1> + <div id="app"> - <div id="helpdiv" style="height: 6em;" - x-show="isHelp" - > - <hr/> - <template x-data="{ - help_cap: 128, // if set to 0 then no buffer - help_display_count: 5, // if help_cap is 0, ignore - help_contents: [], - help_lastcount: 0, - help_count: 0, - help_crsr: -1, - - addHelpContents(v) { - if (this.help_cap == 0) { - this.help_display_count = v.length; - this.help_count = v.length; - this.help_contents = v; - this.help_crsr = this.help_count - 1; - } else { - for (let i = 0; i < v.length; i++) { - this.help_crsr += 1; - this.help_crsr %= this.help_cap; - this.help_contents[this.help_crsr] = v[i]; - if (this.help_count < this.help_cap) { - this.help_count += 1; - } - } - } - if (v.length > 0) { - this.help_lastcount = v.length; - } - }, - - get helpContents() { - if (this.help_crsr < 0) { - return []; - } - let contents = []; - - let l = this.help_display_count; - if (l > this.help_count) { - l = this.help_count; - } + + <h1><a href="https://defalsify.org/git/forro" + x-text="getTitle"></a> + </h1> - let c = (this.help_crsr - l + 1); - if (c < 0) { - c = l + c; - } - - new_threshold = l - this.help_lastcount; - for (let i = 0; i < l; i++) { - if (this.help_cap > 0) { - c %= this.help_cap; - } - is_old = i < new_threshold; - v = [this.help_contents[c], is_old ? 'old' : 'new']; - contents.push(v); //this.help_contents[c]); - c += 1; - } - return contents; - }, - }" - x-init="tryHelpFor('welcome');" - x-for="(v) in helpContents" - @help.window="addHelpContents($event.detail.v);"> - <p style="font-size: 0.8em; line-height: 0.3em;" x-html="v[0]" :class="v[1]" /> - </template> - </div> - <hr/> + <div id="localkey" x-data="{ passphrase_cache: '', passphrase_status: '', @@ -165,6 +104,8 @@ x-show='!unlockedKey' + class="localkey-container" + > <input name="pwd" type="password" x-model='passphrase_cache' @@ -172,6 +113,8 @@ @passfail.window='passphrase_status = "wrong_passphrase"; passphrase_cache = "";'; @messagestatechange.window='if (checkState(STATE["LOCAL_KEY"])) { passphrase_default_status = "please unlock key"; };' @rst.window='passphrase_status = "please create new key";' + class="input-passphrase" + > <button x-data="{ go_label: 'go', @@ -183,12 +126,19 @@ @rst.window='go_label = "go";' @click='setPwd(passphrase_cache);' + class="btn-primary" + ></button> - <button x-show='!haveKey' @click='tryHelpFor("nopass"); setPwd();' >without passphrase</button> + <button x-show='!haveKey' @click='tryHelpFor("nopass"); setPwd();' class="btn-secondary" + >without passphrase</button> </div> - <div id="message_panel" + + + <div class="container"> + + <div class="form" id="message_panel" x-show='unlockedKey' x-init='setUp();' x-data="{ @@ -267,32 +217,131 @@ document.getElementById("fileAdder").value=""; } '> - <dl> - <dt>Status:</dt> - <dd x-text="message_status" x-on:messagestatechange.window="defaultname = !g_local_key_identified;"></dd> - - <dt>Your identity:</dt> - <dd><a x-text="keyDisplay" title="Click to download your private key" x-bind:href="localKeyArmor" x-bind:download="localKeyFilename"></a></dd> - <dt>Their identity:</dt> - <dd><a x-text="rkeyDisplay" title="Click to download the recipient's public key" x-bind:href="remoteKeyArmor" x-bind:download="remoteKeyFilename"></a></dd> - <dt>Message number:</dt> - <dd x-text="message_count"></dd> - <dt>Your receipt:<dt> - <dd><a x-bind:href="g_data_endpoint + '/' + rcpt" x-text="rcpt"></a></dd> - <dt>Add message:</dt> - <dd> + + + <div id="helpdiv" + x-show="isHelp" + > + <h3 class="title">Let's get in touch</h3> + + <template x-data="{ + help_cap: 128, // if set to 0 then no buffer + help_display_count: 5, // if help_cap is 0, ignore + help_contents: [], + help_lastcount: 0, + help_count: 0, + help_crsr: -1, + + addHelpContents(v) { + if (this.help_cap == 0) { + this.help_display_count = v.length; + this.help_count = v.length; + this.help_contents = v; + this.help_crsr = this.help_count - 1; + } else { + for (let i = 0; i < v.length; i++) { + this.help_crsr += 1; + this.help_crsr %= this.help_cap; + this.help_contents[this.help_crsr] = v[i]; + if (this.help_count < this.help_cap) { + this.help_count += 1; + } + } + } + if (v.length > 0) { + this.help_lastcount = v.length; + } + }, + + get helpContents() { + if (this.help_crsr < 0) { + return []; + } + let contents = []; + + let l = this.help_display_count; + if (l > this.help_count) { + l = this.help_count; + } + + + let c = (this.help_crsr - l + 1); + if (c < 0) { + c = l + c; + } + + new_threshold = l - this.help_lastcount; + for (let i = 0; i < l; i++) { + if (this.help_cap > 0) { + c %= this.help_cap; + } + is_old = i < new_threshold; + v = [this.help_contents[c], is_old ? 'old' : 'new']; + contents.push(v); //this.help_contents[c]); + c += 1; + } + return contents; + }, + }" + x-init="tryHelpFor('welcome');" + x-for="(v) in helpContents" + @help.window="addHelpContents($event.detail.v);"> + <p x-html="v[0]" :class="v[1] == 'old' ? 'help-text old' : 'help-text new'" /> + + </template> + </div> + + <div class="contact-form"> + + + + <form autocomplete="off"> + <h3 class="title">Get in touch!</h3> + <div class="details"> + + <div > + <label for="status">Status:</label> + <span x-text="message_status" x-on:messagestatechange.window="defaultname = !g_local_key_identified;"></span> + </div> + + <div> + <label for="your-identity">Your identity:</label> + <p><a x-text="keyDisplay" title="Click to download your private key" x-bind:href="localKeyArmor" x-bind:download="localKeyFilename"></a></p> + </div> + + + + <div> + <label for="their-identity">Their identity:</label> + <p><a x-text="rkeyDisplay" title="Click to download the recipient's public key" x-bind:href="remoteKeyArmor" x-bind:download="remoteKeyFilename"></a></p> + </div> + + + <div> + <label for="message-number">Message number:</label> + <span x-text="message_count"></span> + </div> + + <div> + <label for="receipt">Your receipt:</label> + <p><a x-bind:href="g_data_endpoint + '/' + rcpt" x-text="rcpt"></a></p> + </div> + </div> + <div class="input-container textarea"> + <label for="">Message:</label> + <textarea - cols=72 - rows=10 + name="message" + placeholder="message" + class="input" x-model="content" @focus="tryHelpFor('writemsg');" @messagestatechange.window="if (checkState(STATE.ACK_MESSAGE)) {console.log('foo'); content = '';}" > </textarea> - </dd> - <dt>Add files:</dt> - <dd> - <input type="file" id="fileAdder" + </div> + <div class="input-container"> + <input class="btn" type="file" id="fileAdder" @change="fileChange();" /> <ol> @@ -305,14 +354,13 @@ <li x-text="v"></li> </template> </ol> - </dd> - </dl> - <br/> + </div> + <div x-data='{ realname: "", realemail: "", }'> - <div x-show='defaultname' + <div x-show='defaultname && !g_local_key_identified' x-data='{ identify: false, }' @@ -323,8 +371,12 @@ <option value=0 defaultselected>Stay anonymous</option> <option value=1>Identify yourself</option> </select> - <div x-show="identify"> - <input name="id_name" placeholder="name" x-model="realname" /> <input name="id_email" placeholder="email" x-model="realemail" /> + <div class="input-container identify" x-show="identify"> + + <input name="id_name" class="input" type="text" placeholder="Name" x-model="realname" /> + <input name="id_email" class="input" type="email" placeholder="Email" x-model="realemail" /> + + </div> </div> <div x-data="{ @@ -333,21 +385,25 @@ x-on:messagestatechange.window='ready = checkState(STATE["RTS"]);' > <button + class="btn" x-bind:disabled='!ready' - @click="tryDispatch(content, realname, realemail, filez);">sign, encrypt and send</button> + @click="tryDispatch(content, realname, realemail, filez);"> + sign, encrypt and send + </button> </div> </div> + + </form> + <div id="reset" x-data="{ rst: false }"> + <button x-show="haveKey && !rst" @click="rst = true;">Discard key</button> + <button x-show="rst" @click="if (confirm('Are you sure you want to discard the key?')) { rst = false; $dispatch('rst'); purgeLocalKey(); }">Discard key</button> </div> - <div id="reset" - x-data="{ - rst: false, - }"> - <button x-show='haveKey && !rst' @click='rst = true;'>Discard key</button> - <button x-show='rst' @dblclick='rst = false; $dispatch("rst"); purgeLocalKey();'>Double click to confirm discard key</button> + </div> </div> - + </div> + + <div id="dev" x-show='isDev'> - <hr/> <h2>Devmode details</h2> <dl> <dt>last event</dt> diff --git a/style.css b/style.css @@ -1,8 +1,487 @@ -div#helpdiv .old { - color: #aaa; +@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800&display=swap"); + +* { + margin: 0; + padding: 0; + box-sizing: border-box; } -h1 { - margin-block-end: 1em; - margin-block-start: 0.5em; - font-size: 2em; + +body, +input, +textarea { + font-family: "Poppins", sans-serif; } + +.container { + position: relative; + width: 100%; + min-height: 100vh; + padding: 2rem; + background-color: #fafafa; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; +} +.localkey-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; + padding: 2rem; + background-color: #ffffff; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 50%; + margin: 2rem auto; +} + +.input-passphrase { + width: 80%; + padding: 0.75rem; + border: 1px solid #d1d5db; + border-radius: 0.375rem; + outline: none; + font-size: 1rem; +} + +.input-passphrase:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25); +} + +.btn-primary, .btn-secondary { + width: 80%; + padding: 0.75rem 1rem; + text-align: center; + font-weight: 500; + border-radius: 0.375rem; + cursor: pointer; + outline: none; + transition: background-color 0.3s ease, box-shadow 0.3s ease; +} + +.btn-primary { + background-color: #3b82f6; + color: #ffffff; +} + +.btn-primary:hover { + background-color: #2563eb; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.btn-secondary { + background-color: #6b7280; + color: #ffffff; +} + +.btn-secondary:hover { + background-color: #4b5563; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.help-text { + font-size: 1rem; + color: #333; + margin-bottom: 0.5rem; + transition: background-color 0.3s, color 0.3s; + padding: 0.5rem; + border-radius: 4px; +} + +.help-text.new { + background-color: #e0f7fa; + color: #007bff; +} + +.help-text.old { + background-color: #f1f1f1; + color: #999; +} + +.form { + width: 100%; + max-width: 820px; + background-color: #fff; + border-radius: 10px; + box-shadow: 0 0 20px 1px rgba(0, 0, 0, 0.1); + z-index: 1000; + overflow: hidden; + display: grid; + grid-template-columns: repeat(2, 1fr); +} + +.contact-form { + background-color: #1abc9c; + position: relative; +} + + +.contact-form:before { + content: ""; + position: absolute; + width: 26px; + height: 26px; + background-color: #1abc9c; + transform: rotate(45deg); + top: 50px; + left: -13px; +} + +form { + padding: 1rem; + z-index: 10; + overflow: hidden; + position: relative; +} + +.title { + color: #fff; + font-weight: 500; + font-size: 1.5rem; + line-height: 1; + margin-bottom: 0.7rem; +} + +.input-container { + position: relative; + margin: 1rem 0; +} + +form { + max-width: 600px; + width: 100%; + padding: 2rem; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-radius: 8px; + } + + + + .input-container input[type="file"] { + padding: 10px; + font-size: 16px; + color: #1abc9c; + background-color: #ffffff; + border: 2px solid #1abc9c; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease, color 0.3s ease; + } + + .input-container input[type="file"]:hover { + background-color: #0056b3; + } + + .input-container input[type="file"]:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5); + } + + .input-container ol { + list-style-type: decimal; + padding-left: 20px; + } + + .input-container li { + font-size: 14px; + color: #333; + padding: 5px 0; + } + +.input { + width: 100%; + outline: none; + border: 2px solid #fafafa; + background: none; + padding: 0.6rem 1.2rem; + color: #000; + font-weight: 500; + letter-spacing: 0.5px; + border-radius: 5px; + transition: 0.3s; + font-size: 16px; + +} + +textarea.input { + padding: 0.8rem 1.2rem; + min-height: 150px; + border-radius: 5px; + resize: none; + overflow-y: auto; +} + + + +.btn { + padding: 0.6rem 1.3rem; + background-color: #fff; + border: 2px solid #fafafa; + font-size: 0.95rem; + color: #1abc9c; + line-height: 1; + border-radius: 5px; + outline: none; + cursor: pointer; + transition: 0.3s; + margin: 0; +} + +#identity_select{ + margin-bottom: 10px; + width: 200px; + padding: 10px; + font-size: 16px; + color: #333; + background-color: #f8f8f8; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background-image: url('data:image/svg+xml;utf8,<svg fill="%23333" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10l5 5 5-5z"/></svg>'); + background-repeat: no-repeat; + background-position-x: 95%; + background-position-y: 50%; + cursor: pointer; +} +.btn:hover { + background-color: transparent; + color: #fff; +} + +.input-container span { + position: absolute; + top: 0; + left: 25px; + transform: translateY(-50%); + font-size: 0.8rem; + padding: 0 0.4rem; + color: transparent; + pointer-events: none; + z-index: 500; +} + +.input-container span:before, +.input-container span:after { + content: ""; + position: absolute; + width: 10%; + opacity: 0; + transition: 0.3s; + height: 5px; + background-color: #1abc9c; + top: 50%; + transform: translateY(-50%); +} + +.input-container span:before { + left: 50%; +} + +.input-container span:after { + right: 50%; +} + + + + +.input-container.focus span:before, +.input-container.focus span:after { + width: 50%; + opacity: 1; +} + +#helpdiv { + padding: 2.3rem 2.2rem; + position: relative; +} + +#helpdiv .title { + color: #1abc9c; +} + +.text { + color: #333; + margin: 1.5rem 0 2rem 0; +} + +.icon { + width: 28px; + margin-right: 0.7rem; +} + + + + +.square { + position: absolute; + height: 400px; + top: 50%; + left: 50%; + transform: translate(181%, 11%); + opacity: 0.2; +} + +.details{ + display: flex; + flex-direction: column; + gap: 10px; +} +.identify{ + display: flex; + flex-direction: column; + gap: 10px; +} +#reset { + display: flex; + align-items: center; + gap: 10px; + justify-content: center; + margin-top: 5px; + margin-bottom:5px ; + } + + #reset button { + padding: 10px 20px; + font-size: 16px; + color: #fff; + background-color: #007bff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s ease; + } + + #reset button:hover { + background-color: #0056b3; + } + + #reset button:active { + background-color: #004085; + } + + #reset button:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.5); + } + + p, +span,dd { + word-wrap: break-word; + word-break: break-all; + max-width: 100%; +} +#dev { + margin-top: 2rem; + padding: 1rem; + background-color: #f1f1f1; + border: 1px solid #ccc; + border-radius: 8px; +} + +#dev hr { + margin-bottom: 1rem; +} + +#dev h2 { + font-size: 1.5rem; + margin-bottom: 1rem; +} + +#dev dl { + display: flex; + flex-wrap: wrap; +} + +#dev dt { + width: 100%; + font-weight: bold; + margin-top: 1rem; +} + +#dev dd { + width: 100%; + margin-left: 0; + margin-bottom: 1rem; + overflow-wrap: break-word; +} + +@media (max-width: 850px) { + .form { + grid-template-columns: 1fr; + } + + + .square { + transform: translate(140%, 43%); + height: 350px; + } + + + .text { + margin: 1rem 0 1.5rem 0; + } + + + .localkey-container { + width: 80%; + padding: 1.5rem; +} + +.input-passphrase, .btn-primary, .btn-secondary { + width: 100%; + font-size: 1rem; +} +} + +@media (max-width: 480px) { + .container { + padding: 0.2rem; + } + + + .square{ + display: none; + } + + + form, #helpdiv { + padding: 0.5rem; + } + + .text, + .information{ + font-size: 0.8rem; + } + + .title { + font-size: 1.15rem; + } + + .input { + padding: 0.45rem 1.2rem; + } + + .btn { + padding: 0.45rem 1.2rem; + } + .localkey-container { + width: 95%; + padding: 1rem; +} + +.input-passphrase, .btn-primary, .btn-secondary { + width: 100%; + font-size: 0.9rem; + padding: 0.65rem; +} + +.btn-primary, .btn-secondary { + font-size: 1rem; + padding: 0.75rem 1rem; +} +} +\ No newline at end of file