initial commit

This commit is contained in:
itwrx 2025-05-15 08:01:35 -05:00
commit 74e9056c49
68 changed files with 2942 additions and 0 deletions

1
assets/css/fmn.css Normal file

File diff suppressed because one or more lines are too long

BIN
assets/img/AGPLv3_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
assets/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
assets/img/itwrx_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

208
assets/img/keyhole.svg Normal file
View File

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="keyhole_normal_green.svg"
id="svg5"
version="1.1"
viewBox="0 0 40.346382 87.21019"
height="87.21019mm"
width="40.346382mm">
<metadata
id="metadata1108">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:current-layer="layer1"
inkscape:window-maximized="1"
inkscape:window-y="44"
inkscape:window-x="1920"
inkscape:window-height="1012"
inkscape:window-width="1920"
inkscape:cy="180.40557"
inkscape:cx="65.575668"
inkscape:zoom="1.5037598"
showgrid="false"
inkscape:document-units="mm"
inkscape:pagecheckerboard="0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="namedview7" />
<defs
id="defs2">
<linearGradient
id="linearGradient28135"
inkscape:collect="always">
<stop
id="stop28131"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop28133"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<filter
height="1.017328"
width="1.0499071"
y="-0.0086593488"
x="-0.025077723"
id="filter81415-6"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417-2"
stdDeviation="0.036489637"
inkscape:collect="always" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="356.8486"
x2="104.32743"
y1="62.159691"
x1="113.69997"
id="linearGradient2561"
xlink:href="#linearGradient2559"
inkscape:collect="always" />
<linearGradient
id="linearGradient2559"
inkscape:collect="always">
<stop
id="stop2555"
offset="0"
style="stop-color:#56cc81;stop-opacity:1;" />
<stop
id="stop2557"
offset="1"
style="stop-color:#56cc81;stop-opacity:0;" />
</linearGradient>
<filter
height="1.017328"
width="1.0499071"
y="-0.0086593488"
x="-0.025077723"
id="filter81415"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417"
stdDeviation="0.036489637"
inkscape:collect="always" />
</filter>
<filter
height="1.025424"
width="1.060048"
y="-0.012704888"
x="-0.030088291"
id="filter81415-3"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417-6"
stdDeviation="0.99096184"
inkscape:collect="always" />
</filter>
<filter
height="1.0272352"
width="1.0646462"
y="-0.013616262"
x="-0.032338828"
id="filter11723"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur11725"
stdDeviation="1.2981296"
inkscape:collect="always" />
</filter>
<linearGradient
y2="276.0332"
x2="107.10352"
y1="102.14063"
x1="107.12215"
gradientUnits="userSpaceOnUse"
id="linearGradient1142"
xlink:href="#linearGradient28135"
inkscape:collect="always" />
<linearGradient
y2="282.74365"
x2="107.40199"
y1="169.82687"
x1="114.21398"
gradientUnits="userSpaceOnUse"
id="linearGradient1144"
xlink:href="#linearGradient28135"
inkscape:collect="always" />
</defs>
<g
transform="translate(-74.93443,-90.650693)"
style="display:inline"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1">
<path
transform="matrix(0.34522992,0,0,0.3341248,59.607026,85.258657)"
sodipodi:nodetypes="cscccc"
id="path27876-9"
d="M 84.190098,100.05333 C 49.578564,70.784965 64.606472,18.004271 109.12197,18.359631 c 38.28187,0.305598 57.58067,50.078639 21.70091,82.598339 l 27.85397,173.679 -104.270286,0.0977 z"
style="display:inline;fill:none;fill-opacity:1;stroke:#216946;stroke-width:4.26502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415-6)" />
<path
transform="matrix(0.34522992,0,0,0.3341248,59.607026,85.258657)"
sodipodi:nodetypes="cscccc"
id="path27876"
d="M 84.190098,100.05333 C 49.578564,70.784965 64.606472,18.004271 109.12197,18.359631 c 38.28187,0.305598 57.58067,50.078639 21.70091,82.598339 l 27.85397,173.679 -104.270286,0.0977 z"
style="display:inline;fill:#000000;fill-opacity:1;stroke:url(#linearGradient2561);stroke-width:4.26502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415)" />
<path
transform="matrix(0.28266679,0,0,0.32622246,66.382165,85.515643)"
sodipodi:nodetypes="cscccc"
id="path27876-7"
d="M 84.905522,99.784795 C 40.839877,70.293582 62.782194,21.871465 108.85394,22.189602 c 42.76153,0.295278 65.65005,46.870143 21.41087,78.384818 l 32.49342,175.09636 -113.212126,-0.40082 z"
style="display:inline;opacity:0.906018;fill:none;fill-opacity:1;stroke:url(#linearGradient1142);stroke-width:1.68433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415-3)" />
<path
transform="matrix(0.27350332,0,0,0.31564701,67.281971,86.316684)"
sodipodi:nodetypes="cscccc"
id="path27876-7-5"
d="M 87.54975,98.926416 C 44.070545,73.090418 62.089502,23.189352 108.16266,23.250803 c 44.73652,0.05967 63.85665,48.215787 20.47012,76.470328 -0.59806,1.421019 32.83204,177.678029 32.83204,177.678029 L 52.131872,277.22122 Z"
style="display:inline;opacity:0.847536;mix-blend-mode:normal;fill:none;fill-opacity:1;stroke:url(#linearGradient1144);stroke-width:0.690081;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter11723)" />
<ellipse
ry="10.204735"
rx="11.024756"
cy="102.94592"
cx="86.384682"
id="path2321"
style="display:inline;opacity:0;fill:#2e125a;fill-opacity:0.935433;stroke:#000000;stroke-width:0.850991;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccc"
id="path21434"
d="m 100.81531,118.89015 4.99583,47.77708 -9.211852,11.19365 -9.564218,-11.23175 5.461711,-47.71289 c 2.717143,1.00275 5.634532,1.01491 8.318529,-0.0261 z"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0913423px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
id="path21436"
d="m 96.581176,160.0613 0.149186,-40.3136"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0913423px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccc"
id="path21438"
d="m 87.03507,166.62913 9.546106,-6.56783 9.229964,6.60594"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0913423px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -0,0 +1,208 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="keyhole_light_green.svg"
id="svg5"
version="1.1"
viewBox="0 0 40.448196 87.43029"
height="87.43029mm"
width="40.448196mm">
<metadata
id="metadata31">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
inkscape:document-rotation="0"
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="44"
inkscape:window-x="1920"
inkscape:window-height="1012"
inkscape:window-width="1920"
inkscape:cy="180.55931"
inkscape:cx="65.808366"
inkscape:zoom="1.5037598"
showgrid="false"
inkscape:document-units="mm"
inkscape:pagecheckerboard="0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="namedview7" />
<defs
id="defs2">
<linearGradient
id="linearGradient28135"
inkscape:collect="always">
<stop
id="stop28131"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop28133"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<filter
height="1.017328"
width="1.0499071"
y="-0.0086593488"
x="-0.025077723"
id="filter81415-6-36"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417-2-7"
stdDeviation="0.036489637"
inkscape:collect="always" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="360.95953"
x2="104.69003"
y1="103.2221"
x1="107.53219"
id="linearGradient5083"
xlink:href="#linearGradient5081"
inkscape:collect="always" />
<linearGradient
id="linearGradient5081"
inkscape:collect="always">
<stop
id="stop5077"
offset="0"
style="stop-color:#6fffa3;stop-opacity:1;" />
<stop
id="stop5079"
offset="1"
style="stop-color:#6fffa3;stop-opacity:0;" />
</linearGradient>
<filter
height="1.017328"
width="1.0499071"
y="-0.0086593488"
x="-0.025077723"
id="filter81415-62"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417-91"
stdDeviation="0.036489637"
inkscape:collect="always" />
</filter>
<filter
height="1.025424"
width="1.060048"
y="-0.012704888"
x="-0.030088291"
id="filter81415-3-9"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur81417-6-36"
stdDeviation="0.99096184"
inkscape:collect="always" />
</filter>
<filter
height="1.0272352"
width="1.0646462"
y="-0.013616262"
x="-0.032338828"
id="filter11723-2"
style="color-interpolation-filters:sRGB"
inkscape:collect="always">
<feGaussianBlur
id="feGaussianBlur11725-6"
stdDeviation="1.2981296"
inkscape:collect="always" />
</filter>
<linearGradient
y2="276.0332"
x2="107.10352"
y1="102.14063"
x1="107.12215"
gradientUnits="userSpaceOnUse"
id="linearGradient900"
xlink:href="#linearGradient28135"
inkscape:collect="always" />
<linearGradient
y2="282.74365"
x2="107.40199"
y1="169.82687"
x1="114.21398"
gradientUnits="userSpaceOnUse"
id="linearGradient902"
xlink:href="#linearGradient28135"
inkscape:collect="always" />
</defs>
<g
transform="translate(-74.872859,-90.610012)"
style="display:inline;opacity:1"
inkscape:label="light_green_layer"
id="layer2"
inkscape:groupmode="layer">
<path
transform="matrix(0.34610112,0,0,0.33496798,59.506776,85.204369)"
sodipodi:nodetypes="cscccc"
id="path27876-9-1"
d="M 84.190098,100.05333 C 49.578564,70.784965 64.606472,18.004271 109.12197,18.359631 c 38.28187,0.305598 57.58067,50.078639 21.70091,82.598339 l 27.85397,173.679 -104.270286,0.0977 z"
style="display:inline;fill:none;fill-opacity:1;stroke:#499f68;stroke-width:4.26502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415-6-36)" />
<path
transform="matrix(0.34610112,0,0,0.33496798,59.506776,85.204369)"
sodipodi:nodetypes="cscccc"
id="path27876-87"
d="M 84.190098,100.05333 C 49.578564,70.784965 64.606472,18.004271 109.12197,18.359631 c 38.28187,0.305598 57.58067,50.078639 21.70091,82.598339 l 27.85397,173.679 -104.270286,0.0977 z"
style="display:inline;fill:#000000;fill-opacity:1;stroke:url(#linearGradient5083);stroke-width:4.26502;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415-62)" />
<path
transform="matrix(0.28338011,0,0,0.3270457,66.299011,85.462017)"
sodipodi:nodetypes="cscccc"
id="path27876-7-9"
d="M 84.905522,99.784795 C 40.839877,70.293582 62.782194,21.871465 108.85394,22.189602 c 42.76153,0.295278 65.65005,46.870143 21.41087,78.384818 l 32.49342,175.09636 -113.212126,-0.40082 z"
style="display:inline;opacity:0.906018;fill:none;fill-opacity:1;stroke:url(#linearGradient900);stroke-width:1.68433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter81415-3-9)" />
<path
transform="matrix(0.27419352,0,0,0.31644355,67.201088,86.265066)"
sodipodi:nodetypes="cscccc"
id="path27876-7-5-20"
d="M 87.54975,98.926416 C 44.070545,73.090418 62.089502,23.189352 108.16266,23.250803 c 44.73652,0.05967 63.85665,48.215787 20.47012,76.470328 -0.59806,1.421019 32.83204,177.678029 32.83204,177.678029 L 52.131872,277.22122 Z"
style="display:inline;opacity:0.847536;mix-blend-mode:normal;fill:none;fill-opacity:1;stroke:url(#linearGradient902);stroke-width:0.690081;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter11723-2)" />
<ellipse
ry="10.230486"
rx="11.052577"
cy="102.93627"
cx="86.352005"
id="path2321-2"
style="display:inline;opacity:0;fill:#2e125a;fill-opacity:0.935433;stroke:#000000;stroke-width:0.853138;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccccccc"
id="path21434-3"
d="m 100.81903,118.92076 5.00844,47.89763 -9.235098,11.22191 -9.588338,-11.2601 5.475479,-47.8333 c 2.723999,1.0053 5.648747,1.01748 8.339517,-0.0261 z"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0915728px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
sodipodi:nodetypes="cc"
id="path21436-7"
d="m 96.574214,160.1958 0.149559,-40.41532"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0915728px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
sodipodi:nodetypes="ccc"
id="path21438-5"
d="m 87.004034,166.7802 9.57018,-6.5844 9.253256,6.62259"
style="display:inline;opacity:0.680098;fill:none;stroke:none;stroke-width:0.0915728px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/img/touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

