commit 74e9056c49b139c0f271fcc257fef3c0fbc22733 Author: itwrx Date: Thu May 15 08:01:35 2025 -0500 initial commit diff --git a/assets/css/fmn.css b/assets/css/fmn.css new file mode 100644 index 0000000..cfdde11 --- /dev/null +++ b/assets/css/fmn.css @@ -0,0 +1 @@ +/*! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.-mt-3{margin-top:-.75rem}.ml-1{margin-left:.25rem}.ml-8{margin-left:2rem}.mr-4{margin-right:1rem}.mt-3{margin-top:.75rem}.mt-3\.5{margin-top:.875rem}.mt-4{margin-top:1rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.hidden{display:none}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-2\/5{width:40%}.w-3\/4{width:75%}.flex-1{flex:1 1 0%}.basis-1\/4{flex-basis:25%}.basis-1\/5{flex-basis:20%}.basis-3\/5{flex-basis:60%}.basis-1\/12{flex-basis:8.333333%}.basis-2\/4{flex-basis:50%}.flex-row{flex-direction:row}.rounded-lg{border-radius:.5rem}.border-b{border-bottom-width:1px}.border-indigo-400{--tw-border-opacity:1;border-color:rgb(129 140 248/var(--tw-border-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(22 101 52/var(--tw-bg-opacity))}.bg-slate-400{--tw-bg-opacity:1;background-color:rgb(148 163 184/var(--tw-bg-opacity))}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-4{padding:1rem}.px-12{padding-left:3rem;padding-right:3rem}.py-8{padding-bottom:2rem;padding-top:2rem}.text-center{text-align:center}.text-end{text-align:end}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-xl{font-size:1.25rem}.text-lg,.text-xl{line-height:1.75rem}.text-lg{font-size:1.125rem}.font-semibold{font-weight:600}.text-indigo-300{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-slate-950{--tw-text-opacity:1;color:rgb(2 6 23/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity))}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}body,html{height:100%}body{--tw-text-opacity:1;color:rgb(165 180 252/var(--tw-text-opacity))}main{line-height:1.625}h1,header{font-size:1.25rem;line-height:1.75rem}.main-bg{background:#14151b}a{color:rgb(199 210 254/var(--tw-text-opacity));cursor:pointer}a,a:hover{--tw-text-opacity:1}a:hover{color:rgb(224 231 255/var(--tw-text-opacity))}input[type=email],input[type=password],input[type=text],select,textarea{--tw-bg-opacity:1;background-color:rgb(38 38 38/var(--tw-bg-opacity));border:2px solid #6c78b1;border-radius:5px;color:#fff;margin:1em 0 0 .5em;padding:.15em .35em}input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:50%;border-width:2px;height:14px;width:14px}input:focus,textarea:focus{border-color:#a5b4fc;box-shadow:inset 0 0 6px #a5b4fc;outline:none}input:checked{--tw-bg-opacity:1;background-color:rgb(129 140 248/var(--tw-bg-opacity))}.btn-grn{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(22 101 52/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);font-weight:600;padding:3px .5rem}.btn-grn,.btn-grn:hover{color:rgb(255 255 255/var(--tw-text-opacity))}.btn-grn:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.btn-orange{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(217 119 6/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:rgb(255 255 255/var(--tw-text-opacity));font-weight:600;padding:3px .5rem}.btn-orange:hover{background-color:rgb(245 158 11/var(--tw-bg-opacity))}.btn-orange:hover,.btn-red{--tw-bg-opacity:1;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.btn-red{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(159 18 57/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);font-weight:600;padding:3px .5rem}.btn-red:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(190 18 60/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.btn-blu{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(96 165 250/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:rgb(12 74 110/var(--tw-text-opacity));font-weight:600;padding:2px .5rem}.btn-blu:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(147 197 253/var(--tw-bg-opacity));color:rgb(12 74 110/var(--tw-text-opacity))}.btn-disabled{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(82 82 82/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);color:rgb(255 255 255/var(--tw-text-opacity));font-weight:600;padding:3px .5rem}.btn-disabled:hover{background-color:rgb(115 115 115/var(--tw-bg-opacity))}.a-btn-grn,.btn-disabled:hover{--tw-bg-opacity:1;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.a-btn-grn{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(22 101 52/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);font-weight:600;padding:5px .5rem}.a-btn-grn:hover{background-color:rgb(21 128 61/var(--tw-bg-opacity))}.a-btn-grn:hover,.a-btn-red{--tw-bg-opacity:1;--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.a-btn-red{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);background-color:rgb(159 18 57/var(--tw-bg-opacity));border-radius:.5rem;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow);font-weight:600;padding:5px .5rem}.a-btn-red:hover{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:rgb(190 18 60/var(--tw-bg-opacity));color:rgb(255 255 255/var(--tw-text-opacity))}.form-success{--tw-text-opacity:1;color:rgb(190 242 100/var(--tw-text-opacity))}.form-error{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.hide{display:none}.drop-btn{border:none;cursor:pointer}.drop-down{display:inline-block}.drop-content{display:none}.drop-content a{display:block;text-decoration:none}.show{display:block}.help-text{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity));cursor:help}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.focus\:ring-yellow-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(253 224 71/var(--tw-ring-opacity))}@media (min-width:1024px){.lg\:ml-4{margin-left:1rem}.lg\:ml-44{margin-left:11rem}.lg\:mt-0{margin-top:0}.lg\:mt-12{margin-top:3rem}.lg\:block{display:block}.lg\:flex{display:flex}.lg\:hidden{display:none}.lg\:w-2\/5{width:40%}.lg\:max-w-\[1600px\]{max-width:1600px}.lg\:basis-1\/12{flex-basis:8.333333%}.lg\:basis-1\/3{flex-basis:33.333333%}.lg\:basis-10\/12{flex-basis:83.333333%}.lg\:basis-2\/3{flex-basis:66.666667%}.lg\:basis-2\/4{flex-basis:50%}.lg\:flex-row{flex-direction:row}.lg\:flex-col{flex-direction:column}.lg\:py-2{padding-bottom:.5rem;padding-top:.5rem}} \ No newline at end of file diff --git a/assets/img/AGPLv3_logo.png b/assets/img/AGPLv3_logo.png new file mode 100644 index 0000000..06143aa Binary files /dev/null and b/assets/img/AGPLv3_logo.png differ diff --git a/assets/img/favicon.ico b/assets/img/favicon.ico new file mode 100644 index 0000000..01e3631 Binary files /dev/null and b/assets/img/favicon.ico differ diff --git a/assets/img/fmn_logo_125x117.png b/assets/img/fmn_logo_125x117.png new file mode 100644 index 0000000..cfd50b0 Binary files /dev/null and b/assets/img/fmn_logo_125x117.png differ diff --git a/assets/img/itwrx_logo.png b/assets/img/itwrx_logo.png new file mode 100644 index 0000000..391fb9e Binary files /dev/null and b/assets/img/itwrx_logo.png differ diff --git a/assets/img/itwrx_logo_250x250.png b/assets/img/itwrx_logo_250x250.png new file mode 100644 index 0000000..f1a4501 Binary files /dev/null and b/assets/img/itwrx_logo_250x250.png differ diff --git a/assets/img/keyhole.svg b/assets/img/keyhole.svg new file mode 100644 index 0000000..7dd2815 --- /dev/null +++ b/assets/img/keyhole.svg @@ -0,0 +1,208 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/keyhole_light.svg b/assets/img/keyhole_light.svg new file mode 100644 index 0000000..378acab --- /dev/null +++ b/assets/img/keyhole_light.svg @@ -0,0 +1,208 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/img/no_image_found.jpg b/assets/img/no_image_found.jpg new file mode 100644 index 0000000..1f552e5 Binary files /dev/null and b/assets/img/no_image_found.jpg differ diff --git a/assets/img/touch-icon.png b/assets/img/touch-icon.png new file mode 100644 index 0000000..7860c38 Binary files /dev/null and b/assets/img/touch-icon.png differ diff --git a/assets/js/jquery-3.7.1.min.js b/assets/js/jquery-3.7.1.min.js new file mode 100644 index 0000000..7f37b5d --- /dev/null +++ b/assets/js/jquery-3.7.1.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="
",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 01)return this.each(function(){a(this).lightSlider(b)}),this;var d={},e=a.extend(!0,{},c,b),f={},g=this;d.$el=this,"fade"===e.mode&&(e.vertical=!1);var h=g.children(),i=a(window).width(),j=null,k=null,l=0,m=0,n=!1,o=0,p="",q=0,r=e.vertical===!0?"height":"width",s=e.vertical===!0?"margin-bottom":"margin-right",t=0,u=0,v=0,w=0,x=null,y="ontouchstart"in document.documentElement,z={};return z.chbreakpoint=function(){if(i=a(window).width(),e.responsive.length){var b;if(e.autoWidth===!1&&(b=e.item),ie.responsive[0].breakpoint)for(var g in f)f.hasOwnProperty(g)&&(e[g]=f[g]);e.autoWidth===!1&&t>0&&v>0&&b!==e.item&&(q=Math.round(t/((v+e.slideMargin)*e.slideMove)))}},z.calSW=function(){e.autoWidth===!1&&(v=(o-(e.item*e.slideMargin-e.slideMargin))/e.item)},z.calWidth=function(a){var b=a===!0?p.find(".lslide").length:h.length;if(e.autoWidth===!1)m=b*(v+e.slideMargin);else{m=0;for(var c=0;b>c;c++)m+=parseInt(h.eq(c).width())+e.slideMargin}return m},d={doCss:function(){var a=function(){for(var a=["transition","MozTransition","WebkitTransition","OTransition","msTransition","KhtmlTransition"],b=document.documentElement,c=0;c'+e.prevHtml+''+e.nextHtml+""),e.autoWidth?z.calWidth(!1)
'),p=g.parent(".lSSlideWrapper"),e.rtl===!0&&p.parent().addClass("lSrtl"),e.vertical?(p.parent().addClass("vertical"),o=e.verticalHeight,p.css("height",o+"px")):o=g.outerWidth(),h.addClass("lslide"),e.loop===!0&&"slide"===e.mode&&(z.calSW(),z.clone=function(){if(z.calWidth(!0)>o){for(var b=0,c=0,d=0;d=o+e.slideMargin));d++);var f=e.autoWidth===!0?c:e.item;if(fh.length-1-g.find(".clone.right").length;j--)q--,h.eq(j).remove();for(var k=g.find(".clone.right").length;f>k;k++)g.find(".lslide").eq(k).clone().removeClass("lslide").addClass("clone right").appendTo(g),q++;for(var l=g.find(".lslide").length-g.find(".clone.left").length;l>g.find(".lslide").length-f;l--)g.find(".lslide").eq(l-1).clone().removeClass("lslide").addClass("clone left").prependTo(g);h=g.children()}else h.hasClass("clone")&&(g.find(".clone").remove(),a.move(g,0))},z.clone()),z.sSW=function(){l=h.length,e.rtl===!0&&e.vertical===!1&&(s="margin-left"),e.autoWidth===!1&&h.css(r,v+"px"),h.css(s,e.slideMargin+"px"),m=z.calWidth(!1),g.css(r,m+"px"),e.loop===!0&&"slide"===e.mode&&n===!1&&(q=g.find(".clone.left").length)},z.calL=function(){h=g.children(),l=h.length},this.doCss()&&p.addClass("usingCss"),z.calL(),"slide"===e.mode?(z.calSW(),z.sSW(),e.loop===!0&&(t=a.slideValue(),this.move(g,t)),e.vertical===!1&&this.setHeight(g,!1)):(this.setHeight(g,!0),g.addClass("lSFade"),this.doCss()||(h.fadeOut(0),h.eq(q).fadeIn(0))),e.loop===!0&&"slide"===e.mode?h.eq(q).addClass("active"):h.first().addClass("active")},pager:function(){var a=this;if(z.createPager=function(){w=(o-(e.thumbItem*e.thumbMargin-e.thumbMargin))/e.thumbItem;var b=p.find(".lslide"),c=p.find(".lslide").length,d=0,f="",h=0;for(d=0;c>d;d++){"slide"===e.mode&&(e.autoWidth?h+=(parseInt(b.eq(d).width())+e.slideMargin)*e.slideMove:h=d*(v+e.slideMargin)*e.slideMove);var i=b.eq(d*e.slideMove).attr("data-thumb");if(f+=e.gallery===!0?'
  • ':'
  • '+(d+1)+"
  • ","slide"===e.mode&&h>=m-o-e.slideMargin){d+=1;var j=2;e.autoWidth&&(f+='
  • '+(d+1)+"
  • ",j=1),j>d?(f=null,p.parent().addClass("noPager")):p.parent().removeClass("noPager");break}}var k=p.parent();k.find(".lSPager").html(f),e.gallery===!0&&(e.vertical===!0&&k.find(".lSPager").css("width",e.vThumbWidth+"px"),u=d*(e.thumbMargin+w)+.5,k.find(".lSPager").css({property:u+"px","transition-duration":e.speed+"ms"}),e.vertical===!0&&p.parent().css("padding-right",e.vThumbWidth+e.galleryMargin+"px"),k.find(".lSPager").css(r,u+"px"));var l=k.find(".lSPager").find("li");l.first().addClass("active"),l.on("click",function(){return e.loop===!0&&"slide"===e.mode?q+=l.index(this)-k.find(".lSPager").find("li.active").index():q=l.index(this),g.mode(!1),e.gallery===!0&&a.slideThumb(),!1})},e.pager){var b="lSpg";e.gallery&&(b="lSGallery"),p.after('
      ');var c=e.vertical?"margin-left":"margin-top";p.parent().find(".lSPager").css(c,e.galleryMargin+"px"),z.createPager()}setTimeout(function(){z.init()},0)},setHeight:function(a,b){var c=null,d=this;c=e.loop?a.children(".lslide ").first():a.children().first();var f=function(){var d=c.outerHeight(),e=0,f=d;b&&(d=0,e=100*f/o),a.css({height:d+"px","padding-bottom":e+"%"})};f(),c.find("img").length?c.find("img")[0].complete?(f(),x||d.auto()):c.find("img").on("load",function(){setTimeout(function(){f(),x||d.auto()},100)}):x||d.auto()},active:function(a,b){this.doCss()&&"fade"===e.mode&&p.addClass("on");var c=0;if(q*e.slideMove=d&&(c=f)),e.loop===!0&&"slide"===e.mode&&(c=b===!0?q-g.find(".clone.left").length:q*e.slideMove,b===!0&&(d=a.length,f=d-1,c+1===d?c=f:c+1>d&&(c=0))),this.doCss()||"fade"!==e.mode||b!==!1||a.eq(c).fadeIn(e.speed),a.eq(c).addClass("active")}else a.removeClass("active"),a.eq(a.length-1).addClass("active"),this.doCss()||"fade"!==e.mode||b!==!1||(a.fadeOut(e.speed),a.eq(c).fadeIn(e.speed))},move:function(a,b){e.rtl===!0&&(b=-b),this.doCss()?a.css(e.vertical===!0?{transform:"translate3d(0px, "+-b+"px, 0px)","-webkit-transform":"translate3d(0px, "+-b+"px, 0px)"}:{transform:"translate3d("+-b+"px, 0px, 0px)","-webkit-transform":"translate3d("+-b+"px, 0px, 0px)"}):e.vertical===!0?a.css("position","relative").animate({top:-b+"px"},e.speed,e.easing):a.css("position","relative").animate({left:-b+"px"},e.speed,e.easing);var c=p.parent().find(".lSPager").find("li");this.active(c,!0)},fade:function(){this.active(h,!1);var a=p.parent().find(".lSPager").find("li");this.active(a,!0)},slide:function(){var a=this;z.calSlide=function(){m>o&&(t=a.slideValue(),a.active(h,!1),t>m-o-e.slideMargin?t=m-o-e.slideMargin:0>t&&(t=0),a.move(g,t),e.loop===!0&&"slide"===e.mode&&(q>=l-g.find(".clone.left").length/e.slideMove&&a.resetSlide(g.find(".clone.left").length),0===q&&a.resetSlide(p.find(".lslide").length)))},z.calSlide()},resetSlide:function(a){var b=this;p.find(".lSAction a").addClass("disabled"),setTimeout(function(){q=a,p.css("transition-duration","0ms"),t=b.slideValue(),b.active(h,!1),d.move(g,t),setTimeout(function(){p.css("transition-duration",e.speed+"ms"),p.find(".lSAction a").removeClass("disabled")},50)},e.speed+100)},slideValue:function(){var a=0;if(e.autoWidth===!1)a=q*(v+e.slideMargin)*e.slideMove;else{a=0;for(var b=0;q>b;b++)a+=parseInt(h.eq(b).width())+e.slideMargin}return a},slideThumb:function(){var a;switch(e.currentPagerPosition){case"left":a=0;break;case"middle":a=o/2-w/2;break;case"right":a=o-w}var b=q-g.find(".clone.left").length,c=p.parent().find(".lSPager");"slide"===e.mode&&e.loop===!0&&(b>=c.children().length?b=0:0>b&&(b=c.children().length));var d=b*(w+e.thumbMargin)-a;d+o>u&&(d=u-o-e.thumbMargin),0>d&&(d=0),this.move(c,d)},auto:function(){e.auto&&(clearInterval(x),x=setInterval(function(){g.goToNextSlide()},e.pause))},pauseOnHover:function(){var b=this;e.auto&&e.pauseOnHover&&(p.on("mouseenter",function(){a(this).addClass("ls-hover"),g.pause(),e.auto=!0}),p.on("mouseleave",function(){a(this).removeClass("ls-hover"),p.find(".lightSlider").hasClass("lsGrabbing")||b.auto()}))},touchMove:function(a,b){if(p.css("transition-duration","0ms"),"slide"===e.mode){var c=a-b,d=t-c;if(d>=m-o-e.slideMargin)if(e.freeMove===!1)d=m-o-e.slideMargin;else{var f=m-o-e.slideMargin;d=f+(d-f)/5}else 0>d&&(e.freeMove===!1?d=0:d/=5);this.move(g,d)}},touchEnd:function(a){if(p.css("transition-duration",e.speed+"ms"),"slide"===e.mode){var b=!1,c=!0;t-=a,t>m-o-e.slideMargin?(t=m-o-e.slideMargin,e.autoWidth===!1&&(b=!0)):0>t&&(t=0);var d=function(a){var c=0;if(b||a&&(c=1),e.autoWidth)for(var d=0,f=0;f=t));f++);else{var g=t/((v+e.slideMargin)*e.slideMove);q=parseInt(g)+c,t>=m-o-e.slideMargin&&g%1!==0&&q++}};a>=e.swipeThreshold?(d(!1),c=!1):a<=-e.swipeThreshold&&(d(!0),c=!1),g.mode(c),this.slideThumb()}else a>=e.swipeThreshold?g.goToPrevSlide():a<=-e.swipeThreshold&&g.goToNextSlide()},enableDrag:function(){var b=this;if(!y){var c=0,d=0,f=!1;p.find(".lightSlider").addClass("lsGrab"),p.on("mousedown",function(b){return o>m&&0!==m?!1:void("lSPrev"!==a(b.target).attr("class")&&"lSNext"!==a(b.target).attr("class")&&(c=e.vertical===!0?b.pageY:b.pageX,f=!0,b.preventDefault?b.preventDefault():b.returnValue=!1,p.scrollLeft+=1,p.scrollLeft-=1,p.find(".lightSlider").removeClass("lsGrab").addClass("lsGrabbing"),clearInterval(x)))}),a(window).on("mousemove",function(a){f&&(d=e.vertical===!0?a.pageY:a.pageX,b.touchMove(d,c))}),a(window).on("mouseup",function(g){if(f){p.find(".lightSlider").removeClass("lsGrabbing").addClass("lsGrab"),f=!1,d=e.vertical===!0?g.pageY:g.pageX;var h=d-c;Math.abs(h)>=e.swipeThreshold&&a(window).on("click.ls",function(b){b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopImmediatePropagation(),b.stopPropagation(),a(window).off("click.ls")}),b.touchEnd(h)}})}},enableTouch:function(){var a=this;if(y){var b={},c={};p.on("touchstart",function(a){c=a.originalEvent.targetTouches[0],b.pageX=a.originalEvent.targetTouches[0].pageX,b.pageY=a.originalEvent.targetTouches[0].pageY,clearInterval(x)}),p.on("touchmove",function(d){if(o>m&&0!==m)return!1;var f=d.originalEvent;c=f.targetTouches[0];var g=Math.abs(c.pageX-b.pageX),h=Math.abs(c.pageY-b.pageY);e.vertical===!0?(3*h>g&&d.preventDefault(),a.touchMove(c.pageY,b.pageY)):(3*g>h&&d.preventDefault(),a.touchMove(c.pageX,b.pageX))}),p.on("touchend",function(){if(o>m&&0!==m)return!1;var d;d=e.vertical===!0?c.pageY-b.pageY:c.pageX-b.pageX,a.touchEnd(d)})}},build:function(){var b=this;b.initialStyle(),this.doCss()&&(e.enableTouch===!0&&b.enableTouch(),e.enableDrag===!0&&b.enableDrag()),a(window).on("focus",function(){b.auto()}),a(window).on("blur",function(){clearInterval(x)}),b.pager(),b.pauseOnHover(),b.controls(),b.keyPress()}},d.build(),z.init=function(){z.chbreakpoint(),e.vertical===!0?(o=e.item>1?e.verticalHeight:h.outerHeight(),p.css("height",o+"px")):o=p.outerWidth(),e.loop===!0&&"slide"===e.mode&&z.clone(),z.calL(),"slide"===e.mode&&g.removeClass("lSSlide"),"slide"===e.mode&&(z.calSW(),z.sSW()),setTimeout(function(){"slide"===e.mode&&g.addClass("lSSlide")},1e3),e.pager&&z.createPager(),e.adaptiveHeight===!0&&e.vertical===!1&&g.css("height",h.eq(q).outerHeight(!0)),e.adaptiveHeight===!1&&("slide"===e.mode?e.vertical===!1?d.setHeight(g,!1):d.auto():d.setHeight(g,!0)),e.gallery===!0&&d.slideThumb(),"slide"===e.mode&&d.slide(),e.autoWidth===!1?h.length<=e.item?p.find(".lSAction").hide():p.find(".lSAction").show():z.calWidth(!1)0)e.onBeforePrevSlide.call(this,g,q),q--,g.mode(!1),e.gallery===!0&&d.slideThumb();else if(e.loop===!0){if(e.onBeforePrevSlide.call(this,g,q),"fade"===e.mode){var a=l-1;q=parseInt(a/e.slideMove)}g.mode(!1),e.gallery===!0&&d.slideThumb()}else e.slideEndAnimation===!0&&(g.addClass("leftEnd"),setTimeout(function(){g.removeClass("leftEnd")},400))},g.goToNextSlide=function(){var a=!0;if("slide"===e.mode){var b=d.slideValue();a=b=q?b+(q-c):q>=b+c?q-b-c:q-c}return a+1},g.getTotalSlideCount=function(){return p.find(".lslide").length},g.goToSlide=function(a){q=e.loop?a+g.find(".clone.left").length-1:a,g.mode(!1),e.gallery===!0&&d.slideThumb()},g.destroy=function(){g.lightSlider&&(g.goToPrevSlide=function(){},g.goToNextSlide=function(){},g.mode=function(){},g.play=function(){},g.pause=function(){},g.refresh=function(){},g.getCurrentSlideCount=function(){},g.getTotalSlideCount=function(){},g.goToSlide=function(){},g.lightSlider=null,z={init:function(){}},g.parent().parent().find(".lSAction, .lSPager").remove(),g.removeClass("lightSlider lSFade lSSlide lsGrab lsGrabbing leftEnd right").removeAttr("style").unwrap().unwrap(),g.children().removeAttr("style"),h.removeClass("lslide active"),g.find(".clone").remove(),h=null,x=null,n=!1,q=0)},setTimeout(function(){e.onSliderLoad.call(this,g)},10),a(window).on("resize orientationchange",function(a){setTimeout(function(){a.preventDefault?a.preventDefault():a.returnValue=!1,z.init()},200)}),this}}(jQuery); \ No newline at end of file diff --git a/assets/js/lightSlider.min.js.gz b/assets/js/lightSlider.min.js.gz new file mode 100644 index 0000000..adae23f Binary files /dev/null and b/assets/js/lightSlider.min.js.gz differ diff --git a/assets/js/lightSlider_start.js b/assets/js/lightSlider_start.js new file mode 100644 index 0000000..7b964af --- /dev/null +++ b/assets/js/lightSlider_start.js @@ -0,0 +1,40 @@ + + $(document).ready(function() { + + var autoplaySlider = $('#lightSlider, #lightSlider2').lightSlider({ + item:1, + slideMove:1, + keyPress:true, + pager:false, + auto:true, + controls:false, + pause:6000, + mode:'fade', + loop:true, + easing: 'cubic-bezier(0.25, 0, 0.25, 1)', + speed:50, + responsive : [ + { + breakpoint:800, + settings: { + item:1, + slideMove:1, + slideMargin:6, + } + }, + { + breakpoint:800, + settings: { + item:1, + slideMove:1 + } + } + ] +}); + +$('#lightSlider, #lightSlider2').parent().on('mouseenter',function(){ autoplaySlider.pause(); }); +$('#lightSlider, #lightSlider2').parent().on('mouseleave',function(){ autoplaySlider.play(); }); +//$('#lightSlider2').parent().on('mouseenter',function(){ autoplaySlider2.pause(); }); +//$('#lightSlider2').parent().on('mouseleave',function(){ autoplaySlider2.play(); }); +$('#lightSlider, #lightSlider2').css("visibility", "visible"); }); +//$('#lightSlider2').css("visibility", "visible"); }); diff --git a/assets/js/pjax-0.2.8.min.js b/assets/js/pjax-0.2.8.min.js new file mode 100644 index 0000000..d1d5e15 --- /dev/null +++ b/assets/js/pjax-0.2.8.min.js @@ -0,0 +1 @@ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Pjax=f()}})(function(){var define,module,exports;return function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i]+>/gi;var htmlAttribsRegex=/\s?[a-z:]+(?:=['"][^'">]+['"])*/gi;var matches=html.match(htmlRegex);if(matches&&matches.length){matches=matches[0].match(htmlAttribsRegex);if(matches.length){matches.shift();matches.forEach(function(htmlAttrib){var attr=htmlAttrib.trim().split("=");if(attr.length===1){tmpEl.documentElement.setAttribute(attr[0],true)}else{tmpEl.documentElement.setAttribute(attr[0],attr[1].slice(1,-1))}})}}tmpEl.documentElement.innerHTML=html;this.log("load content",tmpEl.documentElement.attributes,tmpEl.documentElement.innerHTML.length);if(document.activeElement&&contains(document,this.options.selectors,document.activeElement)){try{document.activeElement.blur()}catch(e){}}this.switchSelectors(this.options.selectors,tmpEl,document,options)},abortRequest:require("./lib/abort-request"),doRequest:require("./lib/send-request"),handleResponse:require("./lib/proto/handle-response"),loadUrl:function(href,options){options=typeof options==="object"?extend({},this.options,options):clone(this.options);this.log("load href",href,options);this.abortRequest(this.request);trigger(document,"pjax:send",options);this.request=this.doRequest(href,options,this.handleResponse.bind(this))},afterAllSwitches:function(){var autofocusEl=Array.prototype.slice.call(document.querySelectorAll("[autofocus]")).pop();if(autofocusEl&&document.activeElement!==autofocusEl){autofocusEl.focus()}this.options.selectors.forEach(function(selector){forEachEls(document.querySelectorAll(selector),function(el){executeScripts(el)})});var state=this.state;if(state.options.history){if(!window.history.state){this.lastUid=this.maxUid=newUid();window.history.replaceState({url:window.location.href,title:document.title,uid:this.maxUid,scrollPos:[0,0]},document.title)}this.lastUid=this.maxUid=newUid();window.history.pushState({url:state.href,title:state.options.title,uid:this.maxUid,scrollPos:[0,0]},state.options.title,state.href)}this.forEachSelectors(function(el){this.parseDOM(el)},this);trigger(document,"pjax:complete pjax:success",state.options);if(typeof state.options.analytics==="function"){state.options.analytics()}if(state.options.history){var a=document.createElement("a");a.href=this.state.href;if(a.hash){var name=a.hash.slice(1);name=decodeURIComponent(name);var curtop=0;var target=document.getElementById(name)||document.getElementsByName(name)[0];if(target){if(target.offsetParent){do{curtop+=target.offsetTop;target=target.offsetParent}while(target)}}window.scrollTo(0,curtop)}else if(state.options.scrollTo!==false){if(state.options.scrollTo.length>1){window.scrollTo(state.options.scrollTo[0],state.options.scrollTo[1])}else{window.scrollTo(0,state.options.scrollTo)}}}else if(state.options.scrollRestoration&&state.options.scrollPos){window.scrollTo(state.options.scrollPos[0],state.options.scrollPos[1])}this.state={numPendingSwitches:0,href:null,options:null}}};Pjax.isSupported=require("./lib/is-supported");if(Pjax.isSupported()){module.exports=Pjax}else{var stupidPjax=noop;for(var key in Pjax.prototype){if(Pjax.prototype.hasOwnProperty(key)&&typeof Pjax.prototype[key]==="function"){stupidPjax[key]=noop}}module.exports=stupidPjax}},{"./lib/abort-request":2,"./lib/events/on":4,"./lib/events/trigger":5,"./lib/execute-scripts":6,"./lib/foreach-els":7,"./lib/foreach-selectors":8,"./lib/is-supported":9,"./lib/parse-options":10,"./lib/proto/attach-form":11,"./lib/proto/attach-link":12,"./lib/proto/handle-response":13,"./lib/proto/log":14,"./lib/proto/parse-element":15,"./lib/send-request":16,"./lib/switches":18,"./lib/switches-selectors":17,"./lib/uniqueid":19,"./lib/util/clone":20,"./lib/util/contains":21,"./lib/util/extend":22,"./lib/util/noop":23}],2:[function(require,module,exports){var noop=require("./util/noop");module.exports=function(request){if(request&&request.readyState<4){request.onreadystatechange=noop;request.abort()}}},{"./util/noop":23}],3:[function(require,module,exports){module.exports=function(el){var code=el.text||el.textContent||el.innerHTML||"";var src=el.src||"";var parent=el.parentNode||document.querySelector("head")||document.documentElement;var script=document.createElement("script");if(code.match("document.write")){if(console&&console.log){console.log("Script contains document.write. Can’t be executed correctly. Code skipped ",el)}return false}script.type="text/javascript";script.id=el.id;if(src!==""){script.src=src;script.async=false}if(code!==""){try{script.appendChild(document.createTextNode(code))}catch(e){script.text=code}}parent.appendChild(script);if((parent instanceof HTMLHeadElement||parent instanceof HTMLBodyElement)&&parent.contains(script)){parent.removeChild(script)}return true}},{}],4:[function(require,module,exports){var forEachEls=require("../foreach-els");module.exports=function(els,events,listener,useCapture){events=typeof events==="string"?events.split(" "):events;events.forEach(function(e){forEachEls(els,function(el){el.addEventListener(e,listener,useCapture)})})}},{"../foreach-els":7}],5:[function(require,module,exports){var forEachEls=require("../foreach-els");module.exports=function(els,events,opts){events=typeof events==="string"?events.split(" "):events;events.forEach(function(e){var event;event=document.createEvent("HTMLEvents");event.initEvent(e,true,true);event.eventName=e;if(opts){Object.keys(opts).forEach(function(key){event[key]=opts[key]})}forEachEls(els,function(el){var domFix=false;if(!el.parentNode&&el!==document&&el!==window){domFix=true;document.body.appendChild(el)}el.dispatchEvent(event);if(domFix){el.parentNode.removeChild(el)}})})}},{"../foreach-els":7}],6:[function(require,module,exports){var forEachEls=require("./foreach-els");var evalScript=require("./eval-script");module.exports=function(el){if(el.tagName.toLowerCase()==="script"){evalScript(el)}forEachEls(el.querySelectorAll("script"),function(script){if(!script.type||script.type.toLowerCase()==="text/javascript"){if(script.parentNode){script.parentNode.removeChild(script)}evalScript(script)}})}},{"./eval-script":3,"./foreach-els":7}],7:[function(require,module,exports){module.exports=function(els,fn,context){if(els instanceof HTMLCollection||els instanceof NodeList||els instanceof Array){return Array.prototype.forEach.call(els,fn,context)}return fn.call(context,els)}},{}],8:[function(require,module,exports){var forEachEls=require("./foreach-els");module.exports=function(selectors,cb,context,DOMcontext){DOMcontext=DOMcontext||document;selectors.forEach(function(selector){forEachEls(DOMcontext.querySelectorAll(selector),cb,context)})}},{"./foreach-els":7}],9:[function(require,module,exports){module.exports=function(){return window.history&&window.history.pushState&&window.history.replaceState&&!navigator.userAgent.match(/((iPod|iPhone|iPad).+\bOS\s+[1-4]\D|WebApps\/.+CFNetwork)/)}},{}],10:[function(require,module,exports){var defaultSwitches=require("./switches");module.exports=function(options){options=options||{};options.elements=options.elements||"a[href], form[action]";options.selectors=options.selectors||["title",".js-Pjax"];options.switches=options.switches||{};options.switchesOptions=options.switchesOptions||{};options.history=typeof options.history==="undefined"?true:options.history;options.analytics=typeof options.analytics==="function"||options.analytics===false?options.analytics:defaultAnalytics;options.scrollTo=typeof options.scrollTo==="undefined"?0:options.scrollTo;options.scrollRestoration=typeof options.scrollRestoration!=="undefined"?options.scrollRestoration:true;options.cacheBust=typeof options.cacheBust==="undefined"?true:options.cacheBust;options.debug=options.debug||false;options.timeout=options.timeout||0;options.currentUrlFullReload=typeof options.currentUrlFullReload==="undefined"?false:options.currentUrlFullReload;if(!options.switches.head){options.switches.head=defaultSwitches.switchElementsAlt}if(!options.switches.body){options.switches.body=defaultSwitches.switchElementsAlt}return options};function defaultAnalytics(){if(window._gaq){_gaq.push(["_trackPageview"])}if(window.ga){ga("send","pageview",{page:location.pathname,title:document.title})}}},{"./switches":18}],11:[function(require,module,exports){var on=require("../events/on");var clone=require("../util/clone");var attrState="data-pjax-state";var formAction=function(el,event){if(isDefaultPrevented(event)){return}var options=clone(this.options);options.requestOptions={requestUrl:el.getAttribute("action")||window.location.href,requestMethod:el.getAttribute("method")||"GET"};var virtLinkElement=document.createElement("a");virtLinkElement.setAttribute("href",options.requestOptions.requestUrl);var attrValue=checkIfShouldAbort(virtLinkElement,options);if(attrValue){el.setAttribute(attrState,attrValue);return}event.preventDefault();if(el.enctype==="multipart/form-data"){options.requestOptions.formData=new FormData(el)}else{options.requestOptions.requestParams=parseFormElements(el)}el.setAttribute(attrState,"submit");options.triggerElement=el;this.loadUrl(virtLinkElement.href,options)};function parseFormElements(el){var requestParams=[];var formElements=el.elements;for(var i=0;i1||event.metaKey||event.ctrlKey||event.shiftKey||event.altKey){return"modifier"}if(el.protocol!==window.location.protocol||el.host!==window.location.host){return"external"}if(el.hash&&el.href.replace(el.hash,"")===window.location.href.replace(location.hash,"")){return"anchor"}if(el.href===window.location.href.split("#")[0]+"#"){return"anchor-empty"}}var isDefaultPrevented=function(event){return event.defaultPrevented||event.returnValue===false};module.exports=function(el){var that=this;el.setAttribute(attrState,"");on(el,"click",function(event){linkAction.call(that,el,event)});on(el,"keyup",function(event){if(event.keyCode===13){linkAction.call(that,el,event)}}.bind(this))}},{"../events/on":4,"../util/clone":20}],13:[function(require,module,exports){var clone=require("../util/clone");var newUid=require("../uniqueid");var trigger=require("../events/trigger");module.exports=function(responseText,request,href,options){options=clone(options||this.options);options.request=request;if(responseText===false){trigger(document,"pjax:complete pjax:error",options);return}var currentState=window.history.state||{};window.history.replaceState({url:currentState.url||window.location.href,title:currentState.title||document.title,uid:currentState.uid||newUid(),scrollPos:[document.documentElement.scrollLeft||document.body.scrollLeft,document.documentElement.scrollTop||document.body.scrollTop]},document.title,window.location.href);var oldHref=href;if(request.responseURL){if(href!==request.responseURL){href=request.responseURL}}else if(request.getResponseHeader("X-PJAX-URL")){href=request.getResponseHeader("X-PJAX-URL")}else if(request.getResponseHeader("X-XHR-Redirected-To")){href=request.getResponseHeader("X-XHR-Redirected-To")}var a=document.createElement("a");a.href=oldHref;var oldHash=a.hash;a.href=href;if(oldHash&&!a.hash){a.hash=oldHash;href=a.href}this.state.href=href;this.state.options=options;try{this.loadContent(responseText,options)}catch(e){trigger(document,"pjax:error",options);if(!this.options.debug){if(console&&console.error){console.error("Pjax switch fail: ",e)}return this.latestChance(href)}else{throw e}}}},{"../events/trigger":5,"../uniqueid":19,"../util/clone":20}],14:[function(require,module,exports){module.exports=function(){if(this.options.debug&&console){if(typeof console.log==="function"){console.log.apply(console,arguments)}else if(console.log){console.log(arguments)}}}},{}],15:[function(require,module,exports){var attrState="data-pjax-state";module.exports=function(el){switch(el.tagName.toLowerCase()){case"a":if(!el.hasAttribute(attrState)){this.attachLink(el)}break;case"form":if(!el.hasAttribute(attrState)){this.attachForm(el)}break;default:throw"Pjax can only be applied on or
      submit"}}},{}],16:[function(require,module,exports){var updateQueryString=require("./util/update-query-string");module.exports=function(location,options,callback){options=options||{};var queryString;var requestOptions=options.requestOptions||{};var requestMethod=(requestOptions.requestMethod||"GET").toUpperCase();var requestParams=requestOptions.requestParams||null;var formData=requestOptions.formData||null;var requestPayload=null;var request=new XMLHttpRequest;var timeout=options.timeout||0;request.onreadystatechange=function(){if(request.readyState===4){if(request.status===200){callback(request.responseText,request,location,options)}else if(request.status!==0){callback(null,request,location,options)}}};request.onerror=function(e){console.log(e);callback(null,request,location,options)};request.ontimeout=function(){callback(null,request,location,options)};if(requestParams&&requestParams.length){queryString=requestParams.map(function(param){return param.name+"="+param.value}).join("&");switch(requestMethod){case"GET":location=location.split("?")[0];location+="?"+queryString;break;case"POST":requestPayload=queryString;break}}else if(formData){requestPayload=formData}if(options.cacheBust){location=updateQueryString(location,"t",Date.now())}request.open(requestMethod,location,true);request.timeout=timeout;request.setRequestHeader("X-Requested-With","XMLHttpRequest");request.setRequestHeader("X-PJAX","true");request.setRequestHeader("X-PJAX-Selectors",JSON.stringify(options.selectors));if(requestPayload&&requestMethod==="POST"&&!formData){request.setRequestHeader("Content-Type","application/x-www-form-urlencoded")}request.send(requestPayload);return request}},{"./util/update-query-string":24}],17:[function(require,module,exports){var forEachEls=require("./foreach-els");var defaultSwitches=require("./switches");module.exports=function(switches,switchesOptions,selectors,fromEl,toEl,options){var switchesQueue=[];selectors.forEach(function(selector){var newEls=fromEl.querySelectorAll(selector);var oldEls=toEl.querySelectorAll(selector);if(this.log){this.log("Pjax switch",selector,newEls,oldEls)}if(newEls.length!==oldEls.length){throw"DOM doesn’t look the same on new loaded page: ’"+selector+"’ - new "+newEls.length+", old "+oldEls.length}forEachEls(newEls,function(newEl,i){var oldEl=oldEls[i];if(this.log){this.log("newEl",newEl,"oldEl",oldEl)}var callback=switches[selector]?switches[selector].bind(this,oldEl,newEl,options,switchesOptions[selector]):defaultSwitches.outerHTML.bind(this,oldEl,newEl,options);switchesQueue.push(callback)},this)},this);this.state.numPendingSwitches=switchesQueue.length;switchesQueue.forEach(function(queuedSwitch){queuedSwitch()})}},{"./foreach-els":7,"./switches":18}],18:[function(require,module,exports){var on=require("./events/on");module.exports={outerHTML:function(oldEl,newEl){oldEl.outerHTML=newEl.outerHTML;this.onSwitch()},innerHTML:function(oldEl,newEl){oldEl.innerHTML=newEl.innerHTML;if(newEl.className===""){oldEl.removeAttribute("class")}else{oldEl.className=newEl.className}this.onSwitch()},switchElementsAlt:function(oldEl,newEl){oldEl.innerHTML=newEl.innerHTML;if(newEl.hasAttributes()){var attrs=newEl.attributes;for(var i=0;i for details.]# + +import std/[strutils, cgi, strtabs, cookies, sysrand, base64] +import guildenstern/[httpserver], sqliteral +import "../models/session", "db", "global" + +#[type + Session* = object + sessionId*, csrfToken*: string + id*, userId*: int]# +type + User* = object + email*, password*: string + id*: int + +proc getSessionBySessionId*(sessionId: string): Session = + {.gcsafe.}: + var session: Session + for row in db1.rows(SelectSessionBySessionId, sessionId): + session.id = row.getInt(0) + session.sessionId = row.getString(1) + session.userId = row.getInt(2) + session.csrfToken = row.getString(3) + return session + +proc createUserSession*(userSession: Session) = + {.gcsafe.}: + db1.transaction: + discard db1.insert(InsertUserSession, userSession.sessionId, userSession.userId, userSession.csrfToken) + +proc deleteSession*(sessionId: string) = + {.gcsafe.}: + db1.transaction: + db1.exec(DeleteSessionBySessionId, sessionId) + +#Both form and auth have this template. The form.nim copy is exported. +template formInput(input: string): untyped = + readData(getBody()).getOrDefault(input) + +proc getSessionIdFromCookies*(): string = + {.gcsafe.}: + let cookieString = http.headers.getOrDefault("cookie") + let allCookies = parseCookies(cookieString) + if allCookies.hasKey(APP_NAME & "_session"): + return allCookies[APP_NAME & "_session"] + else: + return "" + +proc setVisitorCsrfToken*(): string = + {.gcsafe.}: + #create csrf token. + var csrfToken = $urandom(32) + csrfToken = base64.encode(csrfToken) + #delete old VisitorSessions, as they are just the one time use CSRF Tokens. + #may need to be redesigned for better multiuser robustness if it's deleting other users' unused tokens. + #deleteVisitorSessions() + #create session in db. + var visitorSession: Session + visitorSession.csrfToken = csrfToken + discard createVisitorSession(visitorSession) + return csrfToken + +proc getUserPageCsrfToken*(): string = + {.gcsafe.}: + var sessionId: string + var userSession: Session + sessionId = getSessionIdFromCookies() + userSession = getSessionBySessionId(sessionId) + return userSession.csrfToken + +proc newCsrfToken*(): string = + {.gcsafe.}: + #create csrf token. + var csrfToken = $urandom(32) + csrfToken = base64.encode(csrfToken) + return csrfToken + +#[proc fCsrfToken*(): string = + {.gcsafe.}: + let sessionId = getSessionIdFromCookies() + let userSession = getUserSessionBySessionId(sessionId) + result = userSession.csrfToken]# + +proc isValidVisitorCsrfToken*(csrfToken: string): bool = + #our previoulsy self-generated, valid csrfToken from the DB. + let visitorSessionCsrfToken = getSessionByCsrfToken(csrfToken).csrfToken + if visitorSessionCsrfToken.len > 0 and csrfToken == visitorSessionCsrfToken: + return true + else: + return false + +proc isValidUserCsrfToken*(csrfToken: string): bool = + var sessionId: string + var userSession: Session + sessionId = getSessionIdFromCookies() + userSession = getSessionBySessionId(sessionId) + if userSession.csrfToken == csrfToken: + return true + else: + return false + +proc isAuthdAdmin*(): bool = + {.gcsafe.}: + #get sessionId from request's cookie and see if it exists in session DB. + var sessionId: string + sessionId = getSessionIdFromCookies() + if sessionId.len() > 0: + var userSession: Session + try: + userSession = getSessionBySessionId(sessionId) + if userSession.id > 0: + return true + else: + return false + except Exception as e: + echo e.msg + return false + else: + return false + +#adds CSRF checking for POST requests. +proc isAuthdAdminPost*(): bool = + {.gcsafe.}: + #get sessionId from request's cookie and see if it exists in session DB. + #var sessionId{.threadvar.}: string + var sessionId: string + sessionId = getSessionIdFromCookies() + if sessionId.len() > 0: + try: + let userSession = getSessionBySessionId(sessionId) + if userSession.id > 0: + return true + else: + return false + except Exception as e: + echo e.msg + return false + else: + return false + +template ifAuthAdminPost*(procName: untyped): untyped = + if isAuthdAdminPost() == true: + procName + else: + reply(Http302, [location("/login")]) diff --git a/helpers/datetime.nim b/helpers/datetime.nim new file mode 100644 index 0000000..398d5b9 --- /dev/null +++ b/helpers/datetime.nim @@ -0,0 +1,152 @@ +import std/[times, strutils] + +proc weekdayFromString(dayStr: string): Weekday = + ## Convert a string representation of a weekday to Weekday enum + case dayStr + of "Monday": return dMon + of "Tuesday": return dTue + of "Wednesday": return dWed + of "Thursday": return dThu + of "Friday": return dFri + of "Saturday": return dSat + of "Sunday": return dSun + +proc monthFromString(monthStr: string): Month = + ## Convert a string representation of a weekday to Weekday enum + case monthStr + of "January": return mJan + of "Febuary": return mFeb + of "March": return mMar + of "April": return mApr + of "May": return mMay + of "June": return mJun + of "July": return mJul + of "August": return mAug + of "September": return mSep + of "October": return mOct + of "November": return mNov + of "December": return mDec + +proc nextWeekday*(targetWeekdayString: string): DateTime = + ## Calculates the next occurrence of a specific weekday + var nextDate = now() + # We're not including current day, so we move to the next day + nextDate = nextDate + 1.days + #convert passed weekday string to nim Weekday. + let targetWeekday = weekdayFromString(targetWeekdayString) + # Find the next occurrence of the target weekday + while nextDate.weekday != targetWeekday: + nextDate = nextDate + 1.days + return nextDate + +proc nthWeekdayInMonth*(year: int, month: Month, weekdayString: string, nth: range[1..3]): DateTime = + # Start from the first day of the month + #var currentDate = dateTime(year, month, 1, 0, 0, 0, 0) + var currentDate = dateTime(year, month, 1) + let weekday = weekdayFromString(weekdayString) + # Find the first occurrence of the specified weekday + while currentDate.weekday != weekday: + currentDate = currentDate + 1.days + # Move to the nth occurrence + currentDate = currentDate + days((nth - 1) * 7) + return currentDate + +proc lastWeekdayInMonth(year: int, month: Month, weekdayString: string): DateTime = + # Create a DateTime for the last day of the given month + #var lastDay = dateTime(year, month, getDaysInMonth(month, year), 0, 0, 0, 0) + var lastDay = dateTime(year, month, getDaysInMonth(month, year)) + let weekday = weekdayFromString(weekdayString) + # Work backwards until we find the last occurrence of the specified weekday + while lastDay.weekday != weekday: + lastDay = lastDay - 1.days + return lastDay + +proc nextMonthlyOnWeekdayOfWeek*(weekdayString: string, ocurrence: string): DateTime = + var nextSendDate: DateTime + let nextMonthsDate = now() + 1.months + let weekNumStrings = @["1", "2", "3"] + if ocurrence in weekNumStrings: + nextSendDate = nthWeekdayInMonth(year(nextMonthsDate), month(nextMonthsDate), weekdayString, parseInt(ocurrence)) + #when ocurrence == "last". + else: + nextSendDate = lastWeekdayInMonth(year(nextMonthsDate), month(nextMonthsDate), weekdayString) + #only use next month if that day has already occured this month, otherwise adjust it for this month instead. + if monthDay(nextSendDate - 1.months) > monthDay(now()): + if ocurrence in weekNumStrings: + nextSendDate = nthWeekdayInMonth(year(nextMonthsDate), month(now()), weekdayString, parseInt(ocurrence)) + else: + nextSendDate = lastWeekdayInMonth(year(nextMonthsDate), month(now()), weekdayString) + return nextSendDate + +proc nextYearlyOnWeekdayOfWeekOfMonth*(weekdayString, ocurrence, monthString: string): DateTime = + var nextSendDate, startingDate: DateTime + let nextYearsDate = now() + 1.years + #let startingDate = dateTime(year(nextYearsDate), monthFromString(monthString), 01, 0, 0, 0, 0) + startingDate = dateTime(year(nextYearsDate), monthFromString(monthString), 01) + let weekNumStrings = @["1", "2", "3"] + if ocurrence in weekNumStrings: + nextSendDate = nthWeekdayInMonth(year(startingDate), month(startingDate), weekdayString, parseInt(ocurrence)) + #when ocurrence == "last". + else: + nextSendDate = lastWeekdayInMonth(year(startingDate), month(startingDate), weekdayString) + #only use next year if that month and day has already occured this year, otherwise adjust it for this year instead. + if nextSendDate - 1.years > now(): + startingDate = dateTime(year(now()), monthFromString(monthString), 01) + let weekNumStrings = @["1", "2", "3"] + if ocurrence in weekNumStrings: + nextSendDate = nthWeekdayInMonth(year(startingDate), month(startingDate), weekdayString, parseInt(ocurrence)) + #when ocurrence == "last". + else: + nextSendDate = lastWeekdayInMonth(year(startingDate), month(startingDate), weekdayString) + return nextSendDate + +#[proc nextYearDate*(monthString: string, day: int): DateTime = + ## Aalways returns a date in the next year, + ## regardless of whether the target date has passed in the current year + #var nextSendDate = dateTime(year(now()) + 1, monthFromString(monthString), day, 0, 0, 0, 0) + #var nextSendDate = dateTime(year(now()) + 1, monthFromString(monthString), day) + #let nextSendDateString = + #echo nextSendDate + #return parse($nextSendDate, "yyyy-MM-dd") + var + let nextYear = year(now()) + 1 + let dt = dateTime(nextYear, monthFromString(monthString), day, 00, 00, 00, 00) + #echo dt + let nextSendDateString = format(dt, "yyyy-MM-dd") + #echo nextSendDateString + #let nextSendDateString = $nextYear & "-" & formattedMonthString & "-" & $day + let nextSendDate = parse(nextSendDateString, "yyyy-MM-dd") + #echo nextSendDateString + return nextSendDate]# + +proc nextYearlyDate*(monthString: string, targetDay: int): DateTime = + ## Calculates a date for the next occurrence of a specific month and day + ## + ## Parameters: + ## - baseDate: The starting date to calculate from + ## - targetMonth: The month (Month enum) for the target date + ## - targetDay: The day of month for the target date + ## + ## Returns the next occurrence of the specified month and day, which could be: + ## - Later this year if the target date hasn't occurred yet + ## - Next year if the target date has already passed this year + + let baseDate = now() + let targetMonth = monthFromString(monthString) + + # Get the current year + let currentYear = baseDate.year + + # Create a DateTime for the target date in the current year + var nextSendDate = dateTime(currentYear, targetMonth, targetDay, 00, 00, 00, 00) + + # If the target date has already passed this year, move to next year + if nextSendDate <= baseDate: + nextSendDate = dateTime(currentYear + 1, targetMonth, targetDay, 00, 00, 00, 00) + + let nextSendDateString = format(nextSendDate, "yyyy-MM-dd") + #echo nextSendDateString + #let nextSendDateString = $nextYear & "-" & formattedMonthString & "-" & $day + nextSendDate = parse(nextSendDateString, "yyyy-MM-dd") + + return nextSendDate diff --git a/helpers/db.nim b/helpers/db.nim new file mode 100644 index 0000000..f524a9a --- /dev/null +++ b/helpers/db.nim @@ -0,0 +1,35 @@ +#[Copyright 2024 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import sqliteral + +#### Db1 ### +const RemindersSchema* = "CREATE TABLE IF NOT EXISTS Reminders(id INTEGER PRIMARY KEY, title TEXT, message TEXT, notify_via TEXT, repeats INTEGER, repeat_freq TEXT, weekly_on TEXT, monthly_on_day INTEGER, monthly_on_weekday TEXT, monthly_on_week TEXT, yearly_on_month TEXT, yearly_on_day INTEGER, yearly_on_week TEXT, yearly_on_weekday TEXT, yearly_on_month2 TEXT, send_date TEXT, send_time_hr INTEGER, send_time_min INTEGER, send_time_am_pm TEXT)" +const UsersSchema* = "CREATE TABLE IF NOT EXISTS Users(id INTEGER PRIMARY KEY, email TEXT NOT NULL, password TEXT NOT NULL)" +const SessionsSchema* = "CREATE TABLE IF NOT EXISTS Sessions(id INTEGER PRIMARY KEY, session_id TEXT, user_id INTEGER, csrf_token TEXT NOT NULL)" + +type + Db1Sql* = enum + #Reminders + SelectAllReminders = "SELECT * FROM Reminders" + InsertReminder = """INSERT INTO Reminders (title, message, notify_via, repeats, repeat_freq, weekly_on, monthly_on_day, monthly_on_weekday, monthly_on_week, yearly_on_month, yearly_on_day, yearly_on_week, yearly_on_weekday, yearly_on_month2, send_date, send_time_hr, send_time_min, send_time_am_pm) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""" + UpdateReminder = """UPDATE Reminders SET title = ?, message = ?, notify_via = ?, repeats = ?, repeat_freq = ?, weekly_on = ?, monthly_on_day = ?, monthly_on_weekday = ?, monthly_on_week = ?, yearly_on_month = ?, yearly_on_day = ?, yearly_on_week = ?, yearly_on_weekday = ?, yearly_on_month2 = ?, send_date = ?, send_time_hr = ?, send_time_min = ?, send_time_am_pm = ? WHERE id = ?""" + UpdateReminderSendDate = """UPDATE Reminders SET send_date = ? WHERE id = ?""" + DeleteReminder = """DELETE FROM Reminders WHERE id = ?""" + #Users + SelectUsersByEmail = """SELECT * FROM Users WHERE email = ?""" + SelectUserById = """SELECT * FROM Users WHERE id = ? LIMIT 1""" + InsertUser = """INSERT INTO Users (email, password) VALUES (?,?)""" + #Sessions + SelectSessions = "SELECT * FROM Sessions" + SelectSessionBySessionId = "SELECT * FROM Sessions WHERE session_id = ?" + InsertUserSession = """INSERT INTO Sessions (session_id, user_id, csrf_token) VALUES (?,?,?)""" + DeleteSessionBySessionId = """DELETE FROM Sessions WHERE session_id = ?""" + DeleteSessions = """DELETE FROM Sessions""" + SelectSessionByCsrfToken = """SELECT * FROM Sessions WHERE csrf_token = ?""" + InsertVisitorSession = """INSERT INTO Sessions (csrf_token) VALUES (?)""" + +var + db1*: SQLiteral diff --git a/helpers/form.nim b/helpers/form.nim new file mode 100644 index 0000000..3d8c99a --- /dev/null +++ b/helpers/form.nim @@ -0,0 +1,211 @@ +#[Copyright 2025 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import std/[cgi, strtabs, strutils, cookies, uri, tables] +import jsony, guildenstern/httpserver, sqliteral +import "auth", "global" + +type + FormError* = object + fieldName*, fieldMessage*: string +type + FormOldInput* = object + fieldName*, fieldOldInput*: string +type + FormResult* = object + id*: int + message*, messageClass*, errors*, oldInputs*: string + +var formError*: FormError +var formErrors*: seq[FormError] +var formOldInput*: FormOldInput +var formOldInputs*: seq[FormOldInput] +var formResult*: FormResult + +#might still be in use by cookie FR type. too lazy to investigate now. +proc clearFormResult*() = + formResult = FormResult(message : "", + messageClass : "", + errors : "", + oldInputs : "") + +proc assignErrorFR*(formErrors: seq[FormError], formOldInputs: seq[FormOldInput]): FormResult = + {.gcsafe.}: + formResult.message = "Error(s) encountered while validating form input(s)." + formResult.messageClass = "form-error" + formResult.errors = toJson(formErrors) + formResult.oldInputs = toJson(formOldInputs) + +proc assignGeneralErrorFR*(errorMsg: string): FormResult = + {.gcsafe.}: + formResult.message = errorMsg + formResult.messageClass = "form-error" + +proc assignGeneralSuccessFR*(successMsg: string): FormResult = + {.gcsafe.}: + formResult.message = successMsg + formResult.messageClass = "form-success" + +proc assignLoginSuccessFR*(): FormResult = + {.gcsafe.}: + formResult.message = "You have logged in successfully." + formResult.messageClass = "form-success" + +proc assignCECreateSuccessFR*(): FormResult = + {.gcsafe.}: + formResult.message = "Content Entity successfully created." + formResult.messageClass = "form-success" + +proc assignCEEditSuccessFR*(): FormResult = + {.gcsafe.}: + formResult.message = "Content Entity successfully edited." + formResult.messageClass = "form-success" + +proc assignCEDeleteSuccessFR*(): FormResult = + {.gcsafe.}: + formResult.message = "Content Entity successfully deleted." + formResult.messageClass = "form-success" + +#using cookie to store login page formResult, because there's no session to use as formResult id yet. +proc getCookieFormResult*(): FormResult = + let cookieString = http.headers.getOrDefault("cookie") + let cookiesTable = parseCookies(cookieString) + var frJsonString: string + if cookiesTable.hasKey("form_result"): + frJsonString = cookiesTable["form_result"] + var formResult: FormResult + if frJsonString.len > 0: + formResult = frJsonString.fromJson(FormResult) + formErrors = @[] + clearFormResult() + if not formResult.message.len > 0: + formResult.id = 0 + formResult.message = "" + formResult.messageClass = "" + formResult.errors = "" + formResult.oldInputs = "" + return formResult + +#using strtabs for storing all other formResults. +proc getFormResult*(): FormResult = + let sessionId = getSessionIdFromCookies() + var frJsonString, strTabSessionId: string + if frStrTab.hasKey("sessionId"): + strTabSessionId = frStrTab["sessionId"] + if strTabSessionId == sessionId: + if frStrTab.hasKey("frJson"): + frJsonString = frStrTab["frJson"] + var newFormResult: FormResult + if frJsonString.len > 0: + newFormResult = frJsonString.fromJson(FormResult) + #reset formErrors seq variable used in form handlers. + formErrors = @[] + #clear the existing formResult so it won't show on next page's GET (without new POST). + clear(frStrTab, modeCaseSensitive) + if not newFormResult.message.len > 0: + newFormResult.id = 0 + newFormResult.message = "" + newFormResult.messageClass = "" + newFormResult.errors = "" + newFormResult.oldInputs = "" + return newFormResult + +proc setFR*() = + let sessionId = getSessionIdFromCookies() + let frJson = formResult.toJson() + frStrTab = {"sessionId": sessionId, "frJson": frJson}.newStringTable + +proc formInput*(input: string): string = + {.gcsafe.}: + if server.contenttype == Compact: + #readData:cgi, getBody:Guildenstern, getOrDefault:strtabs. + return readData(getBody()).getOrDefault(input) + else: + #getMPStringInput(input: string) + echo "not url-encoded" + +proc formInputAll*(): Table[string, string] = + {.gcsafe.}: + if server.contenttype == Compact: + #getBody:Guildenstern + let formDataStr = getBody() + var formData = initTable[string, string]() + # Use decodeQuery from the uri module + for (key, value) in decodeQuery(formDataStr): + formData[key] = value + return formData + else: + echo "not url-encoded" + +proc formInputInt*(input: string): int = + {.gcsafe.}: + let readInput = readData(getBody()).getOrDefault(input) + return parseIntIf(readInput) + +template formInputSeq*(input: string): seq[string] = + {.gcsafe.}: + readData(getBody()).getOrDefault(input) + +proc addFormError*(inputName: string, msgString: string) = + {.gcsafe.}: + formError.fieldName = inputName + formError.fieldMessage = msgString + formErrors.add(formError) + +proc addFormOldInput*(inputName: string, inputData: string) = + {.gcsafe.}: + formOldInput.fieldName = inputName + formOldInput.fieldOldInput = inputData + formOldInputs.add(formOldInput) + +#takes a fieldName and returns the fieldMessage. +proc fFieldMsg*(fr: FormResult, fieldName: string): string = + {.gcsafe.}: + if fr.errors.len() > 0: + let errors = fromJson(fr.errors, seq[FormError]) + for error in errors: + if error.fieldName == fieldName: + return error.fieldMessage + +#this may need to handle more than one message per form field at some point, but not for this app (yet). +proc fErrorMsg*(fr: FormResult, fieldName:string): string = + {.gcsafe.}: + if fFieldMsg(fr, fieldName).len > 0: + return """""" & fFieldMsg(fr, fieldName) & "
      " + +#takes a fieldName and returns the fieldOldInput. +proc fOldInput*(fr: FormResult, fieldName: string, defaultValue = ""): string = + {.gcsafe.}: + var fieldOldInput: string + if fr.oldInputs.len() > 0: + let oldInputs = fromJson(fr.oldInputs, seq[FormOldInput]) + for oldInput in oldInputs: + if oldInput.fieldName == fieldName: + fieldOldInput = oldInput.fieldOldInput + return fieldOldInput + else: + return defaultValue + +proc getOldInputJson*(): string = + {.gcsafe.}: + let postData = readData(getBody()) + #pairs is a strtabs iterator. + for key,value in pairs(postData): + if key.len() > 0: + if key != "password" and key != "csrf_token": + addFormOldInput(key, value) + return toJson(formOldInputs) + +#the empty string defaultValue makes it possible to optionally supply a value from the DB: as needed in edit forms. +#checked proc works on radios and checboxes. +proc checked*(fr: FormResult, groupName: string, targetValue: string, defaultValue = ""): string = + {.gcsafe.}: + if fOldInput(fr, groupName, defaultValue) == targetValue: + return "checked" + +proc selected*(fr: FormResult, selectName: string, targetValue: string, defaultValue = ""): string = + {.gcsafe.}: + if fOldInput(fr, selectName, defaultValue) == targetValue: + return "selected='selected'" diff --git a/helpers/global.nim b/helpers/global.nim new file mode 100644 index 0000000..505b9db --- /dev/null +++ b/helpers/global.nim @@ -0,0 +1,90 @@ +#import std/[times, logging] +import std/[times, strutils, re, uri, paths, random, strtabs] + +#universal +const APP_PATH* = "/var/www/forget-me-not-gs" +const ASSETS_PATH* = "/var/www/forget-me-not-gs/app/assets" +const APP_NAME* = "Forget-Me-Not" +const APP_MODE* = "dev" +#dev +const APP_URL* = "http://fmn-gs" +#const SITE_URL* = "http://" +const ASSETS_URL* = "http://assets.fmn-gs" +#prod +#const APP_URL* = "https://ssm.itwrx.org" +#const SITE_URL* = "https://itwrx.org" +#const ASSETS_URL* = "https://assets.itwrx.org" + +var frStrTab* = newStringTable() + +#Guildensterns logger is conflicting with my, evidently incorrect, usage of the std lib logger so i'll just write some lines to a file for now. +#var logger* = newFileLogger("errors.log") + +let dt = now() +let nowDT* = dt.format("M-d-YYYY h:mm:ss tt") + +proc writeLogLine*(errorMsg: string) = + {.gcsafe.}: + let logFile = open("errors.log", fmAppend) + defer: logFile.close() + logFile.writeLine(errorMsg) + +#template location*(slug: string, csrfToken: string, fr: FormResult): untyped = +template location*(slug: string): untyped = + "location: " & APP_URL & slug + +template locationBack*(): string = + "location: " & http.headers.getOrDefault("referer") + +template locationOrigin*(origin: string): untyped = + "location: " & origin + +proc filenameToSentence*(filename: string): string = + #remove file extension. + let filePath = Path filename + let filePathEnum = splitFile(filePath) + var name = filePathEnum[1].string + #replace dashes and underscores with spaces. + name = name.replace(re"_", " ") + name = name.replace(re"-", " ") + #strip numbers. + name = name.replace(re"[0-9]", "") + return name + +proc titleToSlug*(title: string): string = + #replace one or more spaces with dash + var dataString = title.replace(re" +", "-") + #replace anything that is not a letter, number or underscore with nothing. + dataString = dataString.replace(re"[^a-zA-Z0-9-]", "") + #convert to all lowercase. + dataString = dataString.toLowerAscii() + return dataString + +proc getIdFromURI*(uri: string): int = + #let parsedUri = parseUri(uri) + let pathSeq = parseUri(uri).path.split('/') + result = strutils.parseInt(pathSeq[2]) + +proc parseIntIf*(input: string): int = + if input.len > 0: + return parseInt(input) + else: + return 0 + +proc parseFloatIf*(input: string): float = + if input.len > 0: + return parseFloat(input) + else: + return 0.0 + +#not cryptographically secure. +proc rndStr20*(): string = + for _ in 0..20: + add(result, char(rand(int('A') .. int('z')))) + +proc boolToInt*(myBool: bool): int = + if myBool == true: + return 1 + else: + return 0 + diff --git a/helpers/reminder.nim b/helpers/reminder.nim new file mode 100644 index 0000000..2a7ac31 --- /dev/null +++ b/helpers/reminder.nim @@ -0,0 +1,90 @@ +import std/[times, osproc, strutils], smtp +import ../models/reminder, ../models/user, datetime + +proc setFutureSendDate(reminderId: int) = + var reminder: Reminder + reminder = getReminderById(reminderId) + var newSendDate: DateTime + case reminder.repeatFreq: + of "day": + newSendDate = now() + 1.days + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + of "week": + newSendDate = nextWeekday(reminder.weeklyOn) + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + of "month": + if reminder.monthlyOnDay > 0: + #create DateTime with current month and year and reminder.monthlyOnDay + newSendDate = dateTime(year(now()), month(now()), reminder.monthlyOnDay) + #add 1 month only if monthlyOnDay hasn't occured in current month yet. + if reminder.monthlyOnDay < monthDay(now()): + newSendDate = newSendDate + 1.months + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + else: + #monthly on week number and weekday. e.g. "third thursday of every month". + newSendDate = nextMonthlyOnWeekdayOfWeek($reminder.monthlyOnWeekday, $reminder.monthlyOnWeek) + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + of "year": + #yearly on month and day. + #string zeros (db artifacts) have a length of 1... + if reminder.yearlyOnMonth.len() > 1: + newSendDate = nextYearlyDate(reminder.yearlyOnMonth, reminder.yearlyOnDay) + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + #yearly on weekday of week of month. e.g. third thursday of november of each year. + else: + newSendDate = nextYearlyOnWeekdayOfWeekOfMonth($reminder.yearlyOnWeekday, $reminder.yearlyOnWeek, $reminder.yearlyOnMonth2) + reminder.sendDate = $format(newSendDate, "yyyy-MM-dd") + updateReminderSendDate(reminder) + else: + echo "Invalid value for reminder.repeatFreq in setFutureSendDate()" + clearAllReminders() + +proc sendEmail(reminderMsg: string) = + let userEmailAddress = getEmailByUserId("1") + let headers = @[("From", "mailer@itwrx.org")] + let msg = createMessage("Reminder from Forget-Me-Not", reminderMsg, @[userEmailAddress], mCc = @[""], otherHeaders = headers) + {.cast(raises: []).}: + let smtpConn = newSmtp(debug=false) + smtpConn.connect("email.itwrx.org", Port 587) + smtpConn.startTls() + smtpConn.auth("mailer@itwrx.org", ".AQ8u((xB(AgZh^a`jEJ~W~{0Eq?fd$") + #loop through email messages and send to reuse connection? + smtpConn.sendmail("mailer@itwrx.org", @[userEmailAddress], $msg) + #manually closing not necessary? + smtpConn.close() + +proc sendXMPP(reminderMsg: string) = + var output: string + var status: int + (output, status) = execCmdEx("xmppc -m message chat itwrx@sec-chat.itwrx.org \"" & reminderMsg & "\"") + if status != 0: + #log this later instead. + echo output + echo "error sending XMPP msg" + +#send reminders that haven't been sent yet and set new sendDate (if repeating). +proc sendReminders*() = + let nowDT = now() + let reminders = getAllReminders() + for reminder in reminders: + let sendDateDTString = $reminder.sendDate & " " & $reminder.sendTimeHr & ":" & $reminder.sendTimeMin & ":" & $reminder.sendTimeAmPm + #single digits for minutes, as db send_time_min is integer and won't use "00", which results in runtime parse error. + let sendDateDT = parse(sendDateDTString, "yyyy-M-d h:m:tt") + #if sendDate is not in the future, it hasn't been sent yet (we are changing sendDate to future date during each send) and needs to be sent. + if sendDateDT <= nowDT: + #send reminder notification + case reminder.notifyVia: + of "email": + sendEmail(reminder.message) + of "xmpp": + sendXMPP(reminder.message) + else: + sendXMPP(reminder.message) + sendEmail(reminder.message) + if reminder.repeats == 1: + setFutureSendDate(reminder.id) diff --git a/helpers/validation.nim b/helpers/validation.nim new file mode 100644 index 0000000..25e350d --- /dev/null +++ b/helpers/validation.nim @@ -0,0 +1,226 @@ +#[Copyright 2024 ITwrx. +This file is part of ITwrxorg-SiteUpdata. +ITwrxorg-SiteUpdata is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import std/[strutils, re, typetraits, times, tables] +import valido/[email, password] +import "form", "global" + +var msgString{.threadvar.}: string + +proc vSize(sizeName: string, sizeValue: int, inputName: string, inputData: string, inputType: string) = + case sizeName: + of "min": + #how we determine size depends on input type. + case inputType: + of "string": + if not (inputData.len >= sizeValue): + msgString = inputName & " must have a character count of at least " & $sizeValue & " ." + addFormError(inputName, msgString) + of "integer": + if not (parseIntIf(inputData) >= sizeValue): + msgString = inputName & " must have a value of at least " & $sizeValue & " ." + addFormError(inputName, msgString) + of "float": + if not (parseFloat(inputData) >= sizeValue.float): + msgString = inputName & " must have a value of at least " & $sizeValue & " ." + addFormError(inputName, msgString) + of "max": + #how we determine size depends on input type. + case inputType: + of "string": + if inputData.len > sizeValue: + msgString = inputName & " must have a character count that is no greater than " & $sizeValue & " ." + addFormError(inputName, msgString) + of "integer": + if parseIntIf(inputData) > sizeValue: + msgString = inputName & " must have a value that is no greater than " & $sizeValue & " ." + addFormError(inputName, msgString) + of "float": + if parseFloat(inputData) > sizeValue.float: + msgString = inputName & " must have a value that is no greater than " & $sizeValue & " ." + addFormError(inputName, msgString) + +proc vType(inputName: string, inputData: string, validators: seq[string]): string = + var inputType: string + if "integer" in validators: + try: + #don't try to parse empty string as int. + if inputData.len > 0: + discard parseIntIf(inputData) + inputType = "integer" + except: + msgString = inputName & " must be a whole number." + addFormError(inputName, msgString) + elif "float" in validators: + try: + discard parseFloat(inputData) + inputType = "float" + except: + msgString = inputName & " must be a floating point number; a number with a decimal point. i.e. '1.5'" + addFormError(inputName, msgString) + elif "boolean" in validators: + if (inputData == "true") or not (inputData == "false"): + msgString = inputName & " must be interpretable as a boolean. i.e. true, or false." + addFormError(inputName, msgString) + inputType = "boolean" + else: + inputType = "string" + return inputType + +proc vRegex(inputName: string, inputData: string, inputType: string, validators: seq[string]) = + #check for regex validators. + for v in validators: + if match(v, re"^(min):([0-9]+)$"): + #get size's value from the string. + let vNameSeq = v.split(':') + let sizeName = vNameSeq[0] + let sizeValue = parseIntIf(vNameSeq[1]) + vSize(sizeName, sizeValue, inputName, inputData, inputType) + if match(v, re"^(max):([0-9]+)$"): + let vNameSeq = v.split(':') + let sizeName = vNameSeq[0] + let sizeValue = parseIntIf(vNameSeq[1]) + vSize(sizeName, sizeValue, inputName, inputData, inputType) + #ex. matches: "required_without:parent_id" + if match(v, re"^(required_without):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField = vNameSeq[1] + var conditionFieldInputData{.threadvar.}: string + conditionFieldInputData = formInput(conditionField) + if conditionFieldInputData.len == 0: + if inputData.len == 0: + msgString = inputName & " is required when " & conditionField & " isn't set." + addFormError(inputName, msgString) + if match(v, re"^(must_unset_with):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField = vNameSeq[1] + var conditionFieldInputData: string + conditionFieldInputData = formInput(conditionField) + if conditionFieldInputData.len > 0: + if inputData.len > 0: + msgString = inputName & " must be empty if " & conditionField & " is not." + addFormError(inputName, msgString) + if match(v, re"^(required_with):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField = vNameSeq[1] + var conditionFieldInputData: string + conditionFieldInputData = formInput(conditionField) + if conditionFieldInputData.len > 0: + if inputData.len == 0: + msgString = inputName & " is also required when " & conditionField & " is set." + addFormError(inputName, msgString) + if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField1 = vNameSeq[1] + var conditionField1InputData: string + conditionField1InputData = formInput(conditionField1) + let conditionField2 = vNameSeq[3] + var conditionField2InputData: string + conditionField2InputData = formInput(conditionField2) + if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0: + if inputData.len == 0: + msgString = inputName & " is required when " & conditionField1 & " is set and " & conditionField2 & " isn't set." + addFormError(inputName, msgString) + if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+):(and_without):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField1 = vNameSeq[1] + var conditionField1InputData: string + conditionField1InputData = formInput(conditionField1) + let conditionField2 = vNameSeq[3] + var conditionField2InputData: string + conditionField2InputData = formInput(conditionField2) + let conditionField3 = vNameSeq[5] + var conditionField3InputData: string + conditionField3InputData = formInput(conditionField3) + if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0 and not conditionField3InputData.len > 0: + if inputData.len == 0: + msgString = inputName & " is required when " & conditionField1 & " is set and " & conditionField2 & " and " & conditionField3 & " aren't set." + addFormError(inputName, msgString) + if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField1 = vNameSeq[1] + var conditionField1InputData: string + conditionField1InputData = formInput(conditionField1) + let conditionField2 = vNameSeq[3] + if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2: + if inputData.len == 0: + msgString = inputName & " is required when " & conditionField1 & " equals " & conditionField2 + addFormError(inputName, msgString) + if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+):(without):([a-z_]+)$"): + let vNameSeq = v.split(':') + #skip 0 index, as that's just the name of the validator. + let conditionField1 = vNameSeq[1] + var conditionField1InputData: string + conditionField1InputData = formInput(conditionField1) + let conditionField2 = vNameSeq[3] + let conditionField3 = vNameSeq[5] + var conditionField3InputData: string + conditionField3InputData = formInput(conditionField3) + if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2 and conditionField3InputData.len == 0: + if inputData.len == 0: + msgString = inputName & " is required when " & conditionField1 & " equals " & conditionField2 & " and " & conditionField3 & " is not set." + addFormError(inputName, msgString) + +proc vStandard(inputName: string, inputData: string, inputType: string, validators: seq[string]) = + if "email" in validators: + if not isEmail(inputData): + msgString = inputName & " is not recognized as a valid email address." + addFormError(inputName, msgString) + if "min_complexity" in validators: + if not isStrongPassword(inputData): + msgString = inputName & " is not random/complex enough. Try to make your " & inputName & " more unpredictable by adding random numbers, random letter casing, unrelated words, special characters, etc." + addFormError(inputName, msgString) + +proc vHardcoded*(inputDataAll: Table[string, string], validators: seq[string]) = + ##Warning: this validator requires specific (hardcoded) input names to exist in the posted form data. + if "future_datetime" in validators: + let nowDT = now() + var inputDT: DateTime + let inputDateString = inputDataAll.getOrDefault("send_date", "") + let inputTimeHrString = inputDataAll.getOrDefault("send_time_hr", "") + let inputTimeMinString = inputDataAll.getOrDefault("send_time_min", "") + let inputTimeAmPmString = inputDataAll.getOrDefault("send_time_am_pm", "") + if inputDateString.len > 0 and inputTimeHrString.len > 0 and inputTimeMinString.len > 0 and inputTimeAmPmString.len > 0: + let inputDatetimeString = inputDateString & " " & inputTimeHrString & ":" & inputTimeMinString & ":" & inputTimeAmPmString + inputDT = parse(inputDatetimeString, "yyyy-M-d h:m:tt") + else: + inputDT = now() + if inputDT <= nowDT: + msgString = "send_date and send_time combined must be a DateTime in the future (compared to the DateTime at form submit)." + addFormError("send_date", msgString) + +proc vInput*(inputName: string, validators: seq[string]) = + var inputType: string + var inputData: string + var inputDataAll: Table[string, string] + inputData = formInput(inputName) + inputDataAll = formInputAll() + #check if 'required' is set and validate input if it exists. otherwise return error. + if "required" in validators: + if (inputData.len == 0): + msgString = inputName & " is required." + addFormError(inputName, msgString) + else: + #proceed with validation. + #get inputType. + inputType = vType(inputName, inputData, validators) + #check for other validators and run if they exist. + vStandard(inputName, inputData, inputType, validators) + vHardcoded(inputDataAll, validators) + vRegex(inputName, inputData, inputType, validators) + #required is not set, but input could still exist. validate it. + else: + #get inputType. + inputType = vType(inputName, inputData, validators) + #check for other validators and run if they exist. + vStandard(inputName, inputData, inputType, validators) + vHardcoded(inputDataAll, validators) + vRegex(inputName, inputData, inputType, validators) diff --git a/models/human_checker.nim b/models/human_checker.nim new file mode 100644 index 0000000..fab210c --- /dev/null +++ b/models/human_checker.nim @@ -0,0 +1,66 @@ +#[Copyright 2024 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import std/random +#import sqliteral, "../helpers/db" + +type + HumanChecker* = object + question*: string + id*, answer*: int + +var humanCheckers: seq[HumanChecker] +humanCheckers = @[ + HumanChecker(id: 1, question: "26 + four, minus 10", answer: 20), + HumanChecker(id: 2, question: "10 minus 2, + 14", answer: 22), + HumanChecker(id: 3, question: "15 + five, minus 3", answer: 17), + HumanChecker(id: 4, question: "9 + nine, minus 6", answer: 12), + HumanChecker(id: 5, question: "13 - three, plus 5", answer: 15), + HumanChecker(id: 6, question: "7 + six, plus one", answer: 14), + HumanChecker(id: 7, question: "22 + 8, - 2", answer: 28), + HumanChecker(id: 8, question: "4 - four, + ten", answer: 10), + HumanChecker(id: 9, question: "16 + four, minus three", answer: 17), + HumanChecker(id: 10, question: "twelve minus four, plus 8", answer: 16) + ] + +proc getHumanChecker*(): HumanChecker = + {.gcsafe.}: + randomize() + return sample(humancheckers) + +proc getHCById*(id: int): HumanChecker = + {.gcsafe.}: + for hc in humanCheckers: + if hc.id == id: + return hc + +#for some reason we are creating the humanCheckers here and then checking them from the DB, instead of one data location or the other, like we probably should have. +#the below was probably supposed to be a test, but then it just stayed like that. +#[proc getHumanChecker*(): HumanChecker = + {.gcsafe.}: + randomize() + var humanCheckers{.threadvar.}: seq[HumanChecker] + humanCheckers.add(HumanChecker(id: 1, question: "26 + four, minus 10", answer: 20)) + humanCheckers.add(HumanChecker(id: 2, question: "10 minus 2, + 14", answer: 22)) + humanCheckers.add(HumanChecker(id: 3, question: "15 + five, minus 3", answer: 17)) + humanCheckers.add(HumanChecker(id: 4, question: "9 + nine, minus 6", answer: 12)) + humanCheckers.add(HumanChecker(id: 5, question: "13 - three, plus 5", answer: 15)) + humanCheckers.add(HumanChecker(id: 6, question: "7 + six, plus one", answer: 14)) + humanCheckers.add(HumanChecker(id: 7, question: "22 + 8, - 2", answer: 28)) + humanCheckers.add(HumanChecker(id: 8, question: "4 - four, + ten", answer: 10)) + humanCheckers.add(HumanChecker(id: 9, question: "16 + four, minus three", answer: 17)) + humanCheckers.add(HumanChecker(id: 10, question: "twelve minus four, plus 8", answer: 16)) + return sample(humancheckers) + +proc getHCById*(id: int): HumanChecker = + {.gcsafe.}: + var hc {.threadvar.}: HumanChecker + #prepareDb2SQL() + for row in db2.rows(SelectHumanCheckerById, id): + hc.id = row.getInt(0) + hc.question = row.getString(1) + hc.answer = row.getInt(2) + return hc]# + diff --git a/models/reminder.nim b/models/reminder.nim new file mode 100644 index 0000000..3fe35e8 --- /dev/null +++ b/models/reminder.nim @@ -0,0 +1,52 @@ +#[Copyright 2024 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import sqliteral, "../helpers/db", "../helpers/global.nim", sequtils + +type + Reminder* = object + title*, message*, notifyVia*, repeatFreq*, sendDate*, sendTimeAmPm*, monthlyOnWeek*, yearlyOnWeek*, weeklyOn*, monthlyOnWeekday*, yearlyOnMonth*, yearlyOnWeekday*, yearlyOnMonth2*: string + id*, repeats*, monthlyOnDay*, yearlyOnDay*, sendTimeHr*, sendTimeMin*: int + +var allReminders: seq[Reminder] + +proc getAllReminders*(): seq[Reminder] = + {.gcsafe.}: + if allReminders.len == 0: + for row in db1.rows(SelectAllReminders): + allReminders.add(Reminder(id: row.getInt(0), title: row.getString(1), message: row.getString(2), notifyVia: row.getString(3), repeats: row.getInt(4), repeatFreq: row.getString(5), weeklyOn: row.getString(6), monthlyOnDay: row.getInt(7), monthlyOnWeekday: row.getString(8), monthlyOnWeek: row.getString(9), yearlyOnMonth: row.getString(10), yearlyOnDay: row.getInt(11), yearlyOnWeek: row.getString(12), yearlyOnWeekday: row.getString(13), yearlyOnMonth2: row.getString(14), sendDate: row.getString(15), sendTimeHr: row.getInt(16), sendTimeMin: row.getInt(17), sendTimeAmPm: row.getString(18))) + return allReminders + +proc clearAllReminders*() = + {.gcsafe.}: + allReminders.setLen(0) + +proc getReminderById*(id: int): Reminder = + {.gcsafe.}: + for reminder in getAllReminders(): + if reminder.id == id: + return reminder + +proc createReminder*(reminder: Reminder): int = + {.gcsafe.}: + var reminderId: int + db1.transaction: + reminderId = db1.insert(InsertReminder, reminder.title, reminder.message, reminder.notifyVia, reminder.repeats, reminder.repeatFreq, reminder.weeklyOn, reminder.monthlyOnDay, reminder.monthlyOnWeekday, reminder.monthlyOnWeek, reminder.yearlyOnMonth, reminder.yearlyOnDay, reminder.yearlyOnWeek, reminder.yearlyOnWeekday, reminder.yearlyOnMonth2, reminder.sendDate, reminder.sendTimeHr, reminder.sendTimeMin, reminder.sendTimeAmPm) + return reminderId + +proc updateReminder*(reminder: Reminder) = + {.gcsafe.}: + db1.transaction: + db1.exec(UpdateReminder, reminder.title, reminder.message, reminder.notifyVia, reminder.repeats, reminder.repeatFreq, reminder.weeklyOn, reminder.monthlyOnDay, reminder.monthlyOnWeekday, reminder.monthlyOnWeek, reminder.yearlyOnMonth, reminder.yearlyOnDay, reminder.yearlyOnWeek, reminder.yearlyOnWeekday, reminder.yearlyOnMonth2, reminder.sendDate, reminder.sendTimeHr, reminder.sendTimeMin, reminder.sendTimeAmPm, reminder.id) + +proc updateReminderSendDate*(reminder: Reminder) = + {.gcsafe.}: + db1.transaction: + db1.exec(UpdateReminderSendDate, reminder.sendDate, reminder.id) + +proc deleteReminder*(id: int) = + {.gcsafe.}: + db1.transaction: + db1.exec(DeleteReminder, id) diff --git a/models/session.nim b/models/session.nim new file mode 100644 index 0000000..4f8c06f --- /dev/null +++ b/models/session.nim @@ -0,0 +1,33 @@ +#[Copyright 2024 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import sqliteral, "../helpers/db" + +type + Session* = object + sessionId*, csrfToken*: string + id*, userId*: int + +proc deleteSessions*() = + {.gcsafe.}: + db1.transaction: + db1.exec(DeleteSessions) + +proc getSessionByCsrfToken*(csrfToken: string): Session = + {.gcsafe.}: + var session: Session + for row in db1.rows(SelectSessionByCsrfToken, csrfToken): + session.id = row.getInt(0) + session.sessionId = row.getString(1) + session.userId = row.getInt(2) + session.csrfToken = row.getString(3) + return session + +proc createVisitorSession*(visitorSession: Session): int = + {.gcsafe.}: + var visitorSessionId: int + db1.transaction: + visitorSessionId = db1.insert(InsertVisitorSession, visitorSession.csrfToken) + return visitorSessionId diff --git a/models/user.nim b/models/user.nim new file mode 100644 index 0000000..5edd177 --- /dev/null +++ b/models/user.nim @@ -0,0 +1,33 @@ +#[Copyright 2025 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import sqliteral, "../helpers/db" + +type + User* = object + email*, password*: string + id*: int + +proc getUserByEmail*(email: string): User = + {.gcsafe.}: + var user: User + for row in db1.rows(SelectUsersByEmail, email): + user.id = row.getInt(0) + user.email = row.getString(1) + user.password = row.getString(2) + return user + +proc getEmailByUserId*(id: string): string = + {.gcsafe.}: + var user: User + for row in db1.rows(SelectUserById, id): + user.id = row.getInt(0) + user.email = row.getString(1) + return user.email + +proc createUser*(email: string, encodedHash: string) = + {.gcsafe.}: + db1.transaction: + discard db1.insert(InsertUser, email, encodedHash) diff --git a/post_handlers/login_post_handler.nim b/post_handlers/login_post_handler.nim new file mode 100644 index 0000000..8500174 --- /dev/null +++ b/post_handlers/login_post_handler.nim @@ -0,0 +1,106 @@ +#[Copyright 2025 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import guildenstern/httpserver +import nimword, jsony +import std/[base64, sysrand] +import "../helpers/form", "../helpers/validation", "../helpers/auth", "../helpers/global", "../models/user", "../models/session" + +proc loginPostHandler*() = + try: + var cookieHeader1, cookieHeader2: string + let email = formInput("email") + let password = formInput("password") + #validate form inputs + vInput("email", @["required", "string", "email", "max:75"]) + vInput("password", @["required", "string", "min_complexity", "max:100"]) + var csrfTokenInput = formInput("csrf_token") + #create formResult and redirect on validation errors. + if formErrors.len > 0: + addFormOldInput("email", formInput("email")) + #discard assignErrorFR(formErrors, formOldInputs, csrfTokenInput) + discard assignErrorFR(formErrors, formOldInputs) + let frJson = formResult.toJson() + #let cookieHeader = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" + if APP_MODE == "dev": + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;" + else: + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;" + reply(Http303, [locationBack(), cookieHeader1]) + else: + #inputs pass validation rules. let's see if the creds supplied are valid. + let registeredUser = getUserByEmail(email) + if registeredUser.email.len > 0: + if password.isValidPassword(registeredUser.password): + #create sessionId + var sessionId = $urandom(32) + sessionId = base64.encode(sessionId) + #create session + var userSession: Session + userSession.sessionId = sessionId + userSession.userId = registeredUser.id + userSession.csrfToken = newCsrfToken() + createUserSession(userSession) + discard assignLoginSuccessFR() + let frJson = formResult.toJson() + #redirect, and set session cookie. + #will probably need to detect requested url for redirect (with static fallback) instead of just static location. + #Set-Cookie: =; Domain=; Secure; HttpOnly + #dev + #let cookieHeader1 = "Set-Cookie: " & APP_NAME & "_session=" & sessionId & ";" & "HttpOnly;" & "SameSite=Lax;" + #let cookieHeader2 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;" + #prod + #let cookieHeader1 = "Set-Cookie: " & APP_NAME & "_session=" & sessionId & ";" & "HttpOnly;" & "Secure=true;" & "SameSite=Lax;" + #let cookieHeader2 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;" + if APP_MODE == "dev": + cookieHeader1 = "Set-Cookie: " & APP_NAME & "_session=" & sessionId & ";" & "HttpOnly;" & "SameSite=Lax;" + cookieHeader2 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;" + else: + cookieHeader1 = "Set-Cookie: " & APP_NAME & "_session=" & sessionId & ";" & "HttpOnly;" & "Secure=true;" & "SameSite=Lax;" + cookieHeader2 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;" + #headers are just an openarray of strings. + reply(Http303, [location("/"), cookieHeader1, cookieHeader2]) + else: + #password supplied did not match password in DB for supplied email address. redirect back to login with creds error. + #create a formError. + formError.fieldName = "password" + formError.fieldMessage = "Entered password is not valid." + #add it to formErrors seq. + formErrors.add(formError) + formResult.message = "Authentication error." + formResult.messageClass = "text-red-500" + formResult.errors = toJson(formErrors) + formResult.oldInputs = getOldInputJson() + let frJson = formResult.toJson() + #let cookieHeader = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" + #let cookieHeader = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" + if APP_MODE == "dev": + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;" + else: + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;" + reply(Http303, [locationBack(), cookieHeader1]) + else: + formError.fieldName = "email" + formError.fieldMessage = "No user found with supplied email address." + #add it to formErrors seq. + formErrors.add(formError) + formResult.message = "Authentication error." + formResult.messageClass = "text-red-500" + formResult.errors = toJson(formErrors) + formResult.oldInputs = getOldInputJson() + let frJson = formResult.toJson() + #let cookieHeader = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" + if APP_MODE == "dev": + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;" + else: + cookieHeader1 = "Set-Cookie: form_result=" & frJson & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;" + reply(Http303, [locationBack(), cookieHeader1]) + + except Exception as e: + echo e.msg + #reply(Http500) + discard assignGeneralErrorFR(e.msg) + setFR() + reply(Http303, [location("/500")]) diff --git a/post_handlers/reminder_post_handler.nim b/post_handlers/reminder_post_handler.nim new file mode 100644 index 0000000..930ff3d --- /dev/null +++ b/post_handlers/reminder_post_handler.nim @@ -0,0 +1,196 @@ +#[Copyright 2024 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import guildenstern/httpserver, jsony +import "../helpers/form", "../helpers/global", "../helpers/validation", "../helpers/auth" +import "../models/reminder" + +proc reminderCreatePostHandler*() = + #try to save to DB. + try: + #attempt validation first. + vInput("title", @["required", "string", "max:150"]) + #vInput("subject", @["string", "max:175"]) + vInput("message", @["required", "string", "max:255"]) + vInput("notify_via", @["required", "string", "max:5"]) + vInput("repeats", @["required", "integer", "max:1"]) + vInput("repeat_freq", @["required_with:repeats", "string", "max:10"]) + vInput("weekly_on", @["required_when:repeat_freq:equals:week", "string", "max:10"]) + vInput("monthly_on_day", @["required_when:repeat_freq:equals:month:without:monthly_on_week", "integer", "max:31"]) + vInput("monthly_on_weekday", @["required_with:monthly_on_week", "string", "max:10"]) + vInput("monthly_on_week", @["required_when:repeat_freq:equals:month:without:monthly_on_day", "string", "max:4"]) + vInput("yearly_on_month", @["required_when:repeat_freq:equals:year:without:yearly_on_week", "string", "max:12"]) + vInput("yearly_on_day", @["required_with:yearly_on_month", "integer", "max:31"]) + vInput("yearly_on_week", @["required_when:repeat_freq:equals:year:without:yearly_on_month", "string", "max:4"]) + vInput("yearly_on_weekday", @["required_with:yearly_on_week", "string", "max:10"]) + vInput("yearly_on_month2", @["required_with:yearly_on_week", "string", "max:12"]) + vInput("send_date", @["required", "string", "max:10"]) + vInput("send_time_hr", @["required", "integer", "max:12"]) + vInput("send_time_min", @["required", "integer", "max:60"]) + vInput("send_time_am_pm", @["required", "string", "max:2"]) + #create formResult and redirect on validation errors. + if formErrors.len > 0: + addFormOldInput("title", formInput("title")) + #addFormOldInput("subject", formInput("subject")) + addFormOldInput("message", formInput("message")) + addFormOldInput("notify_via", formInput("notify_via")) + addFormOldInput("repeats", formInput("repeats")) + addFormOldInput("repeat_freq", formInput("repeat_freq")) + addFormOldInput("weekly_on", formInput("weekly_on")) + addFormOldInput("monthly_on_day", formInput("monthly_on_day")) + addFormOldInput("monthly_on_weekday", formInput("monthly_on_weekday")) + addFormOldInput("monthly_on_week", formInput("monthly_on_week")) + addFormOldInput("yearly_on_month", formInput("yearly_on_month")) + addFormOldInput("yearly_on_day", formInput("yearly_on_day")) + addFormOldInput("yearly_on_week", formInput("yearly_on_week")) + addFormOldInput("yearly_on_weekday", formInput("yearly_on_weekday")) + addFormOldInput("yearly_on_month2", formInput("yearly_on_month2")) + addFormOldInput("send_date", formInput("send_date")) + addFormOldInput("send_time_hr", formInput("send_time_hr")) + addFormOldInput("send_time_min", formInput("send_time_min")) + addFormOldInput("send_time_am_pm", formInput("send_time_am_pm")) + discard assignErrorFR(formErrors, formOldInputs) + setFR() + reply(Http303, [locationBack()]) + #validation passed. Save posted data to db. + else: + var reminder: Reminder + reminder.title = formInput("title") + #reminder.subject = formInput("subject") + reminder.message = formInput("message") + reminder.notifyVia = formInput("notify_via") + reminder.repeats = formInputInt("repeats") + reminder.repeatFreq = formInput("repeat_freq") + reminder.weeklyOn = formInput("weekly_on") + reminder.monthlyOnDay = formInputInt("monthly_on_day") + reminder.monthlyOnWeekday = formInput("monthly_on_weekday") + reminder.monthlyOnWeek = formInput("monthly_on_week") + reminder.yearlyOnMonth = formInput("yearly_on_month") + reminder.yearlyOnDay = formInputInt("yearly_on_day") + reminder.yearlyOnWeek = formInput("yearly_on_week") + reminder.yearlyOnWeekday = formInput("yearly_on_weekday") + reminder.yearlyOnMonth2 = formInput("yearly_on_month2") + reminder.sendDate = formInput("send_date") + reminder.sendTimeHr = formInputInt("send_time_hr") + reminder.sendTimeMin = formInputInt("send_time_min") + reminder.sendTimeAmPm = formInput("send_time_am_pm") + let reminderId = createReminder(reminder) + clearAllReminders() + discard assignCEEditSuccessFR() + setFR() + reply(Http303, [location("/")]) + except CatchableError as e: + echo e.msg + #reply(Http500) + discard assignGeneralErrorFR(e.msg) + setFR() + reply(Http303, [location("/500")]) + +proc reminderUpdatePostHandler*() = + try: + #var origin = http.headers.getOrDefault("origin") + #attempt validation first. + vInput("title", @["required", "string", "max:150"]) + #vInput("subject", @["string", "max:175"]) + vInput("message", @["required", "string", "max:255"]) + vInput("notify_via", @["required", "string", "max:5"]) + vInput("repeats", @["required", "integer", "max:1"]) + vInput("repeat_freq", @["required_with:repeats", "string", "max:10"]) + vInput("weekly_on", @["required_when:repeat_freq:equals:week", "string", "max:10"]) + vInput("monthly_on_day", @["required_when:repeat_freq:equals:month:without:monthly_on_week", "must_unset_with:monthly_on_week", "must_unset_with:monthly_on_weekday", "integer", "max:31"]) + vInput("monthly_on_weekday", @["required_with:monthly_on_week", "must_unset_with:monthly_on_day", "string", "max:10"]) + vInput("monthly_on_week", @["required_when:repeat_freq:equals:month:without:monthly_on_day", "must_unset_with:monthly_on_day", "string", "max:4"]) + vInput("yearly_on_month", @["required_when:repeat_freq:equals:year:without:yearly_on_week", "must_unset_with:yearly_on_week", "must_unset_with:yearly_on_weekday", "must_unset_with:yearly_on_month2", "string", "max:12"]) + vInput("yearly_on_day", @["required_with:yearly_on_month", "must_unset_with:yearly_on_week", "must_unset_with:yearly_on_weekday", "must_unset_with:yearly_on_month2", "integer", "max:31"]) + vInput("yearly_on_week", @["required_when:repeat_freq:equals:year:without:yearly_on_month", "must_unset_with:yearly_on_month", "must_unset_with:yearly_on_day", "string", "max:4"]) + vInput("yearly_on_weekday", @["required_with:yearly_on_week", "must_unset_with:yearly_on_month", "must_unset_with:yearly_on_day", "string", "max:10"]) + vInput("yearly_on_month2", @["required_with:yearly_on_week", "must_unset_with:yearly_on_month", "must_unset_with:yearly_on_day", "string", "max:12"]) + vInput("send_date", @["required", "string", "max:10", "future_datetime"]) + vInput("send_time_hr", @["required", "integer", "max:12"]) + vInput("send_time_min", @["required", "integer", "max:60"]) + vInput("send_time_am_pm", @["required", "string", "max:2"]) + #create formResult and redirect on validation errors. + if formErrors.len > 0: + #since validation failed we better keep add the old inputs. + addFormOldInput("title", formInput("title")) + #addFormOldInput("subject", formInput("subject")) + addFormOldInput("message", formInput("message")) + addFormOldInput("notify_via", formInput("notify_via")) + addFormOldInput("repeats", formInput("repeats")) + addFormOldInput("repeat_freq", formInput("repeat_freq")) + addFormOldInput("weekly_on", formInput("weekly_on")) + addFormOldInput("monthly_on_day", formInput("monthly_on_day")) + addFormOldInput("monthly_on_weekday", formInput("monthly_on_weekday")) + addFormOldInput("monthly_on_week", formInput("monthly_on_week")) + addFormOldInput("yearly_on_month", formInput("yearly_on_month")) + addFormOldInput("yearly_on_day", formInput("yearly_on_day")) + addFormOldInput("yearly_on_week", formInput("yearly_on_week")) + addFormOldInput("yearly_on_weekday", formInput("yearly_on_weekday")) + addFormOldInput("yearly_on_month2", formInput("yearly_on_month2")) + addFormOldInput("send_date", formInput("send_date")) + addFormOldInput("send_time_hr", formInput("send_time_hr")) + addFormOldInput("send_time_min", formInput("send_time_min")) + addFormOldInput("send_time_am_pm", formInput("send_time_am_pm")) + discard assignErrorFR(formErrors, formOldInputs) + setFR() + reply(Http303, [locationBack()]) + #validation passed. Save posted data to db and redirect back with success message. + else: + var reminder: Reminder + reminder.id = formInputInt("reminder_id") + reminder.title = formInput("title") + #reminder.subject = formInput("subject") + reminder.message = formInput("message") + reminder.notifyVia = formInput("notify_via") + reminder.repeats = formInputInt("repeats") + reminder.repeatFreq = formInput("repeat_freq") + reminder.weeklyOn = formInput("weekly_on") + reminder.monthlyOnDay = formInputInt("monthly_on_day") + reminder.monthlyOnWeekday = formInput("monthly_on_weekday") + reminder.monthlyOnWeek = formInput("monthly_on_week") + reminder.yearlyOnMonth = formInput("yearly_on_month") + reminder.yearlyOnDay = formInputInt("yearly_on_day") + reminder.yearlyOnWeek = formInput("yearly_on_week") + reminder.yearlyOnWeekday = formInput("yearly_on_weekday") + reminder.yearlyOnMonth2 = formInput("yearly_on_month2") + reminder.sendDate = formInput("send_date") + reminder.sendTimeHr = formInputInt("send_time_hr") + reminder.sendTimeMin = formInputInt("send_time_min") + reminder.sendTimeAmPm = formInput("send_time_am_pm") + updateReminder(reminder) + clearAllReminders() + discard assignCEEditSuccessFR() + setFR() + reply(Http303, [location("/")]) + except CatchableError as e: + echo e.msg + #reply(Http500) + discard assignGeneralErrorFR(e.msg) + setFR() + reply(Http303, [location("/500")]) + +proc reminderDeletePostHandler*() = + try: + vInput("reminder_id", @["integer", "max:200000"]) + #create formResult and redirect on validation errors. + if formErrors.len > 0: + #since validation failed we better add the old inputs. + discard assignErrorFR(formErrors, formOldInputs) + setFR() + reply(Http303, [locationBack()]) + #validation passed. Save posted data to db and redirect back with success message. + else: + let reminderId = formInputInt("reminder_id") + deleteReminder(reminderId) + clearAllReminders() + discard assignCEDeleteSuccessFR() + setFR() + reply(Http303, [location("/")]) + except CatchableError as e: + echo e.msg + #reply(Http500) + discard assignGeneralErrorFR(e.msg) + setFR() + reply(Http303, [location("/500")]) diff --git a/post_handlers/send_reminders_post_handler.nim b/post_handlers/send_reminders_post_handler.nim new file mode 100644 index 0000000..0ddf441 --- /dev/null +++ b/post_handlers/send_reminders_post_handler.nim @@ -0,0 +1,18 @@ +#[Copyright 2025 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import guildenstern/httpserver, "../helpers/reminder", "../helpers/form" + +proc sendRemindersPostHandler*() = + try: + if formInput("send_reminders_key") == "wpsU5CY1Tn5PkMjN6OBC7cdPVZbaW3x": + sendReminders() + reply(Http200) + else: + echo "'send_reminders_key' not valid" + reply(Http403) + except CatchableError as e: + echo e.msg + reply(Http500) diff --git a/post_handlers/user_session_post_handler.nim b/post_handlers/user_session_post_handler.nim new file mode 100644 index 0000000..2448e90 --- /dev/null +++ b/post_handlers/user_session_post_handler.nim @@ -0,0 +1,18 @@ +#[Copyright 2025 ITwrx. +This file is part of Simple Site Manager. +Simple Site Manager is released under the GNU Affero General Public License 3.0. +See COPYING or for details.]# + +import guildenstern/httpserver, "../models/user_session", "../helpers/form" + +proc userSessionDeletePostHandler*() = + try: + if formInput("delete_user_sessions_key") == "XR5yLeigb4PFXOZBh3PBOuQXc8d7NE6": + deleteUserSessions() + reply(Http200) + else: + echo "'delete_user_sessions_key' not valid" + reply(Http403) + except CatchableError as e: + echo e.msg + reply(Http500) diff --git a/templates/error403.nimf b/templates/error403.nimf new file mode 100644 index 0000000..60285b5 --- /dev/null +++ b/templates/error403.nimf @@ -0,0 +1,14 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc error403Template*(mode: string, csrfToken: string): string = +# result = "" +${mainTemplateTop("Error! Status Code: 403", "", mode, csrfToken)} +
      +

      Error! Status Code: 403


      +

      You are not authorized for the resource your browser requested.

      If you feel this is in error please report to web your host.

      Thanks!

      +
      +${mainTemplateBottom(mode)} +
      + + +#end proc diff --git a/templates/error404.nimf b/templates/error404.nimf new file mode 100644 index 0000000..1e2a4a0 --- /dev/null +++ b/templates/error404.nimf @@ -0,0 +1,14 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc error404Template*(csrfToken: string): string = +# result = "" +${mainTemplateTop("Error! Status Code: 404", csrfToken)} +
      +

      Error! Status Code: 404


      +

      The page or resource requested does not exist.

      Please try again, or report to site admin.

      Thanks!

      +
      +${mainTemplateBottom()} +
      + + +#end proc diff --git a/templates/error500.nimf b/templates/error500.nimf new file mode 100644 index 0000000..f02db16 --- /dev/null +++ b/templates/error500.nimf @@ -0,0 +1,17 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "main_template_top.nimf", "main_template_bottom.nimf", "../helpers/form" +#proc error500Template*(csrfToken: string, fr: FormResult): string = +# result = "" +${mainTemplateTop("Error! Status Code: 500", csrfToken)} +
      +

      Error! Status Code: 500


      + #if fr.message.len > 0: +

      Application Error Message: ${fr.message}

      + #end if +

      If you're seeing this, unfortunately, you've found a bug.

      Please report that you received a "500 page", including any Application Error Message above, to your web host.

      Thank You!

      +
      +${mainTemplateBottom()} +
      + + +#end proc diff --git a/templates/login.nimf b/templates/login.nimf new file mode 100644 index 0000000..54b90bb --- /dev/null +++ b/templates/login.nimf @@ -0,0 +1,45 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global", "../helpers/form" +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc loginTemplate*(csrfToken: string, fr: FormResult): string = +# result = "" +${mainTemplateTop("Log In", csrfToken)} +
      +
      +
       
      +
      +

      Log In

      + #if fr.message.len > 0: +

      ${fr.message}

      + #end if + + + * + #if fErrorMsg(fr, "email").len > 0: + ${fErrorMsg(fr, "email")} + #end if +
      +
      + * + #if fErrorMsg(fr, "password").len > 0: + ${fErrorMsg(fr, "password")} + #end if +
      +
      +
      +
      +
      +
      +
      + +
      +
      + +
      +
       
      +
      +
      + ${mainTemplateBottom()} + ##leave this even when empty. +
      +#end proc diff --git a/templates/main_template_bottom.nimf b/templates/main_template_bottom.nimf new file mode 100644 index 0000000..07ae039 --- /dev/null +++ b/templates/main_template_bottom.nimf @@ -0,0 +1,35 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global" +#proc mainTemplateBottom*(): string = +# result = "" + +
      + + +
      + + +
      + + +
      + + +#end proc diff --git a/templates/main_template_top.nimf b/templates/main_template_top.nimf new file mode 100644 index 0000000..15035b7 --- /dev/null +++ b/templates/main_template_top.nimf @@ -0,0 +1,32 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global", "../models/reminder", "../helpers/auth" +#proc mainTemplateTop*(title, csrfToken: string): string = +#let sections = getAllReminders() +# result = "" + + + + +${title} + + + + + +
      +
      +
      +
      +
      + + +
      +
      +
      +#end proc diff --git a/templates/reminder_create.nimf b/templates/reminder_create.nimf new file mode 100644 index 0000000..0fa39a0 --- /dev/null +++ b/templates/reminder_create.nimf @@ -0,0 +1,351 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global", "../helpers/form", "../helpers/auth", "../models/reminder" +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc reminderCreateTemplate*(csrfToken: string, fr: FormResult): string = +#let pageTitle = "Create a Reminder" +# result = "" + ${mainTemplateTop(pageTitle, csrfToken)} +

      ${pageTitle}

      + #if fr.message.len > 0: +

      ${fr.message}

      + #end if +
      + + +
      + +
      +
      + * + ${fErrorMsg(fr, "notify_via")} + + + +
      +
      + +
      +
      + ${fErrorMsg(fr, "send_time_hr")} + ${fErrorMsg(fr, "send_time_min")} + ${fErrorMsg(fr, "send_time_am_pm")} + *Send Time:  + + + +
      +
      + +
      + + + + +
      +
      + +
      +

      +
      + ${mainTemplateBottom()} +
      + +
      +#end proc diff --git a/templates/reminder_update.nimf b/templates/reminder_update.nimf new file mode 100644 index 0000000..9ee0378 --- /dev/null +++ b/templates/reminder_update.nimf @@ -0,0 +1,361 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global", "../helpers/form", "../helpers/auth", "../models/reminder" +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc reminderUpdateTemplate*(reminderId:int, csrfToken: string, fr: FormResult): string = +#let reminder = getReminderById(reminderId) +#let pageTitle = "Update Reminder: " & reminder.title +# result = "" + ${mainTemplateTop(pageTitle, csrfToken)} +

      ${pageTitle}

      +
      +
      + + + +
      +
      + #if fr.message.len > 0: +

      ${fr.message}

      + #end if +
      + + + +
      + +
      +
      + * + ${fErrorMsg(fr, "notify_via")} + + + +
      +
      + +
      +
      + ${fErrorMsg(fr, "send_time_hr")} + ${fErrorMsg(fr, "send_time_min")} + ${fErrorMsg(fr, "send_time_am_pm")} + *Send Time:  + + + +
      +
      + +
      + + + + +
      +
      + +
      +


      + +
      + +
      + ${mainTemplateBottom()} +#end proc diff --git a/templates/reminders.nimf b/templates/reminders.nimf new file mode 100644 index 0000000..6a3ff1c --- /dev/null +++ b/templates/reminders.nimf @@ -0,0 +1,44 @@ +#? stdtmpl(subsChar = '$', metaChar = '#') +#import "../helpers/global", "../helpers/form", "../helpers/auth", "../models/reminder" +#import "main_template_top.nimf", "main_template_bottom.nimf" +#proc remindersTemplate*(allReminders: seq[Reminder], csrfToken: string, fr: FormResult): string = +# result = "" + ${mainTemplateTop("Upcoming Reminders",csrfToken)} +
      + Upcoming Reminders+ New Reminder +
      + #if allReminders.len > 0: +
      +
      Title
      +
      Repeats?
      +
      Next Send Date
      +
      Actions
      +
      + #else: +

      You don't have any Reminders yet.

      + #end if + #for reminder in allReminders: +
      +
      ${reminder.title}
      +
      + #if reminder.repeats == 0: + No + #else: + Yes + #end if +
      +
      ${reminder.sendDate}
      +
      + View/Edit +
      + + + +
      +
      +
      + #end for + +
      + ${mainTemplateBottom()} +#end proc