initial commit
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 93 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 60 KiB |
|
@ -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 |
|
@ -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 |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 20 KiB |
|
@ -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"); });
|
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 61 KiB |
After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 64 KiB |
After Width: | Height: | Size: 63 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 33 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 235 KiB |
|
@ -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)
|
|
@ -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")])
|
|
@ -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
|
|
@ -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
|
|
@ -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'"
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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]#
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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")])
|
|
@ -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")])
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"> </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"> </div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${mainTemplateBottom()}
|
||||||
|
##leave this even when empty.
|
||||||
|
<div class="page-js"></div>
|
||||||
|
#end proc
|
|
@ -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 & 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
|
|
@ -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
|
|
@ -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:
|
||||||
|
<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:
|
||||||
|
<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
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
|
@ -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:
|
||||||
|
<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:
|
||||||
|
<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
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<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
|
|
@ -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
|