2
assets/js/jquery-3.7.1.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

4
assets/js/lightSlider.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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"); });

1
assets/js/pjax-0.2.8.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

91
fmn_gs.nim Normal file
View File

@ -0,0 +1,91 @@
#[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 <https://www.gnu.org/licenses/> for details.]#
import guildenstern/[epolldispatcher, httpserver], sqliteral
import "templates/error404.nimf", "templates/error500.nimf", "templates/reminders.nimf", "templates/reminder_create.nimf", "templates/reminder_update.nimf", "templates/login.nimf"
import helpers/db, helpers/global, helpers/form, helpers/auth
import models/reminder
import post_handlers/reminder_post_handler, post_handlers/login_post_handler, post_handlers/send_reminders_post_handler
db1.openDatabase(APP_NAME & ".db", [RemindersSchema, UsersSchema, SessionsSchema]);
var allReminders{.threadvar.}: seq[Reminder]
#uncomment, add creds to dev/create_user and compile+run once to add new admin user.
#import dev/create_user
#createNewUser()
proc handleGet() =
{.gcsafe.}:
try:
let uri = getUri()
#overwrite cookie in browser when reply is performed, so form won't keep showing old input to this visitor if they don't POST again and also refresh for some reason.
var cookieHeader: string
if APP_MODE == "dev":
cookieHeader = "Set-Cookie: form_result=" & "" & ";" & "HttpOnly;" & "path=/;" & "SameSite=Lax;"
else:
cookieHeader = "Set-Cookie: form_result=" & "" & ";" & "HttpOnly;" & "Secure=true;" & "path=/;" & "SameSite=Lax;"
if uri == "/login":
reply(loginTemplate(setVisitorCsrfToken(), getCookieFormResult()), [cookieHeader])
if isAuthdAdmin():
allReminders = getAllReminders()
for reminder in allReminders:
if "/reminder/" & $reminder.id & "/update" == uri:
reply(reminderUpdateTemplate(reminder.id, getUserPageCsrfToken(), getFormResult()), [cookieHeader])
case uri:
of "/":
reply(remindersTemplate(allReminders, getUserPageCsrfToken(), getFormResult()), [cookieHeader])
of "/create-reminder":
reply(reminderCreateTemplate(getUserPageCsrfToken(), getFormResult()), [cookieHeader])
of "/500":
reply(error500Template(getUserPageCsrfToken(), getFormResult()), [cookieHeader])
else:
reply(error404Template(getUserPageCsrfToken()), [cookieHeader])
else:
reply(Http302, [location("/login"), cookieHeader])
except Exception as e:
echo e.msg
proc handlePost() =
{.gcsafe.}:
try:
let uri = getUri()
if uri == "/send-reminders":
sendRemindersPostHandler()
let fr = getFormResult()
#let csrfTokenInput = formInput("csrf_token")
if uri == "/login":
if isValidVisitorCsrfToken(formInput("csrf_token")):
loginPostHandler()
else:
discard assignGeneralErrorFR("CSRF Token Validation Failure. Please try again (or report successive failures).")
setFR()
reply(Http302, [location("/login")])
else:
if isAuthdAdminPost():
if isValidUserCsrfToken(formInput("csrf_token")):
case uri
of "/reminder/create":
reminderCreatePostHandler()
of "/reminder/update":
reminderUpdatePostHandler()
of "/reminder/delete":
reminderDeletePostHandler()
else:
reply(error404Template(getUserPageCsrfToken()), ["Content-Type: text/html"])
else:
discard assignGeneralErrorFR("CSRF Token Validation Failure. Please try again (or report successive failures).")
setFR()
reply(Http302, [locationBack()])
else:
reply(Http302, [location("/login")])
except Exception as e:
echo e.msg
let getserver = newHttpServer(handleGet, headerfields = ["cookie", "cookie"])
let postserver = newHttpServer(handlePost, loglevel = INFO, headerfields = ["origin", "cookie", "referer"])
if not epolldispatcher.start(getserver, 8096): quit()
if not epolldispatcher.start(postserver, 8097, threadpoolsize = 20): quit()
joinThreads(getserver.thread, postserver.thread)

148
helpers/auth.nim Normal file
View File

@ -0,0 +1,148 @@
#[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 <https://www.gnu.org/licenses/> 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")])

152
helpers/datetime.nim Normal file
View File

@ -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

35
helpers/db.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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

211
helpers/form.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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 """<span class="text-red-500">""" & fFieldMsg(fr, fieldName) & "</span><br>"
#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'"

90
helpers/global.nim Normal file
View File

@ -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

90
helpers/reminder.nim Normal file
View File

@ -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)

226
helpers/validation.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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)

66
models/human_checker.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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]#

52
models/reminder.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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)

33
models/session.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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

33
models/user.nim Normal file
View File

@ -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 <https://www.gnu.org/licenses/> 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)

View File

@ -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 <https://www.gnu.org/licenses/> 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: <cookie-name>=<cookie-value>; Domain=<domain-value>; 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")])

View File

@ -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 <https://www.gnu.org/licenses/> 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")])

View File

@ -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 <https://www.gnu.org/licenses/> 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)

View File

@ -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 <https://www.gnu.org/licenses/> 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)

14
templates/error403.nimf Normal file
View File

@ -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)}
<div class="pjax-me">
<h1 class="mt-8 text-xl text-red-500">Error! Status Code: 403</h1><br>
<p>You are not authorized for the resource your browser requested.<br><br> If you feel this is in error please report to web your host.<br><br> Thanks!</p>
</div>
${mainTemplateBottom(mode)}
<div class="page-js"></div>
</body>
</html>
#end proc

14
templates/error404.nimf Normal file
View File

@ -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)}
<div class="pjax-me">
<h1 class="mt-8 text-xl text-red-500">Error! Status Code: 404</h1><br>
<p>The page or resource requested does not exist.<br><br> Please try again, or report to site admin.<br><br> Thanks!</p>
</div>
${mainTemplateBottom()}
<div class="page-js"></div>
</body>
</html>
#end proc

17
templates/error500.nimf Normal file
View File

@ -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)}
<div class="pjax-me">
<h1 class="mt-8 text-xl text-red-500">Error! Status Code: 500</h1><br>
#if fr.message.len > 0:
<h2 id="form_message" class="${fr.messageClass}">Application Error Message: ${fr.message}</h2>
#end if
<p>If you're seeing this, unfortunately, you've found a bug.<br><br> Please report that you received a "500 page", including any Application Error Message above, to your web host.<br><br> Thank You!</p>
</div>
${mainTemplateBottom()}
<div class="page-js"></div>
</body>
</html>
#end proc

45
templates/login.nimf Normal file
View File

@ -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)}
<div class="pjax-me">
<div class="flex flex-row">
<div class="basis-1/5">&nbsp;</div>
<div class="basis-3/5">
<h1 class="mt-4 text-2xl">Log In</h1>
#if fr.message.len > 0:
<h2 id="form_message" class="${fr.messageClass}">${fr.message}</h2>
#end if
<form method="POST" action="${APP_URL}/login" accept-charset="utf-8" class="ml-8">
<input type="hidden" name="csrf_token" value="${csrfToken}">
<span class="text-2xl text-red-500">*</span><label for="email" class="inline-block mt-8">Email Address:</label>
#if fErrorMsg(fr, "email").len > 0:
<span class="text-red-500">${fErrorMsg(fr, "email")}</span>
#end if
<br>
<input type="email" name="email" id="email" class="lg:w-2/5 bg-slate-400 text-slate-950" value="${fOldInput(fr, "email")}" required><br>
<span class="text-2xl text-red-500">*</span><label for="password" class="inline-block mt-8">Password:</label>
#if fErrorMsg(fr, "password").len > 0:
<span class="text-red-500">${fErrorMsg(fr, "password")}</span>
#end if
<br>
<input type="password" name="password" id="password" class="lg:w-2/5 bg-slate-400 text-slate-950 focus:ring-yellow-300" required><br>
<br>
<br>
<div class="lg:flex lg:flex-row">
<div class="lg:basis-2/3"></div>
<div class="lg:basis-1/3">
<button class="mt-4 p-1 font-semibold rounded-lg shadow-md text-white hover:text-white bg-green-800 hover:bg-green-700" type="submit">Login</button>
</div>
</div>
</form>
</div>
<div class="basis-1/5">&nbsp;</div>
</div>
</div>
${mainTemplateBottom()}
##leave this even when empty.
<div class="page-js"></div>
#end proc

View File

@ -0,0 +1,35 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
#import "../helpers/global"
#proc mainTemplateBottom*(): string =
# result = ""
</main>
<footer class="py-8 lg:mt-12">
<div class="lg:flex lg:flex-row">
<div class="lg:basis-1/3">
<div class="p-4 lg:py-2 lg:ml-4">
</div>
</div>
<div class="lg:basis-1/3">
</div>
<div class="lg:basis-1/3">
<div class="p-2 lg:ml-44">
<span>Built &amp; Hosted by: </span>
<a href="https://itwrx.org" target="_blank" rel="nofollow" class="ITwrx_link"><img src="${ASSETS_URL}/img/itwrx_logo.png" alt="ITwrx.org logo" width="50" height="50" class="inline -mt-3"></a>
</div>
</div>
</div>
</footer>
<!-- close the middle flex div. -->
</div>
<div class="lg:basis-1/12"></div>
<!-- close the flex-row -->
</div>
<div class="page-js">
</div>
</body>
</html>
#end proc

View File

@ -0,0 +1,32 @@
#? stdtmpl(subsChar = '$', metaChar = '#')
#import "../helpers/global", "../models/reminder", "../helpers/auth"
#proc mainTemplateTop*(title, csrfToken: string): string =
#let sections = getAllReminders()
# result = ""
<!DOCTYPE html>
<html lang="en" class="min-h-full">
<head>
<meta charset="utf-8">
<title>${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" >
<link rel="shortcut icon apple-touch-icon-precomposed" href="${ASSETS_URL}/img/touch-icon.png">
<link href="${ASSETS_URL}/css/fmn.css" rel="stylesheet">
</head>
<body class="main-bg min-h-full">
<div class="flex flex-row min-h-screen">
<div class="lg:basis-1/12"></div>
<div class="lg:basis-10/12 lg:flex lg:flex-col lg:max-w-[1600px]">
<header class="p-4">
<div class="lg:flex lg:flex-row">
<div class="lg:hidden home-link">
<a href="/" title="Click to go home" class="text-3xl text-indigo-300"><img src="${ASSETS_URL}/img/fmn_logo_125x117.png" alt="Forget-Me-Not logo." class="inline" width="125" height="117" />Forget-Me-Not</a>
</div>
<div class="hidden lg:block lg:basis-2/4 home-link">
<div class="mt-4 lg:mt-0">
<a href="/" title="Click to go home" class="text-3xl text-indigo-300"><img src="${ASSETS_URL}/img/fmn_logo_125x117.png" alt="Forget-Me-Not logo." class="inline" width="125" height="117" />Forget-Me-Not</a>
</div>
</div>
</div>
</header>
<main class="flex-1 py-8 px-12 pjax-me">
#end proc

View File

@ -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)}
<h1 class="text-xl">${pageTitle}</h1>
#if fr.message.len > 0:
<h2 id="form_message" class="${fr.messageClass}">${fr.message}</h2>
#end if
<form id="reminder-create" method="POST" action="${APP_URL}/reminder/create" accept-charset="utf-8">
<input type="hidden" name="csrf_token" value="${csrfToken}" form="reminder-create"/>
<label>${fErrorMsg(fr, "title")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="This will be used to generate your reminder's url and meta-title and is very important for SEO, so it should ideally start with your main search target phrase for this reminder, and not have more than one search target phrase, nor be any longer than necessary.">Title</span>:
<input type="text" id="title" name="title" value="${fOldInput(fr, "title")}" form="reminder-create" class="w-2/5" required>
</label>
<div class="mt-4">
<label>${fErrorMsg(fr, "message")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="This will be the body of the notification email, or the XMPP message.">Message</span>:<br>
<textarea name="message" form="reminder-create" rows="2" class="w-3/4" required>${fOldInput(fr, "message")}</textarea>
</label>
</div>
<div class="mt-4">
<span class="text-2xl text-red-500">*</span><label for="notify_via">Notify Via:</label>
${fErrorMsg(fr, "notify_via")}
<label class="ml-1">
<input type="radio" name="notify_via" value="email" ${checked(fr, "notify_via", "email")}>
Email Only</label>
<label class="ml-1">
<input type="radio" name="notify_via" value="xmpp" ${checked(fr, "notify_via", "xmpp")}>
XMPP Only</label>
<label class="ml-1">
<input type="radio" name="notify_via" value="both" ${checked(fr, "notify_via", "both")}>
Both</label>
</div>
<div class="mt-4">
<label>${fErrorMsg(fr, "send_date")}
<span class="text-2xl text-red-500">*</span>Send Date:&nbsp;
<input type="date" id="send_date" name="send_date" value="${fOldInput(fr, "send_date")}">
</label>
</div>
<div class="mt-4">
${fErrorMsg(fr, "send_time_hr")}
${fErrorMsg(fr, "send_time_min")}
${fErrorMsg(fr, "send_time_am_pm")}
<span class="text-2xl text-red-500">*</span>Send Time:&nbsp;
<select name="send_time_hr">
<option value="" ${selected(fr, "send_time_hr", "")}>hr</option>
<option value="1" ${selected(fr, "send_time_hr", "1")}>1</option>
<option value="2" ${selected(fr, "send_time_hr", "2")}>2</option>
<option value="3" ${selected(fr, "send_time_hr", "3")}>3</option>
<option value="4" ${selected(fr, "send_time_hr", "4")}>4</option>
<option value="5" ${selected(fr, "send_time_hr", "5")}>5</option>
<option value="6" ${selected(fr, "send_time_hr", "6")}>6</option>
<option value="7" ${selected(fr, "send_time_hr", "7")}>7</option>
<option value="8" ${selected(fr, "send_time_hr", "8")}>8</option>
<option value="9" ${selected(fr, "send_time_hr", "9")}>9</option>
<option value="10" ${selected(fr, "send_time_hr", "10")}>10</option>
<option value="11" ${selected(fr, "send_time_hr", "11")}>11</option>
<option value="12" ${selected(fr, "send_time_hr", "12")}>12</option>
</select>
<select name="send_time_min">
<option value="" ${selected(fr, "send_time_min", "")}>min</option>
<option value="00" ${selected(fr, "send_time_min", "00")}>00</option>
<option value="15" ${selected(fr, "send_time_min", "15")}>15</option>
<option value="30" ${selected(fr, "send_time_min", "30")}>30</option>
<option value="45" ${selected(fr, "send_time_min", "45")}>45</option>
</select>
<select name="send_time_am_pm">
<option value="" ${selected(fr, "send_time_am_pm", "")}>AM/PM</option>
<option value="am" ${selected(fr, "send_time_am_pm", "am")}>am</option>
<option value="pm" ${selected(fr, "send_time_am_pm", "pm")}>pm</option>
</select>
</div>
<div class="mt-4">
<label>${fErrorMsg(fr, "repeats")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="Reminders set to repeat will do so at their first opportunity, based on your Repeat settings. If the day of the Repeat hasn't occurred yet for any given (relevant) Repeat increment (week, month, year), you will get a notification on the Send Date and the Repeat Date. If you don't want two notifications during that first increment, you will want to set your Send Date to be what the first Repeat Date would be. For instance, if repeating 'weekly on Thursday' set your Send Date to the upcoming Thursday instead of 'Wednesday', as 'Wednesday' will result in a notification being sent on Wednesday and Thursday'.">Repeats?</span>
<input type="checkbox" id="repeats" name="repeats" value="1" ${checked(fr, "repeats", "1")} onclick="showDivIfChecked('repeat_freq', 'repeats')">
</label>
</div>
<div class="mt-4 hidden" id="repeat_freq">
${fErrorMsg(fr, "repeat_freq")}
Every&nbsp;
<label><input type="radio" id="repeat_freq_day" name="repeat_freq" value="day" ${checked(fr, "repeat_freq", "day")} onclick="hideAllInClassGroup('conditional_divs')"> Day</label>
<label><input type="radio" id="repeat_freq_week" name="repeat_freq" value="week" ${checked(fr, "repeat_freq", "week")} onclick="showDivFromClassGroupIfChecked('weekly_on', 'repeat_freq_week', 'conditional_divs')"> Week</label>
<label><input type="radio" id="repeat_freq_month" name="repeat_freq" value="month" ${checked(fr, "repeat_freq", "month")} onclick="showDivFromClassGroupIfChecked('monthly_on', 'repeat_freq_month', 'conditional_divs')"> Month</label>
<label><input type="radio" id="repeat_freq_year" name="repeat_freq" value="year" ${checked(fr, "repeat_freq", "year")} onclick="showDivFromClassGroupIfChecked('yearly_on', 'repeat_freq_year', 'conditional_divs')"> Year</label>
</div>
<div class="mt-4 hidden conditional_divs" id="weekly_on">
${fErrorMsg(fr, "weekly_on")}
<label>On:
<select name="weekly_on">
<option value="" ${selected(fr, "weekly_on", "")}>? Weekday</option>
<option value="Monday" ${selected(fr, "weekly_on", "Monday")}>Monday</option>
<option value="Tuesday" ${selected(fr, "weekly_on", "Tuesday")}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "weekly_on", "Wednesday")}>Wednesday</option>
<option value="Thursday" ${selected(fr, "weekly_on", "Thursday")}>Thursday</option>
<option value="Friday" ${selected(fr, "weekly_on", "Friday")}>Friday</option>
<option value="Saturday" ${selected(fr, "weekly_on", "Saturday")}>Saturday</option>
<option value="Sunday" ${selected(fr, "weekly_on", "Sunday")}>Sunday</option>
</select>
</label>
</div>
<div class="mt-4 hidden conditional_divs" id="monthly_on">
${fErrorMsg(fr, "monthly_on_day")}
<label>On the:
<select id="monthly_on_day" name="monthly_on_day" onchange="clear2ndMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_day", "")}>? Day of Month</option>
<option value="1" ${selected(fr, "monthly_on_day", "1")}>1rst</option>
<option value="2" ${selected(fr, "monthly_on_day", "2")}>2nd</option>
<option value="3" ${selected(fr, "monthly_on_day", "3")}>3rd</option>
<option value="4" ${selected(fr, "monthly_on_day", "4")}>4th</option>
<option value="5" ${selected(fr, "monthly_on_day", "5")}>5th</option>
<option value="6" ${selected(fr, "monthly_on_day", "6")}>6th</option>
<option value="7" ${selected(fr, "monthly_on_day", "7")}>7th</option>
<option value="8" ${selected(fr, "monthly_on_day", "8")}>8th</option>
<option value="9" ${selected(fr, "monthly_on_day", "9")}>9th</option>
<option value="10" ${selected(fr, "monthly_on_day", "10")}>10th</option>
<option value="11" ${selected(fr, "monthly_on_day", "11")}>11th</option>
<option value="12" ${selected(fr, "monthly_on_day", "12")}>12th</option>
<option value="13" ${selected(fr, "monthly_on_day", "13")}>13th</option>
<option value="14" ${selected(fr, "monthly_on_day", "14")}>14th</option>
<option value="15" ${selected(fr, "monthly_on_day", "15")}>15th</option>
<option value="16" ${selected(fr, "monthly_on_day", "16")}>16th</option>
<option value="17" ${selected(fr, "monthly_on_day", "17")}>17th</option>
<option value="18" ${selected(fr, "monthly_on_day", "18")}>18th</option>
<option value="19" ${selected(fr, "monthly_on_day", "19")}>19th</option>
<option value="20" ${selected(fr, "monthly_on_day", "20")}>20th</option>
<option value="21" ${selected(fr, "monthly_on_day", "21")}>21rst</option>
<option value="22" ${selected(fr, "monthly_on_day", "22")}>22nd</option>
<option value="23" ${selected(fr, "monthly_on_day", "23")}>23rd</option>
<option value="24" ${selected(fr, "monthly_on_day", "24")}>24th</option>
<option value="25" ${selected(fr, "monthly_on_day", "25")}>25th</option>
<option value="26" ${selected(fr, "monthly_on_day", "26")}>26th</option>
<option value="27" ${selected(fr, "monthly_on_day", "27")}>27th</option>
<option value="28" ${selected(fr, "monthly_on_day", "28")}>28th</option>
<option value="29" ${selected(fr, "monthly_on_day", "29")}>29th</option>
<option value="30" ${selected(fr, "monthly_on_day", "30")}>30th</option>
<option value="31" ${selected(fr, "monthly_on_day", "31")}>31rst</option>
</select> day
</label>
<div class="mt-3.5">Or...</div>
${fErrorMsg(fr, "monthly_on_week")}
${fErrorMsg(fr, "monthly_on_weekday")}
<label>On the:
<select id="monthly_on_week" name="monthly_on_week" onchange="clear1rstMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_week", "")}>? Week</option>
<option value="1" ${selected(fr, "monthly_on_week", "1")}>1rst</option>
<option value="2" ${selected(fr, "monthly_on_week", "2")}>2nd</option>
<option value="3" ${selected(fr, "monthly_on_week", "3")}>3rd</option>
<option value="last" ${selected(fr, "monthly_on_week", "last")}>Last</option>
</select>&nbsp;
<select id="monthly_on_weekday" name="monthly_on_weekday" onchange="clear1rstMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_weekday", "")}>? Weekday</option>
<option value="Monday" ${selected(fr, "monthly_on_weekday", "Monday")}>Monday</option>
<option value="Tuesday" ${selected(fr, "monthly_on_weekday", "Tuesday")}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "monthly_on_weekday", "Wednesday")}>Wednesday</option>
<option value="Thursday" ${selected(fr, "monthly_on_weekday", "Thursday")}>Thursday</option>
<option value="Friday" ${selected(fr, "monthly_on_weekday", "Friday")}>Friday</option>
<option value="Saturday" ${selected(fr, "monthly_on_weekday", "Saturday")}>Saturday</option>
<option value="Sunday" ${selected(fr, "monthly_on_weekday", "Sunday")}>Sunday</option>
</select>
</label>
</div>
<div class="mt-4 hidden conditional_divs" id="yearly_on">
${fErrorMsg(fr, "yearly_on_month")}
${fErrorMsg(fr, "yearly_on_day")}
<label>On:
<select id="yearly_on_month" name="yearly_on_month" onchange="clear2ndYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_month", "")}>? Month</option>
<option value="January" ${selected(fr, "yearly_on_month", "January")}>January</option>
<option value="Febuary" ${selected(fr, "yearly_on_month", "Febuary")}>Febuary</option>
<option value="March" ${selected(fr, "yearly_on_month", "March")}>March</option>
<option value="April" ${selected(fr, "yearly_on_month", "April")}>April</option>
<option value="May" ${selected(fr, "yearly_on_month", "May")}>May</option>
<option value="June" ${selected(fr, "yearly_on_month", "June")}>June</option>
<option value="July" ${selected(fr, "yearly_on_month", "July")}>July</option>
<option value="August" ${selected(fr, "yearly_on_month", "August")}>August</option>
<option value="September" ${selected(fr, "yearly_on_month", "September")}>September</option>
<option value="October" ${selected(fr, "yearly_on_month", "October")}>October</option>
<option value="November" ${selected(fr, "yearly_on_month", "November")}>November</option>
<option value="December" ${selected(fr, "yearly_on_month", "December")}>December</option>
</select>&nbsp;
<select id="yearly_on_day" name="yearly_on_day" onchange="clear2ndYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_day", "")}>? Day</option>
<option value="1" ${selected(fr, "yearly_on_day", "1")}>1rst</option>
<option value="2" ${selected(fr, "yearly_on_day", "2")}>2nd</option>
<option value="3" ${selected(fr, "yearly_on_day", "3")}>3rd</option>
<option value="4" ${selected(fr, "yearly_on_day", "4")}>4th</option>
<option value="5" ${selected(fr, "yearly_on_day", "5")}>5th</option>
<option value="6" ${selected(fr, "yearly_on_day", "6")}>6th</option>
<option value="7" ${selected(fr, "yearly_on_day", "7")}>7th</option>
<option value="8" ${selected(fr, "yearly_on_day", "8")}>8th</option>
<option value="9" ${selected(fr, "yearly_on_day", "9")}>9th</option>
<option value="10" ${selected(fr, "yearly_on_day", "10")}>10th</option>
<option value="11" ${selected(fr, "yearly_on_day", "11")}>11th</option>
<option value="12" ${selected(fr, "yearly_on_day", "12")}>12th</option>
<option value="13" ${selected(fr, "yearly_on_day", "13")}>13th</option>
<option value="14" ${selected(fr, "yearly_on_day", "14")}>14th</option>
<option value="15" ${selected(fr, "yearly_on_day", "15")}>15th</option>
<option value="16" ${selected(fr, "yearly_on_day", "16")}>16th</option>
<option value="17" ${selected(fr, "yearly_on_day", "17")}>17th</option>
<option value="18" ${selected(fr, "yearly_on_day", "18")}>18th</option>
<option value="19" ${selected(fr, "yearly_on_day", "19")}>19th</option>
<option value="20" ${selected(fr, "yearly_on_day", "20")}>20th</option>
<option value="21" ${selected(fr, "yearly_on_day", "21")}>21rst</option>
<option value="22" ${selected(fr, "yearly_on_day", "22")}>22nd</option>
<option value="23" ${selected(fr, "yearly_on_day", "23")}>23rd</option>
<option value="24" ${selected(fr, "yearly_on_day", "24")}>24th</option>
<option value="25" ${selected(fr, "yearly_on_day", "25")}>25th</option>
<option value="26" ${selected(fr, "yearly_on_day", "26")}>26th</option>
<option value="27" ${selected(fr, "yearly_on_day", "27")}>27th</option>
<option value="28" ${selected(fr, "yearly_on_day", "28")}>28th</option>
<option value="29" ${selected(fr, "yearly_on_day", "29")}>29th</option>
<option value="30" ${selected(fr, "yearly_on_day", "30")}>30th</option>
<option value="31" ${selected(fr, "yearly_on_day", "31")}>31rst</option>
</select>
</label>
<div class="mt-3.5">or...</div>
${fErrorMsg(fr, "yearly_on_week")}
${fErrorMsg(fr, "yearly_on_weekday")}
${fErrorMsg(fr, "yearly_on_month2")}
<label>On the:
<select id="yearly_on_week" name="yearly_on_week" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_week", "")}>? Week</option>
<option value="1" ${selected(fr, "yearly_on_week", "1")}>1rst</option>
<option value="2" ${selected(fr, "yearly_on_week", "2")}>2nd</option>
<option value="3" ${selected(fr, "yearly_on_week", "3")}>3rd</option>
<option value="last" ${selected(fr, "yearly_on_week", "last")}>Last</option>
</select>
<select id="yearly_on_weekday" name="yearly_on_weekday" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_weekday", "")}>? Weekday</option>
<option value="Monday" ${selected(fr, "yearly_on_weekday", "Monday")}>Monday</option>
<option value="Tuesday" ${selected(fr, "yearly_on_weekday", "Tuesday")}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "yearly_on_weekday", "Wednesday")}>Wednesday</option>
<option value="Thursday" ${selected(fr, "yearly_on_weekday", "Thursday")}>Thursday</option>
<option value="Friday" ${selected(fr, "yearly_on_weekday", "Friday")}>Friday</option>
<option value="Saturday" ${selected(fr, "yearly_on_weekday", "Saturday")}>Saturday</option>
<option value="Sunday" ${selected(fr, "yearly_on_weekday", "Sunday")}>Sunday</option>
</select>
<span> of </span>
<select id="yearly_on_month2" name="yearly_on_month2" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_month2", "")}>? Month</option>
<option value="January" ${selected(fr, "yearly_on_month2", "January")}>January</option>
<option value="Febuary" ${selected(fr, "yearly_on_month2", "Febuary")}>Febuary</option>
<option value="March" ${selected(fr, "yearly_on_month2", "March")}>March</option>
<option value="April" ${selected(fr, "yearly_on_month2", "April")}>April</option>
<option value="May" ${selected(fr, "yearly_on_month2", "May")}>May</option>
<option value="June" ${selected(fr, "yearly_on_month2", "June")}>June</option>
<option value="July" ${selected(fr, "yearly_on_month2", "July")}>July</option>
<option value="August" ${selected(fr, "yearly_on_month2", "August")}>August</option>
<option value="September" ${selected(fr, "yearly_on_month2", "September")}>September</option>
<option value="October" ${selected(fr, "yearly_on_month2", "October")}>October</option>
<option value="November" ${selected(fr, "yearly_on_month2", "November")}>November</option>
<option value="December" ${selected(fr, "yearly_on_month2", "December")}>December</option>
</select>
</label>
</div>
<br>
<div class="text-end">
<button class="btn-grn mt-3 mr-4" type="submit" form="reminder-create">Save</button>
</div>
</form><br>
<br>
${mainTemplateBottom()}
<div class="page-js">
<script>
var monthlyOnDay = document.getElementById("monthly_on_day")
var monthlyOnWeek = document.getElementById("monthly_on_week")
var monthlyOnWeekday = document.getElementById("monthly_on_weekday")
function clear2ndMonthlyInputs() {
if (monthlyOnDay.value != "") {
monthlyOnWeek.value = ""
monthlyOnWeekday.value = ""
}
}
function clear1rstMonthlyInputs() {
if ((monthlyOnWeek.value != "") || (monthlyOnWeekday.value != "")) {
monthlyOnDay.value = ""
}
}
var yearlyOnMonth = document.getElementById("yearly_on_month")
var yearlyOnDay = document.getElementById("yearly_on_day")
var yearlyOnWeek = document.getElementById("yearly_on_week")
var yearlyOnWeekday = document.getElementById("yearly_on_weekday")
var yearlyOnMonth2 = document.getElementById("yearly_on_month2")
function clear2ndYearlyInputs() {
if ((yearlyOnMonth.value != "") || (yearlyOnDay.value != "")) {
yearlyOnWeek.value = ""
yearlyOnWeekday.value = ""
yearlyOnMonth2.value = ""
}
}
function clear1rstYearlyInputs() {
if ((yearlyOnWeek.value != "") || (yearlyOnWeekday.value != "") || (yearlyOnMonth2.value != "")) {
yearlyOnMonth.value = ""
yearlyOnDay.value = ""
}
}
function useTitleForSubject() {
let title = document.getElementById("title")
let subject = document.getElementById("subject")
subject.value = title.value
}
function showDivIfChecked(divId, elId) {
let div = document.getElementById(divId);
let el = document.getElementById(elId);
if (el.checked == true) {
div.classList.remove("hidden");
} else {
div.classList.add("hidden");
}
}
function hideAllInClassGroup(classGroup) {
let allDivs = document.getElementsByClassName(classGroup)
Array.from(allDivs).forEach(allDiv => {
allDiv.classList.add("hidden");
});
}
function showDivFromClassGroupIfChecked(divId, elId, classGroup) {
let div = document.getElementById(divId);
let el = document.getElementById(elId);
let allDivs = document.getElementsByClassName(classGroup)
if (el.checked == true) {
div.classList.remove("hidden");
Array.from(allDivs).forEach(allDiv => {
if (allDiv.id != divId) {
allDiv.classList.add("hidden");
}
});
}
}
showDivIfChecked('repeat_freq', 'repeats')
let dayRadio = document.getElementById("repeat_freq_day")
if (dayRadio.checked == true) {
hideAllInClassGroup("conditional_divs")
}
showDivFromClassGroupIfChecked('weekly_on', 'repeat_freq_week', 'conditional_divs')
showDivFromClassGroupIfChecked('monthly_on', 'repeat_freq_month', 'conditional_divs')
showDivFromClassGroupIfChecked('yearly_on', 'repeat_freq_year', 'conditional_divs')
</script>
</div>
#end proc

View File

@ -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)}
<h1>${pageTitle}</h1>
<div class="text-end">
<form method="POST" action="${APP_URL}/reminder/delete" accept-charset="utf-8">
<input type="hidden" name="csrf_token" value="${csrfToken}">
<input type="hidden" name="reminder_id" value="${reminderId}" />
<button class="btn-red" type="submit"><span class="help-text" title="Delete the Reminder. Warning! This will not ask for confirmation.">X Reminder</span></button>
</form>
</div>
#if fr.message.len > 0:
<h2 id="form_message" class="${fr.messageClass}">${fr.message}</h2>
#end if
<form id="reminder-update" method="POST" action="${APP_URL}/reminder/update" accept-charset="utf-8">
<input type="hidden" name="csrf_token" value="${csrfToken}" form="reminder-update"/>
<input type="hidden" name="reminder_id" value="${reminder.id}">
<label>${fErrorMsg(fr, "title")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="This will be used to generate your section's url and meta-title and is very important for SEO, so it should ideally start with your main search target phrase for this section, and not have more than one search target phrase, nor be any longer than necessary.">Title</span>:
<input type="text" name="title" value="${fOldInput(fr, "title", reminder.title)}" form="reminder-update" class="w-2/5" required>
</label>
<div class="mt-4">
<label>${fErrorMsg(fr, "message")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="A shorter and possibly different version of your page's markup, used to entice the visitor to click on the link(s) to visit this Section (child) Page from it's parent Section page.">Message</span>:<br>
<textarea name="message" form="reminder-update" rows="2" class="w-3/4" required>${fOldInput(fr, "message", reminder.message)}</textarea>
</label>
</div>
<div class="mt-4">
<span class="text-2xl text-red-500">*</span><label for="notify_via">Notify Via:</label>
${fErrorMsg(fr, "notify_via")}
<label class="ml-1">
<input type="radio" name="notify_via" value="email" ${checked(fr, "notify_via", "email", reminder.notifyVia)} form="reminder-update">
Email Only</label>
<label class="ml-1">
<input type="radio" name="notify_via" value="xmpp" ${checked(fr, "notify_via", "xmpp", reminder.notifyVia)} form="reminder-update">
XMPP Only</label>
<label class="ml-1">
<input type="radio" name="notify_via" value="both" ${checked(fr, "notify_via", "both", reminder.notifyVia)} form="reminder-update">
Both</label>
</div>
<div class="mt-4">
<label>${fErrorMsg(fr, "send_date")}
<span class="text-2xl text-red-500">*</span>Send Date:&nbsp;
<input type="date" id="send_date" name="send_date" form="reminder-update" value="${fOldInput(fr, "send_date", reminder.send_date)}">
</label>
</div>
<div class="mt-4">
${fErrorMsg(fr, "send_time_hr")}
${fErrorMsg(fr, "send_time_min")}
${fErrorMsg(fr, "send_time_am_pm")}
<span class="text-2xl text-red-500">*</span>Send Time:&nbsp;
<select name="send_time_hr" form="reminder-update" >
<option value="" ${selected(fr, "send_time_hr", "", $reminder.sendTimeHr)} >hr</option>
<option value="1" ${selected(fr, "send_time_hr", "1", $reminder.sendTimeHr)}>1</option>
<option value="2" ${selected(fr, "send_time_hr", "2", $reminder.sendTimeHr)}>2</option>
<option value="3" ${selected(fr, "send_time_hr", "3", $reminder.sendTimeHr)}>3</option>
<option value="4" ${selected(fr, "send_time_hr", "4", $reminder.sendTimeHr)}>4</option>
<option value="5" ${selected(fr, "send_time_hr", "5", $reminder.sendTimeHr)}>5</option>
<option value="6" ${selected(fr, "send_time_hr", "6", $reminder.sendTimeHr)}>6</option>
<option value="7" ${selected(fr, "send_time_hr", "7", $reminder.sendTimeHr)}>7</option>
<option value="8" ${selected(fr, "send_time_hr", "8", $reminder.sendTimeHr)}>8</option>
<option value="9" ${selected(fr, "send_time_hr", "9", $reminder.sendTimeHr)}>9</option>
<option value="10" ${selected(fr, "send_time_hr", "10", $reminder.sendTimeHr)}>10</option>
<option value="11" ${selected(fr, "send_time_hr", "11", $reminder.sendTimeHr)}>11</option>
<option value="12" ${selected(fr, "send_time_hr", "12", $reminder.sendTimeHr)}>12</option>
</select>
<select name="send_time_min" form="reminder-update">
<option value="" ${selected(fr, "send_time_min", "", $reminder.sendTimeMin)}>min</option>
<option value="00" ${selected(fr, "send_time_min", "00", $reminder.sendTimeMin)}>00</option>
<option value="15" ${selected(fr, "send_time_min", "15", $reminder.sendTimeMin)}>15</option>
<option value="30" ${selected(fr, "send_time_min", "30", $reminder.sendTimeMin)}>30</option>
<option value="45" ${selected(fr, "send_time_min", "45", $reminder.sendTimeMin)}>45</option>
</select>
<select name="send_time_am_pm" form="reminder-update">
<option value="" ${selected(fr, "send_time_am_pm", "", $reminder.sendTimeAmPm)}>AM/PM</option>
<option value="am" ${selected(fr, "send_time_am_pm", "am", $reminder.sendTimeAmPm)}>am</option>
<option value="pm" ${selected(fr, "send_time_am_pm", "pm", $reminder.sendTimeAmPm)}>pm</option>
</select>
</div>
<div class="mt-4">
<label>${fErrorMsg(fr, "repeats")}
<span class="text-2xl text-red-500">*</span><span class="help-text" title="Reminders set to repeat will do so at their first opportunity, based on your Repeat settings. If the day of the Repeat hasn't occurred yet for any given (relevant) Repeat increment (week, month, year), you will get a notification on the Send Date and the Repeat Date. If you don't want two notifications during that first increment, you will want to set your Send Date to be what the first Repeat Date would be. For instance, if repeating 'weekly on Thursday' set your Send Date to the upcoming Thursday instead of 'Wednesday', as 'Wednesday' will result in a notification being sent on Wednesday and Thursday'.">Repeats?</span>
<input type="checkbox" id="repeats" name="repeats" value="1" ${checked(fr, "repeats", "1", $reminder.repeats)} onclick="showDivIfChecked('repeat_freq', 'repeats')" form="reminder-update">
</label>
</div>
<div class="mt-4 hidden" id="repeat_freq">
${fErrorMsg(fr, "repeat_freq")}
Every&nbsp;
<label><input type="radio" id="repeat_freq_day" name="repeat_freq" value="day" ${checked(fr, "repeat_freq", "day", reminder.repeat_freq)} onclick="hideAllInClassGroup('conditional_divs')" form="reminder-update"> Day</label>
<label><input type="radio" id="repeat_freq_week" name="repeat_freq" value="week" ${checked(fr, "repeat_freq", "week", reminder.repeat_freq)} onclick="showDivFromClassGroupIfChecked('weekly_on', 'repeat_freq_week', 'conditional_divs')" form="reminder-update"> Week</label>
<label><input type="radio" id="repeat_freq_month" name="repeat_freq" value="month" ${checked(fr, "repeat_freq", "month", reminder.repeat_freq)} onclick="showDivFromClassGroupIfChecked('monthly_on', 'repeat_freq_month', 'conditional_divs')" form="reminder-update"> Month</label>
<label><input type="radio" id="repeat_freq_year" name="repeat_freq" value="year" ${checked(fr, "repeat_freq", "year", reminder.repeat_freq)} onclick="showDivFromClassGroupIfChecked('yearly_on', 'repeat_freq_year', 'conditional_divs')" form="reminder-update"> Year</label>
</div>
<div class="mt-4 hidden conditional_divs" id="weekly_on">
${fErrorMsg(fr, "weekly_on")}
<label>On:
<select name="weekly_on" form="reminder-update">
<option value="" ${selected(fr, "weekly_on", "", $reminder.weeklyOn)}>? Weekday</option>
<option value="Monday" ${selected(fr, "weekly_on", "Monday", $reminder.weeklyOn)}>Monday</option>
<option value="Tuesday" ${selected(fr, "weekly_on", "Tuesday", $reminder.weeklyOn)}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "weekly_on", "Wednesday", $reminder.weeklyOn)}>Wednesday</option>
<option value="Thursday" ${selected(fr, "weekly_on", "Thursday", $reminder.weeklyOn)}>Thursday</option>
<option value="Friday" ${selected(fr, "weekly_on", "Friday", $reminder.weeklyOn)}>Friday</option>
<option value="Saturday" ${selected(fr, "weekly_on", "Saturday", $reminder.weeklyOn)}>Saturday</option>
<option value="Sunday" ${selected(fr, "weekly_on", "Sunday", $reminder.weeklyOn)}>Sunday</option>
</select>
</label>
</div>
<div class="mt-4 hidden conditional_divs" id="monthly_on">
${fErrorMsg(fr, "monthly_on_day")}
<label>On the:
<select name="monthly_on_day" form="reminder-update" id="monthly_on_day" onchange="clear2ndMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_day", "", $reminder.monthlyOnDay)}>? Day of Month</option>
<option value="1" ${selected(fr, "monthly_on_day", "1", $reminder.monthlyOnDay)}>1rst</option>
<option value="2" ${selected(fr, "monthly_on_day", "2", $reminder.monthlyOnDay)}>2nd</option>
<option value="3" ${selected(fr, "monthly_on_day", "3", $reminder.monthlyOnDay)}>3rd</option>
<option value="4" ${selected(fr, "monthly_on_day", "4", $reminder.monthlyOnDay)}>4th</option>
<option value="5" ${selected(fr, "monthly_on_day", "5", $reminder.monthlyOnDay)}>5th</option>
<option value="6" ${selected(fr, "monthly_on_day", "6", $reminder.monthlyOnDay)}>6th</option>
<option value="7" ${selected(fr, "monthly_on_day", "7", $reminder.monthlyOnDay)}>7th</option>
<option value="8" ${selected(fr, "monthly_on_day", "8", $reminder.monthlyOnDay)}>8th</option>
<option value="9" ${selected(fr, "monthly_on_day", "9", $reminder.monthlyOnDay)}>9th</option>
<option value="10" ${selected(fr, "monthly_on_day", "10", $reminder.monthlyOnDay)}>10th</option>
<option value="11" ${selected(fr, "monthly_on_day", "11", $reminder.monthlyOnDay)}>11th</option>
<option value="12" ${selected(fr, "monthly_on_day", "12", $reminder.monthlyOnDay)}>12th</option>
<option value="13" ${selected(fr, "monthly_on_day", "13", $reminder.monthlyOnDay)}>13th</option>
<option value="14" ${selected(fr, "monthly_on_day", "14", $reminder.monthlyOnDay)}>14th</option>
<option value="15" ${selected(fr, "monthly_on_day", "15", $reminder.monthlyOnDay)}>15th</option>
<option value="16" ${selected(fr, "monthly_on_day", "16", $reminder.monthlyOnDay)}>16th</option>
<option value="17" ${selected(fr, "monthly_on_day", "17", $reminder.monthlyOnDay)}>17th</option>
<option value="18" ${selected(fr, "monthly_on_day", "18", $reminder.monthlyOnDay)}>18th</option>
<option value="19" ${selected(fr, "monthly_on_day", "19", $reminder.monthlyOnDay)}>19th</option>
<option value="20" ${selected(fr, "monthly_on_day", "20", $reminder.monthlyOnDay)}>20th</option>
<option value="21" ${selected(fr, "monthly_on_day", "21", $reminder.monthlyOnDay)}>21rst</option>
<option value="22" ${selected(fr, "monthly_on_day", "22", $reminder.monthlyOnDay)}>22nd</option>
<option value="23" ${selected(fr, "monthly_on_day", "23", $reminder.monthlyOnDay)}>23rd</option>
<option value="24" ${selected(fr, "monthly_on_day", "24", $reminder.monthlyOnDay)}>24th</option>
<option value="25" ${selected(fr, "monthly_on_day", "25", $reminder.monthlyOnDay)}>25th</option>
<option value="26" ${selected(fr, "monthly_on_day", "26", $reminder.monthlyOnDay)}>26th</option>
<option value="27" ${selected(fr, "monthly_on_day", "27", $reminder.monthlyOnDay)}>27th</option>
<option value="28" ${selected(fr, "monthly_on_day", "28", $reminder.monthlyOnDay)}>28th</option>
<option value="29" ${selected(fr, "monthly_on_day", "29", $reminder.monthlyOnDay)}>29th</option>
<option value="30" ${selected(fr, "monthly_on_day", "30", $reminder.monthlyOnDay)}>30th</option>
<option value="31" ${selected(fr, "monthly_on_day", "31", $reminder.monthlyOnDay)}>31rst</option>
</select> day
</label>
<div class="mt-3.5">Or...</div>
${fErrorMsg(fr, "monthly_on_week")}
${fErrorMsg(fr, "monthly_on_weekday")}
<label>On the:
<select name="monthly_on_week" form="reminder-update" id="monthly_on_week" onchange="clear1rstMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_week", "", $reminder.monthlyOnWeek)}>? Week</option>
<option value="1" ${selected(fr, "monthly_on_week", "1", $reminder.monthlyOnWeek)}>1rst</option>
<option value="2" ${selected(fr, "monthly_on_week", "2", $reminder.monthlyOnWeek)}>2nd</option>
<option value="3" ${selected(fr, "monthly_on_week", "3", $reminder.monthlyOnWeek)}>3rd</option>
<option value="last" ${selected(fr, "monthly_on_week", "last", $reminder.monthlyOnWeek)}>Last</option>
</select>&nbsp;
<select name="monthly_on_weekday" form="reminder-update" id="monthly_on_weekday" onchange="clear1rstMonthlyInputs()">
<option value="" ${selected(fr, "monthly_on_weekday", "", $reminder.monthlyOnWeekday)}>? Weekday</option>
<option value="Monday" ${selected(fr, "monthly_on_weekday", "Monday", $reminder.monthlyOnWeekday)}>Monday</option>
<option value="Tuesday" ${selected(fr, "monthly_on_weekday", "Tuesday", $reminder.monthlyOnWeekday)}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "monthly_on_weekday", "Wednesday", $reminder.monthlyOnWeekday)}>Wednesday</option>
<option value="Thursday" ${selected(fr, "monthly_on_weekday", "Thursday", $reminder.monthlyOnWeekday)}>Thursday</option>
<option value="Friday" ${selected(fr, "monthly_on_weekday", "Friday", $reminder.monthlyOnWeekday)}>Friday</option>
<option value="Saturday" ${selected(fr, "monthly_on_weekday", "Saturday", $reminder.monthlyOnWeekday)}>Saturday</option>
<option value="Sunday" ${selected(fr, "monthly_on_weekday", "Sunday", $reminder.monthlyOnWeekday)}>Sunday</option>
</select>
</label>
</div>
<div class="mt-4 hidden conditional_divs" id="yearly_on">
${fErrorMsg(fr, "yearly_on_month")}
${fErrorMsg(fr, "yearly_on_day")}
<label>On:
<select name="yearly_on_month" form="reminder-update" id="yearly_on_month" onchange="clear2ndYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_month", "", $reminder.yearlyOnMonth)}>? Month</option>
<option value="January" ${selected(fr, "yearly_on_month", "January", $reminder.yearlyOnMonth)}>January</option>
<option value="Febuary" ${selected(fr, "yearly_on_month", "Febuary", $reminder.yearlyOnMonth)}>Febuary</option>
<option value="March" ${selected(fr, "yearly_on_month", "March", $reminder.yearlyOnMonth)}>March</option>
<option value="April" ${selected(fr, "yearly_on_month", "April", $reminder.yearlyOnMonth)}>April</option>
<option value="May" ${selected(fr, "yearly_on_month", "May", $reminder.yearlyOnMonth)}>May</option>
<option value="June" ${selected(fr, "yearly_on_month", "June", $reminder.yearlyOnMonth)}>June</option>
<option value="July" ${selected(fr, "yearly_on_month", "July", $reminder.yearlyOnMonth)}>July</option>
<option value="August" ${selected(fr, "yearly_on_month", "August", $reminder.yearlyOnMonth)}>August</option>
<option value="September" ${selected(fr, "yearly_on_month", "September", $reminder.yearlyOnMonth)}>September</option>
<option value="October" ${selected(fr, "yearly_on_month", "October", $reminder.yearlyOnMonth)}>October</option>
<option value="November" ${selected(fr, "yearly_on_month", "November", $reminder.yearlyOnMonth)}>November</option>
<option value="December" ${selected(fr, "yearly_on_month", "December", $reminder.yearlyOnMonth)}>December</option>
</select>&nbsp;
<select name="yearly_on_day" form="reminder-update" id="yearly_on_day" onchange="clear2ndYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_day", "", $reminder.yearlyOnDay)}>? Day</option>
<option value="1" ${selected(fr, "yearly_on_day", "1", $reminder.yearlyOnDay)}>1rst</option>
<option value="2" ${selected(fr, "yearly_on_day", "2", $reminder.yearlyOnDay)}>2nd</option>
<option value="3" ${selected(fr, "yearly_on_day", "3", $reminder.yearlyOnDay)}>3rd</option>
<option value="4" ${selected(fr, "yearly_on_day", "4", $reminder.yearlyOnDay)}>4th</option>
<option value="5" ${selected(fr, "yearly_on_day", "5", $reminder.yearlyOnDay)}>5th</option>
<option value="6" ${selected(fr, "yearly_on_day", "6", $reminder.yearlyOnDay)}>6th</option>
<option value="7" ${selected(fr, "yearly_on_day", "7", $reminder.yearlyOnDay)}>7th</option>
<option value="8" ${selected(fr, "yearly_on_day", "8", $reminder.yearlyOnDay)}>8th</option>
<option value="9" ${selected(fr, "yearly_on_day", "9", $reminder.yearlyOnDay)}>9th</option>
<option value="10" ${selected(fr, "yearly_on_day", "10", $reminder.yearlyOnDay)}>10th</option>
<option value="11" ${selected(fr, "yearly_on_day", "11", $reminder.yearlyOnDay)}>11th</option>
<option value="12" ${selected(fr, "yearly_on_day", "12", $reminder.yearlyOnDay)}>12th</option>
<option value="13" ${selected(fr, "yearly_on_day", "13", $reminder.yearlyOnDay)}>13th</option>
<option value="14" ${selected(fr, "yearly_on_day", "14", $reminder.yearlyOnDay)}>14th</option>
<option value="15" ${selected(fr, "yearly_on_day", "15", $reminder.yearlyOnDay)}>15th</option>
<option value="16" ${selected(fr, "yearly_on_day", "16", $reminder.yearlyOnDay)}>16th</option>
<option value="17" ${selected(fr, "yearly_on_day", "17", $reminder.yearlyOnDay)}>17th</option>
<option value="18" ${selected(fr, "yearly_on_day", "18", $reminder.yearlyOnDay)}>18th</option>
<option value="19" ${selected(fr, "yearly_on_day", "19", $reminder.yearlyOnDay)}>19th</option>
<option value="20" ${selected(fr, "yearly_on_day", "20", $reminder.yearlyOnDay)}>20th</option>
<option value="21" ${selected(fr, "yearly_on_day", "21", $reminder.yearlyOnDay)}>21rst</option>
<option value="22" ${selected(fr, "yearly_on_day", "22", $reminder.yearlyOnDay)}>22nd</option>
<option value="23" ${selected(fr, "yearly_on_day", "23", $reminder.yearlyOnDay)}>23rd</option>
<option value="24" ${selected(fr, "yearly_on_day", "24", $reminder.yearlyOnDay)}>24th</option>
<option value="25" ${selected(fr, "yearly_on_day", "25", $reminder.yearlyOnDay)}>25th</option>
<option value="26" ${selected(fr, "yearly_on_day", "26", $reminder.yearlyOnDay)}>26th</option>
<option value="27" ${selected(fr, "yearly_on_day", "27", $reminder.yearlyOnDay)}>27th</option>
<option value="28" ${selected(fr, "yearly_on_day", "28", $reminder.yearlyOnDay)}>28th</option>
<option value="29" ${selected(fr, "yearly_on_day", "29", $reminder.yearlyOnDay)}>29th</option>
<option value="30" ${selected(fr, "yearly_on_day", "30", $reminder.yearlyOnDay)}>30th</option>
<option value="31" ${selected(fr, "yearly_on_day", "31", $reminder.yearlyOnDay)}>31rst</option>
</select>
</label>
<div class="mt-3.5">or...</div>
${fErrorMsg(fr, "yearly_on_week")}
${fErrorMsg(fr, "yearly_on_weekday")}
${fErrorMsg(fr, "yearly_on_month2")}
<label>On the:
<select name="yearly_on_week" form="reminder-update" id="yearly_on_week" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_week", "", $reminder.yearlyOnWeek)}>? Week</option>
<option value="1" ${selected(fr, "yearly_on_week", "1", $reminder.yearlyOnWeek)}>1rst</option>
<option value="2" ${selected(fr, "yearly_on_week", "2", $reminder.yearlyOnWeek)}>2nd</option>
<option value="3" ${selected(fr, "yearly_on_week", "3", $reminder.yearlyOnWeek)}>3rd</option>
<option value="last" ${selected(fr, "yearly_on_week", "last", $reminder.yearlyOnWeek)}>Last</option>
</select>
<select name="yearly_on_weekday" form="reminder-update" id="yearly_on_weekday" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_weekday", "", $reminder.yearlyOnWeekday)}>? Weekday</option>
<option value="Monday" ${selected(fr, "yearly_on_weekday", "Monday", $reminder.yearlyOnWeekday)}>Monday</option>
<option value="Tuesday" ${selected(fr, "yearly_on_weekday", "Tuesday", $reminder.yearlyOnWeekday)}>Tuesday</option>
<option value="Wednesday" ${selected(fr, "yearly_on_weekday", "Wednesday", $reminder.yearlyOnWeekday)}>Wednesday</option>
<option value="Thursday" ${selected(fr, "yearly_on_weekday", "Thursday", $reminder.yearlyOnWeekday)}>Thursday</option>
<option value="Friday" ${selected(fr, "yearly_on_weekday", "Friday", $reminder.yearlyOnWeekday)}>Friday</option>
<option value="Saturday" ${selected(fr, "yearly_on_weekday", "Saturday", $reminder.yearlyOnWeekday)}>Saturday</option>
<option value="Sunday" ${selected(fr, "yearly_on_weekday", "Sunday", $reminder.yearlyOnWeekday)}>Sunday</option>
</select>
<span> of </span>
<select name="yearly_on_month2" form="reminder-update" id="yearly_on_month2" onchange="clear1rstYearlyInputs()">
<option value="" ${selected(fr, "yearly_on_month2", "", $reminder.yearlyOnMonth2)}>? Month</option>
<option value="January" ${selected(fr, "yearly_on_month2", "January", $reminder.yearlyOnMonth2)}>January</option>
<option value="Febuary" ${selected(fr, "yearly_on_month2", "Febuary", $reminder.yearlyOnMonth2)}>Febuary</option>
<option value="March" ${selected(fr, "yearly_on_month2", "March", $reminder.yearlyOnMonth2)}>March</option>
<option value="April" ${selected(fr, "yearly_on_month2", "April", $reminder.yearlyOnMonth2)}>April</option>
<option value="May" ${selected(fr, "yearly_on_month2", "May", $reminder.yearlyOnMonth2)}>May</option>
<option value="June" ${selected(fr, "yearly_on_month2", "June", $reminder.yearlyOnMonth2)}>June</option>
<option value="July" ${selected(fr, "yearly_on_month2", "July", $reminder.yearlyOnMonth2)}>July</option>
<option value="August" ${selected(fr, "yearly_on_month2", "August", $reminder.yearlyOnMonth2)}>August</option>
<option value="September" ${selected(fr, "yearly_on_month2", "September", $reminder.yearlyOnMonth2)}>September</option>
<option value="October" ${selected(fr, "yearly_on_month2", "October", $reminder.yearlyOnMonth2)}>October</option>
<option value="November" ${selected(fr, "yearly_on_month2", "November", $reminder.yearlyOnMonth2)}>November</option>
<option value="December" ${selected(fr, "yearly_on_month2", "December", $reminder.yearlyOnMonth2)}>December</option>
</select>
</label>
</div>
<br>
<div class="text-end">
<button class="btn-grn mt-3 mr-4" type="submit" form="reminder-update">Save</button>
</div>
</form><br><br>
<div class="page-js">
<script>
var monthlyOnDay = document.getElementById("monthly_on_day")
var monthlyOnWeek = document.getElementById("monthly_on_week")
var monthlyOnWeekday = document.getElementById("monthly_on_weekday")
function clear2ndMonthlyInputs() {
if (monthlyOnDay.value != "") {
monthlyOnWeek.value = ""
monthlyOnWeekday.value = ""
}
}
function clear1rstMonthlyInputs() {
if ((monthlyOnWeek.value != "") || (monthlyOnWeekday.value != "")) {
monthlyOnDay.value = ""
}
}
var yearlyOnMonth = document.getElementById("yearly_on_month")
var yearlyOnDay = document.getElementById("yearly_on_day")
var yearlyOnWeek = document.getElementById("yearly_on_week")
var yearlyOnWeekday = document.getElementById("yearly_on_weekday")
var yearlyOnMonth2 = document.getElementById("yearly_on_month2")
function clear2ndYearlyInputs() {
if ((yearlyOnMonth.value != "") || (yearlyOnDay.value != "")) {
yearlyOnWeek.value = ""
yearlyOnWeekday.value = ""
yearlyOnMonth2.value = ""
}
}
function clear1rstYearlyInputs() {
if ((yearlyOnWeek.value != "") || (yearlyOnWeekday.value != "") || (yearlyOnMonth2.value != "")) {
yearlyOnMonth.value = ""
yearlyOnDay.value = ""
}
}
function useTitleForSubject() {
let title = document.getElementById("title")
let subject = document.getElementById("subject")
subject.value = title.value
}
function showDivIfChecked(divId, elId) {
let div = document.getElementById(divId);
let el = document.getElementById(elId);
if (el.checked == true) {
div.classList.remove("hidden");
} else {
div.classList.add("hidden");
}
}
function hideAllInClassGroup(classGroup) {
let allDivs = document.getElementsByClassName(classGroup)
Array.from(allDivs).forEach(allDiv => {
allDiv.classList.add("hidden");
});
}
function showDivFromClassGroupIfChecked(divId, elId, classGroup) {
let div = document.getElementById(divId);
let el = document.getElementById(elId);
let allDivs = document.getElementsByClassName(classGroup)
if (el.checked == true) {
div.classList.remove("hidden");
Array.from(allDivs).forEach(allDiv => {
if (allDiv.id != divId) {
allDiv.classList.add("hidden");
}
});
}
}
showDivIfChecked('repeat_freq', 'repeats')
let dayRadio = document.getElementById("repeat_freq_day")
if (dayRadio.checked == true) {
hideAllInClassGroup("conditional_divs")
}
showDivFromClassGroupIfChecked('weekly_on', 'repeat_freq_week', 'conditional_divs')
showDivFromClassGroupIfChecked('monthly_on', 'repeat_freq_month', 'conditional_divs')
showDivFromClassGroupIfChecked('yearly_on', 'repeat_freq_year', 'conditional_divs')
</script>
</div>
${mainTemplateBottom()}
#end proc

44
templates/reminders.nimf Normal file
View File

@ -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)}
<div class="text-center">
<span class="text-2xl">Upcoming Reminders</span><a href="/create-reminder" class="a-btn-grn ml-8">+ New Reminder</a>
</div>
#if allReminders.len > 0:
<div class="mt-8 flex flex-row p-2 border-b border-indigo-400">
<div class="basis-2/4"><span class="text-xl">Title</span></div>
<div class="basis-1/12"><span class="text-xl">Repeats?</span></div>
<div class="basis-1/4"><span class="text-xl">Next Send Date</span></div>
<div class="basis-1/4"><span class="text-xl">Actions</span></div>
</div>
#else:
<br><br><h2 class="text-lg text-orange-400">You don't have any Reminders yet.</h2>
#end if
#for reminder in allReminders:
<div class="flex flex-row p-2">
<div class="basis-2/4"><span class="text-lg">${reminder.title}</span></div>
<div class="basis-1/12"><span class="text-lg">
#if reminder.repeats == 0:
No
#else:
Yes
#end if
</span></div>
<div class="basis-1/4"><span class="text-lg">${reminder.sendDate}</span></div>
<div class="basis-1/4">
<span><a href="/reminder/${reminder.id}/update" class="a-btn-grn">View/Edit</a></span>
<form method="POST" action="/reminder/delete" accept-charset="utf-8" class="inline">
<input type="hidden" name="csrf_token" value="${csrfToken}">
<input type="hidden" name="reminder_id" value="${reminder.id}" />
<button class="btn-red" type="submit"><span class="help-text" title="Delete the Reminder. Warning! Will not ask for confirmation.">Delete</span></button>
</form>
</div>
</div>
#end for
<div class="page-js"></div>
${mainTemplateBottom()}
#end proc