.\n\n.list-group {\n // No need to set list-style: none; since .list-group-item is block level\n margin-bottom: 20px;\n padding-left: 0; // reset padding because ul and ol\n}\n\n\n// Individual list items\n//\n// Use on `li`s or `div`s within the `.list-group` parent.\n\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n // Place the border on the list items and negative margin up for better styling\n margin-bottom: -1px;\n background-color: @list-group-bg;\n border: 1px solid @list-group-border;\n\n // Round the first and last items\n &:first-child {\n .border-top-radius(@list-group-border-radius);\n }\n &:last-child {\n margin-bottom: 0;\n .border-bottom-radius(@list-group-border-radius);\n }\n}\n\n\n// Interactive list items\n//\n// Use anchor or button elements instead of `li`s or `div`s to create interactive items.\n// Includes an extra `.active` modifier class for showing selected items.\n\na.list-group-item,\nbutton.list-group-item {\n color: @list-group-link-color;\n\n .list-group-item-heading {\n color: @list-group-link-heading-color;\n }\n\n // Hover state\n &:hover,\n &:focus {\n text-decoration: none;\n color: @list-group-link-hover-color;\n background-color: @list-group-hover-bg;\n }\n}\n\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n\n.list-group-item {\n // Disabled state\n &.disabled,\n &.disabled:hover,\n &.disabled:focus {\n background-color: @list-group-disabled-bg;\n color: @list-group-disabled-color;\n cursor: @cursor-disabled;\n\n // Force color to inherit for custom content\n .list-group-item-heading {\n color: inherit;\n }\n .list-group-item-text {\n color: @list-group-disabled-text-color;\n }\n }\n\n // Active class on item itself, not parent\n &.active,\n &.active:hover,\n &.active:focus {\n z-index: 2; // Place active items above their siblings for proper border styling\n color: @list-group-active-color;\n background-color: @list-group-active-bg;\n border-color: @list-group-active-border;\n\n // Force color to inherit for custom content\n .list-group-item-heading,\n .list-group-item-heading > small,\n .list-group-item-heading > .small {\n color: inherit;\n }\n .list-group-item-text {\n color: @list-group-active-text-color;\n }\n }\n}\n\n\n// Contextual variants\n//\n// Add modifier classes to change text and background color on individual items.\n// Organizationally, this must come after the `:hover` states.\n\n.list-group-item-variant(success; @state-success-bg; @state-success-text);\n.list-group-item-variant(info; @state-info-bg; @state-info-text);\n.list-group-item-variant(warning; @state-warning-bg; @state-warning-text);\n.list-group-item-variant(danger; @state-danger-bg; @state-danger-text);\n\n\n// Custom content options\n//\n// Extra classes for creating well-formatted content within `.list-group-item`s.\n\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n","// List Groups\n\n.list-group-item-variant(@state; @background; @color) {\n .list-group-item-@{state} {\n color: @color;\n background-color: @background;\n\n a&,\n button& {\n color: @color;\n\n .list-group-item-heading {\n color: inherit;\n }\n\n &:hover,\n &:focus {\n color: @color;\n background-color: darken(@background, 5%);\n }\n &.active,\n &.active:hover,\n &.active:focus {\n color: #fff;\n background-color: @color;\n border-color: @color;\n }\n }\n }\n}\n","//\n// Panels\n// --------------------------------------------------\n\n\n// Base class\n.panel {\n margin-bottom: @line-height-computed;\n background-color: @panel-bg;\n border: 1px solid transparent;\n border-radius: @panel-border-radius;\n .box-shadow(0 1px 1px rgba(0,0,0,.05));\n}\n\n// Panel contents\n.panel-body {\n padding: @panel-body-padding;\n &:extend(.clearfix all);\n}\n\n// Optional heading\n.panel-heading {\n padding: @panel-heading-padding;\n border-bottom: 1px solid transparent;\n .border-top-radius((@panel-border-radius - 1));\n\n > .dropdown .dropdown-toggle {\n color: inherit;\n }\n}\n\n// Within heading, strip any `h*` tag of its default margins for spacing.\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: ceil((@font-size-base * 1.125));\n color: inherit;\n\n > a,\n > small,\n > .small,\n > small > a,\n > .small > a {\n color: inherit;\n }\n}\n\n// Optional footer (stays gray in every modifier class)\n.panel-footer {\n padding: @panel-footer-padding;\n background-color: @panel-footer-bg;\n border-top: 1px solid @panel-inner-border;\n .border-bottom-radius((@panel-border-radius - 1));\n}\n\n\n// List groups in panels\n//\n// By default, space out list group content from panel headings to account for\n// any kind of custom content between the two.\n\n.panel {\n > .list-group,\n > .panel-collapse > .list-group {\n margin-bottom: 0;\n\n .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n }\n\n // Add border top radius for first one\n &:first-child {\n .list-group-item:first-child {\n border-top: 0;\n .border-top-radius((@panel-border-radius - 1));\n }\n }\n\n // Add border bottom radius for last one\n &:last-child {\n .list-group-item:last-child {\n border-bottom: 0;\n .border-bottom-radius((@panel-border-radius - 1));\n }\n }\n }\n > .panel-heading + .panel-collapse > .list-group {\n .list-group-item:first-child {\n .border-top-radius(0);\n }\n }\n}\n// Collapse space between when there's no additional content.\n.panel-heading + .list-group {\n .list-group-item:first-child {\n border-top-width: 0;\n }\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n\n// Tables in panels\n//\n// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and\n// watch it go full width.\n\n.panel {\n > .table,\n > .table-responsive > .table,\n > .panel-collapse > .table {\n margin-bottom: 0;\n\n caption {\n padding-left: @panel-body-padding;\n padding-right: @panel-body-padding;\n }\n }\n // Add border top radius for first one\n > .table:first-child,\n > .table-responsive:first-child > .table:first-child {\n .border-top-radius((@panel-border-radius - 1));\n\n > thead:first-child,\n > tbody:first-child {\n > tr:first-child {\n border-top-left-radius: (@panel-border-radius - 1);\n border-top-right-radius: (@panel-border-radius - 1);\n\n td:first-child,\n th:first-child {\n border-top-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-top-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n // Add border bottom radius for last one\n > .table:last-child,\n > .table-responsive:last-child > .table:last-child {\n .border-bottom-radius((@panel-border-radius - 1));\n\n > tbody:last-child,\n > tfoot:last-child {\n > tr:last-child {\n border-bottom-left-radius: (@panel-border-radius - 1);\n border-bottom-right-radius: (@panel-border-radius - 1);\n\n td:first-child,\n th:first-child {\n border-bottom-left-radius: (@panel-border-radius - 1);\n }\n td:last-child,\n th:last-child {\n border-bottom-right-radius: (@panel-border-radius - 1);\n }\n }\n }\n }\n > .panel-body + .table,\n > .panel-body + .table-responsive,\n > .table + .panel-body,\n > .table-responsive + .panel-body {\n border-top: 1px solid @table-border-color;\n }\n > .table > tbody:first-child > tr:first-child th,\n > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n }\n > .table-bordered,\n > .table-responsive > .table-bordered {\n border: 0;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n > thead,\n > tbody {\n > tr:first-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n > tbody,\n > tfoot {\n > tr:last-child {\n > td,\n > th {\n border-bottom: 0;\n }\n }\n }\n }\n > .table-responsive {\n border: 0;\n margin-bottom: 0;\n }\n}\n\n\n// Collapsible panels (aka, accordion)\n//\n// Wrap a series of panels in `.panel-group` to turn them into an accordion with\n// the help of our collapse JavaScript plugin.\n\n.panel-group {\n margin-bottom: @line-height-computed;\n\n // Tighten up margin so it's only between panels\n .panel {\n margin-bottom: 0;\n border-radius: @panel-border-radius;\n\n + .panel {\n margin-top: 5px;\n }\n }\n\n .panel-heading {\n border-bottom: 0;\n\n + .panel-collapse > .panel-body,\n + .panel-collapse > .list-group {\n border-top: 1px solid @panel-inner-border;\n }\n }\n\n .panel-footer {\n border-top: 0;\n + .panel-collapse .panel-body {\n border-bottom: 1px solid @panel-inner-border;\n }\n }\n}\n\n\n// Contextual variations\n.panel-default {\n .panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border);\n}\n.panel-primary {\n .panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border);\n}\n.panel-success {\n .panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border);\n}\n.panel-info {\n .panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border);\n}\n.panel-warning {\n .panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border);\n}\n.panel-danger {\n .panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border);\n}\n","// Panels\n\n.panel-variant(@border; @heading-text-color; @heading-bg-color; @heading-border) {\n border-color: @border;\n\n & > .panel-heading {\n color: @heading-text-color;\n background-color: @heading-bg-color;\n border-color: @heading-border;\n\n + .panel-collapse > .panel-body {\n border-top-color: @border;\n }\n .badge {\n color: @heading-bg-color;\n background-color: @heading-text-color;\n }\n }\n & > .panel-footer {\n + .panel-collapse > .panel-body {\n border-bottom-color: @border;\n }\n }\n}\n","// Embeds responsive\n//\n// Credit: Nicolas Gallagher and SUIT CSS.\n\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n\n .embed-responsive-item,\n iframe,\n embed,\n object,\n video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n }\n}\n\n// Modifier class for 16:9 aspect ratio\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n\n// Modifier class for 4:3 aspect ratio\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n","//\n// Wells\n// --------------------------------------------------\n\n\n// Base class\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: @well-bg;\n border: 1px solid @well-border;\n border-radius: @border-radius-base;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));\n blockquote {\n border-color: #ddd;\n border-color: rgba(0,0,0,.15);\n }\n}\n\n// Sizes\n.well-lg {\n padding: 24px;\n border-radius: @border-radius-large;\n}\n.well-sm {\n padding: 9px;\n border-radius: @border-radius-small;\n}\n","//\n// Close icons\n// --------------------------------------------------\n\n\n.close {\n float: right;\n font-size: (@font-size-base * 1.5);\n font-weight: @close-font-weight;\n line-height: 1;\n color: @close-color;\n text-shadow: @close-text-shadow;\n .opacity(.2);\n\n &:hover,\n &:focus {\n color: @close-color;\n text-decoration: none;\n cursor: pointer;\n .opacity(.5);\n }\n\n // Additional properties for button version\n // iOS requires the button element instead of an anchor tag.\n // If you want the anchor version, it requires `href=\"#\"`.\n // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n button& {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n }\n}\n","//\n// Modals\n// --------------------------------------------------\n\n// .modal-open - body class for killing the scroll\n// .modal - container to scroll within\n// .modal-dialog - positioning shell for the actual modal\n// .modal-content - actual modal w/ bg and corners and shit\n\n// Kill the scroll on the body\n.modal-open {\n overflow: hidden;\n}\n\n// Container that the modal scrolls within\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: @zindex-modal;\n -webkit-overflow-scrolling: touch;\n\n // Prevent Chrome on Windows from adding a focus outline. For details, see\n // https://github.com/twbs/bootstrap/pull/10951.\n outline: 0;\n\n // When fading in the modal, animate it to slide down\n &.fade .modal-dialog {\n .translate(0, -25%);\n .transition-transform(~\"0.3s ease-out\");\n }\n &.in .modal-dialog { .translate(0, 0) }\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n// Shell div to position the modal with bottom padding\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n\n// Actual modal\n.modal-content {\n position: relative;\n background-color: @modal-content-bg;\n border: 1px solid @modal-content-fallback-border-color; //old browsers fallback (ie8 etc)\n border: 1px solid @modal-content-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 3px 9px rgba(0,0,0,.5));\n background-clip: padding-box;\n // Remove focus outline from opened modal\n outline: 0;\n}\n\n// Modal background\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: @zindex-modal-background;\n background-color: @modal-backdrop-bg;\n // Fade for backdrop\n &.fade { .opacity(0); }\n &.in { .opacity(@modal-backdrop-opacity); }\n}\n\n// Modal header\n// Top section of the modal w/ title and dismiss\n.modal-header {\n padding: @modal-title-padding;\n border-bottom: 1px solid @modal-header-border-color;\n &:extend(.clearfix all);\n}\n// Close icon\n.modal-header .close {\n margin-top: -2px;\n}\n\n// Title text within header\n.modal-title {\n margin: 0;\n line-height: @modal-title-line-height;\n}\n\n// Modal body\n// Where all modal content resides (sibling of .modal-header and .modal-footer)\n.modal-body {\n position: relative;\n padding: @modal-inner-padding;\n}\n\n// Footer (for actions)\n.modal-footer {\n padding: @modal-inner-padding;\n text-align: right; // right align buttons\n border-top: 1px solid @modal-footer-border-color;\n &:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons\n\n // Properly space out buttons\n .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0; // account for input[type=\"submit\"] which gets the bottom margin like all other inputs\n }\n // but override that for button groups\n .btn-group .btn + .btn {\n margin-left: -1px;\n }\n // and override it for block buttons as well\n .btn-block + .btn-block {\n margin-left: 0;\n }\n}\n\n// Measure scrollbar width for padding body during modal show/hide\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n\n// Scale up the modal\n@media (min-width: @screen-sm-min) {\n // Automatically set modal's width for larger viewports\n .modal-dialog {\n width: @modal-md;\n margin: 30px auto;\n }\n .modal-content {\n .box-shadow(0 5px 15px rgba(0,0,0,.5));\n }\n\n // Modal sizes\n .modal-sm { width: @modal-sm; }\n}\n\n@media (min-width: @screen-md-min) {\n .modal-lg { width: @modal-lg; }\n}\n","//\n// Tooltips\n// --------------------------------------------------\n\n\n// Base class\n.tooltip {\n position: absolute;\n z-index: @zindex-tooltip;\n display: block;\n // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n .reset-text();\n font-size: @font-size-small;\n\n .opacity(0);\n\n &.in { .opacity(@tooltip-opacity); }\n &.top { margin-top: -3px; padding: @tooltip-arrow-width 0; }\n &.right { margin-left: 3px; padding: 0 @tooltip-arrow-width; }\n &.bottom { margin-top: 3px; padding: @tooltip-arrow-width 0; }\n &.left { margin-left: -3px; padding: 0 @tooltip-arrow-width; }\n}\n\n// Wrapper for the tooltip content\n.tooltip-inner {\n max-width: @tooltip-max-width;\n padding: 3px 8px;\n color: @tooltip-color;\n text-align: center;\n background-color: @tooltip-bg;\n border-radius: @border-radius-base;\n}\n\n// Arrows\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n// Note: Deprecated .top-left, .top-right, .bottom-left, and .bottom-right as of v3.3.1\n.tooltip {\n &.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-left .tooltip-arrow {\n bottom: 0;\n right: @tooltip-arrow-width;\n margin-bottom: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.top-right .tooltip-arrow {\n bottom: 0;\n left: @tooltip-arrow-width;\n margin-bottom: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width 0;\n border-top-color: @tooltip-arrow-color;\n }\n &.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width @tooltip-arrow-width @tooltip-arrow-width 0;\n border-right-color: @tooltip-arrow-color;\n }\n &.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -@tooltip-arrow-width;\n border-width: @tooltip-arrow-width 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-left-color: @tooltip-arrow-color;\n }\n &.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-left .tooltip-arrow {\n top: 0;\n right: @tooltip-arrow-width;\n margin-top: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n &.bottom-right .tooltip-arrow {\n top: 0;\n left: @tooltip-arrow-width;\n margin-top: -@tooltip-arrow-width;\n border-width: 0 @tooltip-arrow-width @tooltip-arrow-width;\n border-bottom-color: @tooltip-arrow-color;\n }\n}\n",".reset-text() {\n font-family: @font-family-base;\n // We deliberately do NOT reset font-size.\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: @line-height-base;\n text-align: left; // Fallback for where `start` is not supported\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n}\n","//\n// Popovers\n// --------------------------------------------------\n\n\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: @zindex-popover;\n display: none;\n max-width: @popover-max-width;\n padding: 1px;\n // Our parent element can be arbitrary since popovers are by default inserted as a sibling of their target element.\n // So reset our font and text properties to avoid inheriting weird values.\n .reset-text();\n font-size: @font-size-base;\n\n background-color: @popover-bg;\n background-clip: padding-box;\n border: 1px solid @popover-fallback-border-color;\n border: 1px solid @popover-border-color;\n border-radius: @border-radius-large;\n .box-shadow(0 5px 10px rgba(0,0,0,.2));\n\n // Offset the popover to account for the popover arrow\n &.top { margin-top: -@popover-arrow-width; }\n &.right { margin-left: @popover-arrow-width; }\n &.bottom { margin-top: @popover-arrow-width; }\n &.left { margin-left: -@popover-arrow-width; }\n}\n\n.popover-title {\n margin: 0; // reset heading margin\n padding: 8px 14px;\n font-size: @font-size-base;\n background-color: @popover-title-bg;\n border-bottom: 1px solid darken(@popover-title-bg, 5%);\n border-radius: (@border-radius-large - 1) (@border-radius-large - 1) 0 0;\n}\n\n.popover-content {\n padding: 9px 14px;\n}\n\n// Arrows\n//\n// .arrow is outer, .arrow:after is inner\n\n.popover > .arrow {\n &,\n &:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n }\n}\n.popover > .arrow {\n border-width: @popover-arrow-outer-width;\n}\n.popover > .arrow:after {\n border-width: @popover-arrow-width;\n content: \"\";\n}\n\n.popover {\n &.top > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-top-color: @popover-arrow-outer-color;\n bottom: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n bottom: 1px;\n margin-left: -@popover-arrow-width;\n border-bottom-width: 0;\n border-top-color: @popover-arrow-color;\n }\n }\n &.right > .arrow {\n top: 50%;\n left: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-right-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n left: 1px;\n bottom: -@popover-arrow-width;\n border-left-width: 0;\n border-right-color: @popover-arrow-color;\n }\n }\n &.bottom > .arrow {\n left: 50%;\n margin-left: -@popover-arrow-outer-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-bottom-color: @popover-arrow-outer-color;\n top: -@popover-arrow-outer-width;\n &:after {\n content: \" \";\n top: 1px;\n margin-left: -@popover-arrow-width;\n border-top-width: 0;\n border-bottom-color: @popover-arrow-color;\n }\n }\n\n &.left > .arrow {\n top: 50%;\n right: -@popover-arrow-outer-width;\n margin-top: -@popover-arrow-outer-width;\n border-right-width: 0;\n border-left-color: @popover-arrow-outer-fallback-color; // IE8 fallback\n border-left-color: @popover-arrow-outer-color;\n &:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: @popover-arrow-color;\n bottom: -@popover-arrow-width;\n }\n }\n}\n","//\n// Carousel\n// --------------------------------------------------\n\n\n// Wrapper for the slide container and indicators\n.carousel {\n position: relative;\n}\n\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n\n > .item {\n display: none;\n position: relative;\n .transition(.6s ease-in-out left);\n\n // Account for jankitude on images\n > img,\n > a > img {\n &:extend(.img-responsive);\n line-height: 1;\n }\n\n // WebKit CSS3 transforms for supported devices\n @media all and (transform-3d), (-webkit-transform-3d) {\n .transition-transform(~'0.6s ease-in-out');\n .backface-visibility(~'hidden');\n .perspective(1000px);\n\n &.next,\n &.active.right {\n .translate3d(100%, 0, 0);\n left: 0;\n }\n &.prev,\n &.active.left {\n .translate3d(-100%, 0, 0);\n left: 0;\n }\n &.next.left,\n &.prev.right,\n &.active {\n .translate3d(0, 0, 0);\n left: 0;\n }\n }\n }\n\n > .active,\n > .next,\n > .prev {\n display: block;\n }\n\n > .active {\n left: 0;\n }\n\n > .next,\n > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n }\n\n > .next {\n left: 100%;\n }\n > .prev {\n left: -100%;\n }\n > .next.left,\n > .prev.right {\n left: 0;\n }\n\n > .active.left {\n left: -100%;\n }\n > .active.right {\n left: 100%;\n }\n\n}\n\n// Left/right controls for nav\n// ---------------------------\n\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: @carousel-control-width;\n .opacity(@carousel-control-opacity);\n font-size: @carousel-control-font-size;\n color: @carousel-control-color;\n text-align: center;\n text-shadow: @carousel-text-shadow;\n background-color: rgba(0, 0, 0, 0); // Fix IE9 click-thru bug\n // We can't have this transition here because WebKit cancels the carousel\n // animation if you trip this while in the middle of another animation.\n\n // Set gradients for backgrounds\n &.left {\n #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001));\n }\n &.right {\n left: auto;\n right: 0;\n #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5));\n }\n\n // Hover/focus state\n &:hover,\n &:focus {\n outline: 0;\n color: @carousel-control-color;\n text-decoration: none;\n .opacity(.9);\n }\n\n // Toggles\n .icon-prev,\n .icon-next,\n .glyphicon-chevron-left,\n .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n margin-top: -10px;\n z-index: 5;\n display: inline-block;\n }\n .icon-prev,\n .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n }\n .icon-next,\n .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n }\n .icon-prev,\n .icon-next {\n width: 20px;\n height: 20px;\n line-height: 1;\n font-family: serif;\n }\n\n\n .icon-prev {\n &:before {\n content: '\\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)\n }\n }\n .icon-next {\n &:before {\n content: '\\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)\n }\n }\n}\n\n// Optional indicator pips\n//\n// Add an unordered list with the following class and add a list item for each\n// slide your carousel holds.\n\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n\n li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid @carousel-indicator-border-color;\n border-radius: 10px;\n cursor: pointer;\n\n // IE8-9 hack for event handling\n //\n // Internet Explorer 8-9 does not support clicks on elements without a set\n // `background-color`. We cannot use `filter` since that's not viewed as a\n // background color by the browser. Thus, a hack is needed.\n // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Internet_Explorer\n //\n // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we\n // set alpha transparency for the best results possible.\n background-color: #000 \\9; // IE8\n background-color: rgba(0,0,0,0); // IE9\n }\n .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: @carousel-indicator-active-bg;\n }\n}\n\n// Optional captions\n// -----------------------------\n// Hidden by default for smaller viewports\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: @carousel-caption-color;\n text-align: center;\n text-shadow: @carousel-text-shadow;\n & .btn {\n text-shadow: none; // No shadow for button elements in carousel-caption\n }\n}\n\n\n// Scale up controls for tablets and up\n@media screen and (min-width: @screen-sm-min) {\n\n // Scale up the controls a smidge\n .carousel-control {\n .glyphicon-chevron-left,\n .glyphicon-chevron-right,\n .icon-prev,\n .icon-next {\n width: (@carousel-control-font-size * 1.5);\n height: (@carousel-control-font-size * 1.5);\n margin-top: (@carousel-control-font-size / -2);\n font-size: (@carousel-control-font-size * 1.5);\n }\n .glyphicon-chevron-left,\n .icon-prev {\n margin-left: (@carousel-control-font-size / -2);\n }\n .glyphicon-chevron-right,\n .icon-next {\n margin-right: (@carousel-control-font-size / -2);\n }\n }\n\n // Show and left align the captions\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n\n // Move up the indicators\n .carousel-indicators {\n bottom: 20px;\n }\n}\n","// Clearfix\n//\n// For modern browsers\n// 1. The space content is one way to avoid an Opera bug when the\n// contenteditable attribute is included anywhere else in the document.\n// Otherwise it causes space to appear at the top and bottom of elements\n// that are clearfixed.\n// 2. The use of `table` rather than `block` is only necessary if using\n// `:before` to contain the top-margins of child elements.\n//\n// Source: http://nicolasgallagher.com/micro-clearfix-hack/\n\n.clearfix() {\n &:before,\n &:after {\n content: \" \"; // 1\n display: table; // 2\n }\n &:after {\n clear: both;\n }\n}\n","// Center-align a block level element\n\n.center-block() {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n","// CSS image replacement\n//\n// Heads up! v3 launched with only `.hide-text()`, but per our pattern for\n// mixins being reused as classes with the same name, this doesn't hold up. As\n// of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`.\n//\n// Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757\n\n// Deprecated as of v3.0.1 (has been removed in v4)\n.hide-text() {\n font: ~\"0/0\" a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n\n// New mixin to use as of v3.0.1\n.text-hide() {\n .hide-text();\n}\n","//\n// Responsive: Utility classes\n// --------------------------------------------------\n\n\n// IE10 in Windows (Phone) 8\n//\n// Support for responsive views via media queries is kind of borked in IE10, for\n// Surface/desktop in split view and for Windows Phone 8. This particular fix\n// must be accompanied by a snippet of JavaScript to sniff the user agent and\n// apply some conditional CSS to *only* the Surface/desktop Windows 8. Look at\n// our Getting Started page for more information on this bug.\n//\n// For more information, see the following:\n//\n// Issue: https://github.com/twbs/bootstrap/issues/10497\n// Docs: http://getbootstrap.com/getting-started/#support-ie10-width\n// Source: http://timkadlec.com/2013/01/windows-phone-8-and-device-width/\n// Source: http://timkadlec.com/2012/10/ie10-snap-mode-and-responsive-design/\n\n@-ms-viewport {\n width: device-width;\n}\n\n\n// Visibility utilities\n// Note: Deprecated .visible-xs, .visible-sm, .visible-md, and .visible-lg as of v3.2.0\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n .responsive-invisibility();\n}\n\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n\n.visible-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-visibility();\n }\n}\n.visible-xs-block {\n @media (max-width: @screen-xs-max) {\n display: block !important;\n }\n}\n.visible-xs-inline {\n @media (max-width: @screen-xs-max) {\n display: inline !important;\n }\n}\n.visible-xs-inline-block {\n @media (max-width: @screen-xs-max) {\n display: inline-block !important;\n }\n}\n\n.visible-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-visibility();\n }\n}\n.visible-sm-block {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: block !important;\n }\n}\n.visible-sm-inline {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: inline !important;\n }\n}\n.visible-sm-inline-block {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n display: inline-block !important;\n }\n}\n\n.visible-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-visibility();\n }\n}\n.visible-md-block {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: block !important;\n }\n}\n.visible-md-inline {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: inline !important;\n }\n}\n.visible-md-inline-block {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n display: inline-block !important;\n }\n}\n\n.visible-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-visibility();\n }\n}\n.visible-lg-block {\n @media (min-width: @screen-lg-min) {\n display: block !important;\n }\n}\n.visible-lg-inline {\n @media (min-width: @screen-lg-min) {\n display: inline !important;\n }\n}\n.visible-lg-inline-block {\n @media (min-width: @screen-lg-min) {\n display: inline-block !important;\n }\n}\n\n.hidden-xs {\n @media (max-width: @screen-xs-max) {\n .responsive-invisibility();\n }\n}\n.hidden-sm {\n @media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {\n .responsive-invisibility();\n }\n}\n.hidden-md {\n @media (min-width: @screen-md-min) and (max-width: @screen-md-max) {\n .responsive-invisibility();\n }\n}\n.hidden-lg {\n @media (min-width: @screen-lg-min) {\n .responsive-invisibility();\n }\n}\n\n\n// Print utilities\n//\n// Media queries are placed on the inside to be mixin-friendly.\n\n// Note: Deprecated .visible-print as of v3.2.0\n.visible-print {\n .responsive-invisibility();\n\n @media print {\n .responsive-visibility();\n }\n}\n.visible-print-block {\n display: none !important;\n\n @media print {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n\n @media print {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n\n @media print {\n display: inline-block !important;\n }\n}\n\n.hidden-print {\n @media print {\n .responsive-invisibility();\n }\n}\n","// Responsive utilities\n\n//\n// More easily include all the states for responsive-utilities.less.\n.responsive-visibility() {\n display: block !important;\n table& { display: table !important; }\n tr& { display: table-row !important; }\n th&,\n td& { display: table-cell !important; }\n}\n\n.responsive-invisibility() {\n display: none !important;\n}\n"]}
\ No newline at end of file
diff --git a/public/css/style.css b/public/css/style.css
index b8a9a3b..151c79e 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -4,10 +4,12 @@ body {
}
a {
- color: #00B7FF;
+ color: #00668f;
}
-
+h2#errormsg {
+ color: #FF0000;
+}
input.tinput {
background-color: #99FFCC;
@@ -28,5 +30,24 @@ input.tinput {
}
.estimate {
- background-color: #fff6f6;
+ background-color: #f1db8b;
+}
+
+/* a.button { */
+/* -webkit-appearance:button; */
+/* -moz-appearance: button; */
+/* appearance: button; */
+/* cursor:pointer; */
+/* } */
+
+.aidlink {
+ background-color: #c4c4c4;
+ color: #000;
+}
+a.aidlink:hover{
+ background: #26a69a ;
+}
+
+ul#trackerlinks li {
+ list-style-type: circle;
}
diff --git a/public/js/aidstation.js b/public/js/aidstation.js
index 150d4d7..5db8724 100644
--- a/public/js/aidstation.js
+++ b/public/js/aidstation.js
@@ -1,6 +1,7 @@
// aidstation.js
// handle aidstation input save/edit, display runnerlist table for each aid
+import {runnerList, rankedRunnerList, prepareResultsTable, genRankedList} from './process_results.js';
// DOM Ready =============================================================
$(document).ready(function() {
@@ -26,6 +27,8 @@ $(document).on('click', function (e) {
var inout;
var startnum;
var data;
+ //console.log('click target: ' + target);
+ console.log('aidId: ' + aidId);
if (target.is('.makeNonEditable')) {
e.preventDefault(); // cancel the event flow
@@ -54,7 +57,8 @@ $(document).on('click', function (e) {
// " @ aid=" + aidId + " time=" + time);
saveTimeClick(data);
- } else if (target.is('.makeEditable')) {
+ }
+ else if (target.is('.makeEditable')) {
e.preventDefault();
var editId = target.data('editid');
res = editId.split("_"); // t[in|out]_edit_NN
@@ -71,6 +75,20 @@ $(document).on('click', function (e) {
};
editTimeClick(data);
}
+ else if (target.is('.dnfOrReset')) {
+ e.preventDefault(); // cancel the event flow
+ var setId = target.data('setid');
+ res = setId.split("_"); // tin_[dnf|reset]_NN
+ dnf_reset = res[1]; // dnf|reset
+ startnum = res[2];
+
+ console.log('dnf_reset: ' + dnf_reset);
+ console.log('startnum: ' + startnum);
+ if (dnf_reset === 'reset')
+ resetResultsClick(startnum);
+ if (dnf_reset === 'dnf')
+ setDNFClick(startnum);
+ }
});
@@ -152,9 +170,19 @@ function saveTimeClick(data) {
}
// do somthing like update the table
// update();
+ // fixme: we need to trigger the function to generate the ranked list
+ // righ now this happens while the main result page is loaded
+ // this only reads and processes the timing data. but at that stage could
+ // put back the total finish time and rank into the database ... but it requires login!
+ if ('FINISH' === data.aid) {
+ console.log("Saving Finish time!");
+ prepareResultsTable(genRankedList);
+ console.log("ranked list from results.js : " + rankedRunnerList);
+ }
});
}
+// FIXME: consider changing to current date/time if edit is clicked!?
function editTimeClick(data) {
var editId = data.editId; // eg. tout_7, tin_1
var editRoId = data.editRoId; // toutro_7
@@ -198,7 +226,55 @@ function editTimeClick(data) {
});
}
+function resetResultsClick(startnum) {
+ var valid = /^\d{1,3}$/.test(startnum);
+ if (! valid) {
+ return false; // fixme handle error
+ }
+
+ console.log("trying to reset results for runner: " + startnum);
+ $.ajax({
+ type: 'PUT',
+ dataType: 'JSON',
+ data: {},
+ url: '/runners/resetresult/' + startnum
+ }).done(function(response) {
+ // Check for a successful (blank) response
+ if (response.msg === '') {
+ console.log("reset runner OK!");
+ alert('OK!');
+ }
+ else {
+ alert('Error: ' + response.msg);
+ }
+ });
+}
+
+function setDNFClick(startnum) {
+ var valid = /^\d{1,3}$/.test(startnum);
+ if (! valid) {
+ return false; // fixme handle error
+ }
+
+ console.log("trying to set DNF for runner: " + startnum);
+
+ $.ajax({
+ type: 'PUT',
+ dataType: 'JSON',
+ data: {},
+ url: '/runners/setdnf/' + startnum
+ }).done(function(response) {
+ // Check for a successful (blank) response
+ if (response.msg === '') {
+ console.log("DNF for runner OK!");
+ alert('OK!');
+ }
+ else {
+ alert('Error: ' + response.msg);
+ }
+ });
+}
// theDate is Date() object
function fillStarterTable(docTitle, theDate) {
@@ -206,9 +282,11 @@ function fillStarterTable(docTitle, theDate) {
var matchVP = /^(VP)\d\d?$/i;
var matchStart = /^START$/i;
var matchFinish = /^FINISH$/i;
+ var matchDNF = /^DNF$/i;
var isAidstation = matchVP.test(aidId);
var isStart = matchStart.test(aidId);
var isFinish = matchFinish.test(aidId);
+ var isDnf = matchDNF.test(aidId);
var tableContent = '';
var runnerNum = 1;
@@ -224,6 +302,9 @@ function fillStarterTable(docTitle, theDate) {
var outro = "0";
var inreadonly = "";
var outreadonly = "";
+ var inStyle = ""; //' style="background-color: #99FFCC" ';
+ var outStyle = ""; //' style="background-color: #99FFCC" ';
+ //var roStyle = ' style="background-color: #99FFCC" ';
var roStyle = "";
// check the results field if we have valid times for this runner/aid
@@ -239,15 +320,19 @@ function fillStarterTable(docTitle, theDate) {
indate = results[aidId].indate;
inro = "1";
inreadonly = "readonly";
- roStyle = ' style="background-color: #FF2F2F59" ';
+ inStyle = ' style="background-color: #FF2F2F59" ';
}
+ else
+ inStyle = ' style="background-color: #99FFCC" ';
if (true === results[aidId].outtime_valid) {
outtime = results[aidId].outtime; // FIXME: outtimeObj = new Date(results[aidId].outtime);
outdate = results[aidId].outdate;
outro = "1";
outreadonly = "readonly";
- roStyle = ' style="background-color: #FF2F2F59" ';
+ outStyle = ' style="background-color: #FF2F2F59" ';
}
+ else
+ outStyle = ' style="background-color: #99FFCC" ';
console.log('fillStarterTable (runner=' + this.startnum + '): ' + aidId + ' in valid: ' + results[aidId].intime_valid);
console.log('fillStarterTable (runner=' + this.startnum + '): ' + aidId + ' in time: ' + results[aidId].intime);
console.log('fillStarterTable (runner=' + this.startnum + '): ' + aidId + ' out valid: ' + results[aidId].outtime_valid);
@@ -260,12 +345,12 @@ function fillStarterTable(docTitle, theDate) {
tableContent += '
' + this.lastname + ' | ';
// FIXME: below maxlength/size is hardcoded to 10 to match date format of yyyy-mm-dd
- if (! isStart) {
+ if (! isStart && ! isDnf) {
tableContent += '
| ';
+ + indate + '" ' + inreadonly + inStyle + '/>';
tableContent += '
'
+ + '" class="tinput" type="time" value="' + intime + '" ' + inreadonly + inStyle + '>'
+ ' | ';
// FIXME: data- does not require set or edit info since we have the class already
tableContent += '
| ';
}
- if (! isFinish) {
+ if (! isFinish && ! isDnf) {
tableContent += '
| ';
+ + outdate + '" ' + outreadonly + outStyle + '/>';
tableContent += '
'
+ + '" class="tinput" type="time" value="' + outtime + '"' + outreadonly + outStyle + '>'
+ ' | ';
tableContent += '
Edit | ';
}
+ if (isDnf) {
+ tableContent += '
| ';
+
+ tableContent += '
'
+ + ' | ';
+ tableContent += '
DNF | ';
+ tableContent += '
RESET | ';
+ }
tableContent += '';
runnerNum++;
diff --git a/public/js/aidstationlist.js b/public/js/aidstationlist.js
new file mode 100644
index 0000000..a821270
--- /dev/null
+++ b/public/js/aidstationlist.js
@@ -0,0 +1,30 @@
+
+// DOM Ready =============================================================
+$(document).ready(function() {
+ genAidstationList();
+});
+
+
+function genAidstationList() {
+ var tableContent = '';
+ var aidNum = 1;
+
+ $.getJSON( '/aid', function(data) {
+ // For each item in our JSON, add a table row and cells to the content string
+ $.each(data, function() {
+ var osmLink = 'https://www.openstreetmap.org/?mlat=' + this.lat + '&mlon=' + this.lng + '#map=18/' + this.lat + '/' + this.lng
+ tableContent += '
';
+ tableContent += '' + this.name + ' | ';
+ tableContent += '' + this.directions + ' | ';
+ tableContent += '' + this.totalDistance + ' | ';
+ tableContent += '' + this.legDistance + ' | ';
+ tableContent += '' + this.lat + ', ' + this.lng + ' | ';
+ tableContent += '' + this.height + ' | ';
+ tableContent += '
';
+ aidNum++;
+ });
+
+ // Inject the whole content string into our existing HTML table
+ $('#aidstationListTable table tbody').html(tableContent);
+ });
+}
diff --git a/public/js/process_results.js b/public/js/process_results.js
new file mode 100644
index 0000000..b12158e
--- /dev/null
+++ b/public/js/process_results.js
@@ -0,0 +1,432 @@
+
+"use strict";
+
+// todo: consider one single export statement
+
+export let runnerList = {};
+export let rankedRunnerList = [];
+
+
+// validation
+export function isValidAid(aid) {
+ var reAid = /^(START|FINISH|VP\d{1,3})+$/i;
+ if (! reAid.test(aid)) {
+ return false;
+ }
+ return true;
+}
+
+export function isValidDate(time) {
+ var reTime = /^\d\d\d\d-\d\d-\d\d$/;
+ if (! reTime.test(time)) {
+ return false;
+ }
+ return true;
+}
+
+export function isValidTime(time) {
+ var reTime = /^\d\d:\d\d$/;
+ if (! reTime.test(time)) {
+ return false;
+ }
+ return true;
+}
+
+// calculation
+/*
+ * expect t to be a string "hh:mm", d is distance in km
+ */
+export function calcPace(t, d) {
+ var intime = t.split(':');
+ //console.log("intime h="+intime[0]+", intime m="+intime[1]);
+ if (isNaN(intime[0])) return "n/a";
+
+ var mins = parseInt(intime[0], 10) * 60 + parseInt(intime[1], 10); //intime[0] * 60 + intime[1];
+ var pace = mins / d;
+
+ var paceSec = Math.floor(60 * (pace - Math.floor(pace)));
+ var paceSecStr = paceSec < 10 ? "0" + paceSec : paceSec;
+ var result = Math.floor(pace) + ":" + paceSecStr;
+ return result;
+}
+
+export function calcTotalPause(totalp, delta) {
+ var d = delta.split(':');
+ var t = totalp.split(':');
+ var dMins = parseInt(d[0], 10) * 60 + parseInt(d[1], 10);
+ var tMins = parseInt(t[0], 10) * 60 + parseInt(t[1], 10);
+ var sum = tMins + dMins;
+
+ var result = String(100 + Math.floor(sum / 60)).substr(1) + ':' +
+ String(100 + sum % 60).substr(1);
+ return result;
+}
+
+export function calcTotalTime(totalDist, avgpace){
+ var p = avgpace.split(':'); // input pace "mm:ss"
+ var minsPerK = parseInt(p[0], 10) + (parseInt(p[1], 10) / 60);
+ var totalMins = Math.floor(minsPerK * totalDist);
+ //console.log("minsPerK: " + minsPerK + " min/km");
+ //console.log("totalMins: " + totalMins + " min");
+
+ var result = String(100 + Math.floor(totalMins / 60)).substr(1) + ':' +
+ String(100 + totalMins % 60).substr(1);
+
+ return result;
+}
+
+/*
+ * substractTimeDate2Str(outTime, outDate, intime, indate);
+ * substrace the in time/date from out date/time
+ * return difference as hours and minutes in the form "hh:mm"
+ * parm outTime
+ * parm outDate
+ * parm inTime
+ * parm inTime
+ */
+export function substractTimeDate2Str(outTime, outDate, inTime, inDate) {
+ var outD = new Date(outDate + " " + outTime);
+ var inD = new Date(inDate + " " + inTime);
+ var diffMin = ((inD - outD) / 1000) / 60;
+ //console.log(outD); console.log(inD);
+ var result = String(100 + Math.floor(diffMin / 60)).substr(1) + ':' +
+ String(100 + diffMin % 60).substr(1);
+
+ return result;
+}
+
+export function addTimeDate2Str(startTime, startDate, delta) { // start date/time + estTotalTime hh:mm
+ var dt = new Date(startDate + " " + startTime);
+ var d = delta.split(":");
+ var dMins = parseInt(d[0], 10) * 60 + parseInt(d[1], 10);
+ //console.log("addTimeDate2Str: start=" + dt + ", dMins=" + dMins);
+ // add minutes
+ var newD = new Date(dt.getTime() + dMins * 60000);
+ //console.log("addTimeDate2Str: intime=" + newD);
+ var newMinutes = newD.getMinutes() < 10 ? "0" + newD.getMinutes() : newD.getMinutes();
+ var result = newD.getHours() + ":" + newMinutes;
+ return result;
+}
+
+
+export function getAidstationNames(aid) {
+ var a = [], n;
+ for (n in aid) {
+ a.push(aid[n].name);
+ }
+ return a;
+}
+
+export function setRunnerList(num, aid, time) {
+ if (typeof runnerList[num] === 'undefined') {
+ runnerList[num] = {};
+ }
+ //runnerList[num].rank = 0;
+ runnerList[num].lastAidIn = aid;
+ runnerList[num].totalTime = time;
+}
+
+export function isFinisher(num) {
+ if ((typeof runnerList[num] !== 'undefined') &&
+ ('FINISH' === runnerList[num].lastAidIn.toUpperCase())) {
+ return true;
+ }
+ return false;
+}
+
+/*
+ *
+ */
+export function sortResultObject(o) {
+ var a = [], i;
+ for (i in o) {
+ if (o.hasOwnProperty(i)) {
+ a.push([i, o[i]]);
+ }
+ }
+
+ a.sort(function(a, b) {
+ var idA = a[0].toUpperCase();
+ var idB = b[0].toUpperCase();
+
+ if (('START' === idA) && ('FINISH' === idB))
+ return -1;
+
+ if (('START' === idB) && ('FINISH' === idA))
+ return 1;
+
+ if (('START' === idA) || ('FINISH' === idB))
+ return -1;
+
+ if (('START' === idB) || ('FINISH' === idA))
+ return 1;
+
+ return idA.localeCompare(idB, 'en', {numeric: true});
+ });
+ return a;
+}
+
+
+/*
+ *
+ */
+function setRankAndFinishtime(startnum) {
+ var ranking = {
+ 'rankall' : runnerList[startnum].rank,
+ 'finishtime' : runnerList[startnum].totalTime
+ //'startnum' : data.startnum,
+ //'rankcat' : data.rankcat,
+ };
+ console.log("setRankAndFinishtime, ranking: " + ranking);
+
+ $.ajax({
+ type: 'PUT',
+ dataType: 'JSON',
+ data: ranking,
+ url: '/runners/update/rank/' + startnum
+ }).done(function(response) {
+ // Check for a successful (blank) response
+ if (response.msg === '') {
+ console.log("update rank/time for runner " + data.startnum + " OK!");
+ }
+ else {
+ alert('Error: ' + response.msg);
+ }
+ });
+}
+
+export function genRankedList(o) {
+ var rankedList = [], i, n;
+ for (i in o) {
+ if (o.hasOwnProperty(i)) {
+ rankedList.push([i, o[i]]);
+ }
+ }
+ rankedList.sort(function(a, b) {
+ // we need FINISH before VPx
+ var idA = a[1].lastAidIn.toUpperCase() === 'FINISH' ? 'ZZZ' : a[1].lastAidIn.toUpperCase();
+ var idB = b[1].lastAidIn.toUpperCase() === 'FINISH' ? 'ZZZ' : b[1].lastAidIn.toUpperCase();
+
+ if (idA < idB) return 1;
+ if (idA > idB) return -1;
+ // same VP (aidstation name), check time
+ if (a[1].totalTime > b[1].totalTime) return 1; // totalTime format: "hh:mm"
+ if (a[1].totalTime < b[1].totalTime) return -1;
+ return 0;
+ });
+
+ // fill-out rank
+ for (n in rankedList) {
+ var startnum = rankedList[n][0];
+ var rank = parseInt(n) + 1;
+
+ rankedList[n][1]['rank'] = rank;
+ runnerList[startnum].rank = rank;
+
+ console.log("genrankedklist: startnum=" + startnum + ", totalTime=" + runnerList[startnum].totalTime
+ + ", rank=" + rank + ", isFinisher=" + runnerList[startnum].finisher);
+
+ // here we update the html result table FIXME: do this in result.js
+ // (which might need to be renamed to something like fillResuts.js)
+ if (null !== document.getElementById('rank_' + startnum)) {
+ document.getElementById('rank_' + startnum).innerHTML = rank;
+ }
+ else if (true === runnerList[startnum].finisher) {
+ // update database with rank and finisher time
+ setRankAndFinishtime(startnum);
+ }
+ }
+
+ // return rankedList;
+ rankedRunnerList = rankedList;
+}
+
+export function prepareResultsTable(callback) {
+ var aidStations = [];
+
+ // note: /aid returns data sorted by total distance
+ $.getJSON('/aid', function(data) {
+ $.each(data, function() {
+ //console.log("aid data: " + this.name);
+ if (!isValidAid(this.name)) {
+ console.log("invalid aid name!");
+ return false;
+ }
+ aidStations.push(this);
+ console.log("results.js: push to aidstations:" + this.name);
+ });
+ })
+ .then(function(aidstations) {
+ console.log("results.js: aidstations: " + aidStations); // aidStations are objects!
+
+ // FIXME check input validation (using database input)
+ $.getJSON('/runners', function(data) {
+ $.each(data, function() {
+ var intimeValid = false;
+ var outtimeValid = false;
+ var resultsList = [];
+ var intime = "n/a";
+ var indate = "n/a";
+ var outtime = "n/a";
+ var outdate = "n/a";
+ var pause = "n/a";
+ var lastpace = "n/a";
+ var avgpace = "n/a";
+ var lasttime = "n/a";
+ var totaltime = "n/a";
+ var finishTime = "n/a";
+ var rank = "n/a";
+ var totalpause = "0:00";
+ var curStarter = this.startnum;
+ var rankId = "rank_" + this.startnum;
+ var k;
+ var startTime, startDate;
+ //var aidEstimates = getAidstationNames(aidStations);
+ //console.log(aidEstimates);
+
+ console.log("=== checking runner #" + this.startnum + " ===");
+
+ // check the results field if we have valid times for this runner/aid
+ var results = this.results;
+ console.log(results);
+
+ // sort result list ... if VPn data for some reason was entered before VPn-1 (eg. entering data after the run)
+ resultsList = sortResultObject(results);
+ console.log('num results: ' + resultsList.length);
+
+ $.each(resultsList, function(index, res) {
+ var aidId = isValidAid(res[0]) ? res[0] : "INVALID"; // validate aidId
+ var times = res[1];
+ var prevAidIdx = aidStations.findIndex(x => x.name === aidId) - 1;
+
+ console.log("AIDID: " + aidId + ":");
+ console.log("index: " + index);
+ pause = "n/a";
+ if ("INVALID" === aidId) {
+ console.log('Error invalid data for runner: ' + curStarter);
+ return true;
+ }
+ // sanity check if number of results index of this aidstation (name)
+ if ((prevAidIdx + 1) != index) {
+ console.log('Error invalid result data for runner: ' + curStarter + ' (prevAidIdx: ' + prevAidIdx + ')');
+ return true;
+ }
+
+ // fixme: we might skip the estimate calculation here as we only need actual results to get the ranking
+ if (results[aidId]) {
+ //if ("FINISH" === aidId)
+ // return;
+ //aidEstimates.shift(); // remove this aidstation
+
+ // make sure valids are really only true or false
+ intimeValid = ((typeof results[aidId].intime_valid !== 'undefined')
+ && (true === results[aidId].intime_valid)) ? true : false;
+ outtimeValid = ((typeof results[aidId].outtime_valid !== 'undefined')
+ && (true === results[aidId].outtime_valid)) ? true : false;
+
+ if (true === intimeValid) {
+ intime = isValidTime(results[aidId].intime) ? results[aidId].intime : "n/a";
+ indate = isValidDate(results[aidId].indate) ? results[aidId].indate : "n/a";
+ }
+ else {
+ intime = "n/a";
+ }
+ if (true === outtimeValid) {
+ outtime = isValidTime(results[aidId].outtime) ? results[aidId].outtime : "n/a";
+ outdate = isValidDate(results[aidId].outdate) ? results[aidId].outdate : "n/a";
+ }
+ else {
+ outtime = "n/a";
+ }
+
+ console.log('fillResultTable: ' + aidId + ' in valid: ' + intimeValid);
+ console.log('fillResultTable: ' + aidId + ' in time: ' + intime);
+ console.log('fillResultTable: ' + aidId + ' in date: ' + indate);
+ console.log('fillResultTable: ' + aidId + ' out valid: ' + outtimeValid);
+ console.log('fillResultTable: ' + aidId + ' out time: ' + outtime);
+ console.log('fillResultTable: ' + aidId + ' out date: ' + outdate);
+
+ if ((true === outtimeValid) && (true === intimeValid)) {
+ pause = substractTimeDate2Str(intime, indate, outtime, outdate);
+ //console.log("pause=" + pause + ", total pause=" + totalpause);
+ var zzz = totalpause;
+ totalpause = calcTotalPause(zzz, pause);
+ console.log('calc total pause: ' + totalpause);
+ console.log('fillResultTable: ' + aidId + ' pause: ' + pause);
+ }
+
+ // get last time (between last aid out and this aid in)
+ //var prevAidIdx = aidStations.findIndex(x => x.name === aidId) - 1;
+ console.log('prevAidIdx: ' + prevAidIdx);
+ if (prevAidIdx >= 0) { // index=0 means START
+ var prevAid = aidStations[prevAidIdx];
+ console.log('prevAid.name=' + prevAid.name);
+ console.log('prev aid results: ' + results[prevAid.name]);
+ // FIXME: if results[prevAid.name] == undefined then this result cannot be valid either
+
+ if ((typeof prevAid !== 'undefined') &&
+ (typeof results[prevAid.name] !== 'undefined') &&
+ (true === results[prevAid.name].outtime_valid) &&
+ (true === results[aidId].intime_valid)) {
+ // FIXME: check if prev time valid, validate time
+ var prevOutTime = isValidTime(results[prevAid.name].outtime) ? results[prevAid.name].outtime : "n/a";
+ var prevOutDate = isValidDate(results[prevAid.name].outdate) ? results[prevAid.name].outdate : "n/a";
+ startTime = isValidTime(results["START"].outtime) ? results["START"].outtime : "n/a";
+ startDate = isValidDate(results["START"].outdate) ? results["START"].outdate : "n/a";
+
+ console.log('last out time: ' + prevOutTime);
+ console.log('last out date: ' + prevOutDate);//results[prevAid.name].outdate);
+ console.log('this in time: ' + intime);
+ console.log('this in date: ' + indate);
+ console.log('this out time: ' + outtime);
+ console.log('this out date: ' + outdate);
+ console.log('start time: ' + startTime);
+ console.log('start date: ' + startDate);
+
+ lasttime = substractTimeDate2Str(prevOutTime, prevOutDate, intime, indate);
+ // total time includes pause time at aidstation once the out-time is valid (saved)
+ if (true === outtimeValid)
+ totaltime = substractTimeDate2Str(startTime, startDate, outtime, outdate);
+ else
+ totaltime = substractTimeDate2Str(startTime, startDate, intime, indate);
+
+ setRunnerList(curStarter, aidId, totaltime);
+ //console.log(runnerList);
+ }
+ else {
+ lasttime = "n/a";
+ totaltime = "n/a";
+ }
+ }
+ // calc pace ...
+ // P1: avg. pace between aid stations
+ var lastDist = aidStations.find(x => x.name === aidId).legDistance;
+ var totalDist = aidStations.find(x => x.name === aidId).totalDistance;
+ lastpace = calcPace(lasttime, lastDist);
+
+ // P2: avg between start and current aidstation in or out
+ avgpace = calcPace(totaltime, totalDist);
+ }
+
+ if ("START" === aidId) {
+ //tableContent += '
' + outtime + ' | ';
+ return true;
+ }
+ if ("FINISH" === aidId) {
+ return true;
+ }
+ return true;
+ });
+
+ if (isFinisher(curStarter)) {
+ runnerList[curStarter].finisher = true;
+ }
+
+ }); // end each
+
+ rankedRunnerList = callback(runnerList);
+ console.log("rankedRunnerList:");
+ console.log(rankedRunnerList);
+ });
+ });
+}
diff --git a/public/js/results.js b/public/js/results.js
index 9179ed0..623ee8a 100644
--- a/public/js/results.js
+++ b/public/js/results.js
@@ -1,11 +1,6 @@
// results.js display leaderboard
// calculate pace and avg. pace, ...
-// TODO:
-// for certificate store rank and finish time ...
-// 'rank_cat' => 1,
-// 'rank_all' => 1,
-// 'finish_time' => 1
// TODO: clean up this mess!!
// probably could "outsource" functions into module
@@ -16,13 +11,31 @@
"use strict";
-var finisher = {};
-var runnerList = {};
-var rankedRunnerList = [];
+import {runnerList,
+ rankedRunnerList,
+ genRankedList,
+ isValidAid,
+ isValidDate,
+ isValidTime,
+ calcPace,
+ calcTotalPause,
+ calcTotalTime,
+ substractTimeDate2Str,
+ addTimeDate2Str,
+ sortResultObject,
+ setRunnerList,
+ isFinisher,
+ getAidstationNames
+ } from './process_results.js';
+
// DOM Ready =============================================================
$(document).ready(function() {
//var now = date();
+ showLiveTrackerLinks();
+
+ // showAidstationLinks(); // depending on logged in status!?
+ // NOTE: right now I implemented this via the results.js route and results.pug template
if (document.title === "results not found") {
alert("no results found!");
@@ -34,9 +47,7 @@ $(document).ready(function() {
var tableObj = document.getElementById('results-table');
sorttable.makeSortable(tableObj);
- //rankedRunnerList = genRankedList(runnerList); not yet available ...
- //console.log("rankedRunnerList:");
- //console.log(rankedRunnerList);
+ //rankedRunnerList = genRankedList(runnerList); not yet available ... therefore using a callback for now
}
// $("td#rank_").each(function( index ) {
@@ -45,234 +56,86 @@ $(document).ready(function() {
});
+function showLiveTrackerLinks() {
-/*
- * sort runnerList by aidId and time, fill in rank
- * return sorted array
- */
-// fixme: callback ...
-function fillRankId() {
-
-}
-// fixme: split function, eg. rank table cell update as extra function
-function genRankedList(o) {
- var a = [], i, n;
- for (i in o) {
- if (o.hasOwnProperty(i)) {
- a.push([i, o[i]]);
+ $.getJSON('/tracking', function(data) {
+ if (data === undefined || typeof data == 'undefined' ||
+ data.length === 0) {
+ return false;
}
- }
- a.sort(function(a, b) {
- // we need FINISH before VPx
- var idA = a[1].lastAidIn.toUpperCase() === 'FINISH' ? 'ZZZ' : a[1].lastAidIn.toUpperCase();
- var idB = b[1].lastAidIn.toUpperCase() === 'FINISH' ? 'ZZZ' : b[1].lastAidIn.toUpperCase();
-
- if (idA < idB) return 1;
- if (idA > idB) return -1;
- // same VP (aidstation name), check time
- if (a[1].totalTime > b[1].totalTime) return 1; // totalTime format: "hh:mm"
- if (a[1].totalTime < b[1].totalTime) return -1;
- return 0;
- });
- // fill-out rank
- for (n in a) {
- var startnum = a[n][0];
- var rank = parseInt(n) + 1;
+ console.log("data:");
+ console.log(data.length);
+ console.log(data);
+ $("h3#trackerlinks").text("live tracking links:");
+
+ $.each(data, function() { // anonymous function so return will act as 'next'
+ console.log("tracking link name: " + this.name);
+ console.log("tracking link url: " + this.url);
+ if (this.name === undefined || this.url === undefined) {
+ console.log("error: no name/url provided");
+ return;
+ }
- a[n][1]['rank'] = rank;
- runnerList[startnum].rank = rank;
- console.log("genrankedklist: startnum=" + startnum + ", rank=" + rank);
- document.getElementById('rank_' + startnum).innerHTML = rank;
- //console.log("rank td#rank_" + startnum + ", " + document.getElementById('rank_' + startnum).innerHTML);
- }
+ // Canonical Decomposition:
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
+ let name = this.name.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
- return a;
+ if (!isValidName(name)) {
+ console.log("invalid tracking link name!");
+ return;
+ }
+ if (!isValidUrl(this.url)) {
+ console.log("invalid tracking link url!");
+ return;
+ }
+ // populate ul with live tracking links if available
+ let li_item = '
' + name + '';
+ console.log("
: " + li_item);
+ $("ul#trackerlinks").append(li_item);
+ });
+
+ });
}
+//function showAidstationLinks() {
+//}
-function setRunnerList(num, aid, time) {
- if (typeof runnerList[num] === 'undefined') {
- runnerList[num] = {};
- }
- runnerList[num].rank = 0;
- runnerList[num].lastAidIn = aid;
- runnerList[num].totalTime = time;
-}
+/*
+ * sort runnerList by aidId and time, fill in rank
+ * return sorted array
+ */
+// fixme: callback ...
+function fillRankId() {
-function isFinisher(num) {
- if ((typeof runnerList[num] !== 'undefined') &&
- ('FINISH' === runnerList[num].lastAidIn.toUpperCase())) {
- return true;
- }
- return false;
}
-function isValidAid(aid) {
- var reAid = /^(START|FINISH|VP\d{1,3})+$/i;
- if (! reAid.test(aid)) {
+
+function isValidUrl(url) {
+ var valid = /^(http|https):\/\/[^ "]+$/.test(url);
+ if (! valid) {
return false;
}
return true;
}
-function isValidDate(time) {
- var reTime = /^\d\d\d\d-\d\d-\d\d$/;
- if (! reTime.test(time)) {
+function isValidName(name) {
+ var valid = /^[\w _-]+$/.test(name);
+ if (! valid) {
return false;
}
return true;
}
-function isValidTime(time) {
- var reTime = /^\d\d:\d\d$/;
- if (! reTime.test(time)) {
+function isValidNum(num) {
+ var valid = /^\d{1,3}$/.test(num);
+ if (! valid) {
return false;
}
return true;
}
-function paceStr2Min(p) {
-
-}
-
-// expect t to be a string "hh:mm", d is distance in km
-function calcPace(t, d) {
- var intime = t.split(':');
- //console.log("intime h="+intime[0]+", intime m="+intime[1]);
- if (isNaN(intime[0])) return "n/a";
-
- var mins = parseInt(intime[0], 10) * 60 + parseInt(intime[1], 10); //intime[0] * 60 + intime[1];
- var pace = mins / d;
-
- var paceSec = Math.floor(60 * (pace - Math.floor(pace)));
- var paceSecStr = paceSec < 10 ? "0" + paceSec : paceSec;
- var result = Math.floor(pace) + ":" + paceSecStr;
- return result;
-}
-/*
- * substractTimeDate2Str(outTime, outDate, intime, indate);
- * substrace the in time/date from out date/time
- * return difference as hours and minutes in the form "hh:mm"
- * parm outTime
- * parm outDate
- * parm inTime
- * parm inTime
- */
-function substractTimeDate2Str(outTime, outDate, inTime, inDate) {
- var outD = new Date(outDate + " " + outTime);
- var inD = new Date(inDate + " " + inTime);
- var diffMin = ((inD - outD) / 1000) / 60;
- //console.log(outD); console.log(inD);
- var result = String(100 + Math.floor(diffMin / 60)).substr(1) + ':' +
- String(100 + diffMin % 60).substr(1);
-
- return result;
-}
-
-function addTimeDate2Str(startTime, startDate, delta) { // start date/time + estTotalTime hh:mm
- var dt = new Date(startDate + " " + startTime);
- var d = delta.split(":");
- var dMins = parseInt(d[0], 10) * 60 + parseInt(d[1], 10);
- //console.log("addTimeDate2Str: start=" + dt + ", dMins=" + dMins);
- // add minutes
- var newD = new Date(dt.getTime() + dMins * 60000);
- //console.log("addTimeDate2Str: intime=" + newD);
- var newMinutes = newD.getMinutes() < 10 ? "0" + newD.getMinutes() : newD.getMinutes();
- var result = newD.getHours() + ":" + newMinutes;
- return result;
-}
-
-function calcTotalPause(totalp, delta) {
- var d = delta.split(':');
- var t = totalp.split(':');
- var dMins = parseInt(d[0], 10) * 60 + parseInt(d[1], 10);
- var tMins = parseInt(t[0], 10) * 60 + parseInt(t[1], 10);
- var sum = tMins + dMins;
-
- var result = String(100 + Math.floor(sum / 60)).substr(1) + ':' +
- String(100 + sum % 60).substr(1);
- return result;
-}
-
-function calcTotalTime(totalDist, avgpace){
- var p = avgpace.split(':'); // input pace "mm:ss"
- var minsPerK = parseInt(p[0], 10) + (parseInt(p[1], 10) / 60);
- var totalMins = Math.floor(minsPerK * totalDist);
- //console.log("minsPerK: " + minsPerK + " min/km");
- //console.log("totalMins: " + totalMins + " min");
-
- var result = String(100 + Math.floor(totalMins / 60)).substr(1) + ':' +
- String(100 + totalMins % 60).substr(1);
-
- return result;
-}
-
-/*
- *
- */
-function sortResultObject(o) {
- var a = [], i;
- for (i in o) {
- if (o.hasOwnProperty(i)) {
- a.push([i, o[i]]);
- }
- }
- a.sort(function(a, b) {
- var idA = a[0].toUpperCase();
- var idB = b[0].toUpperCase();
-
- if ((idA < idB) || ('FINISH' === idB)) {
- return -1;
- }
- if ((idA > idB) || ('FINISH' === idA)) {
- return 1;
- }
- return 0; // should not happen!
- });
- return a;
-}
-
-
-/*
- *
- */
-function setRankAndFinishtime(data) {
-
- var ranking = {
- 'startnum' : data.startnum,
- 'rankall' : data.rankall,
- 'rankcat' : rankcat,
- 'finishtime' : finishtime
- };
-
- $.ajax({
- type: 'PUT',
- dataType: 'JSON',
- data: ranking,
- url: '/runners/update/rank/' + data.startnum
- }).done(function(response) {
- // Check for a successful (blank) response
- if (response.msg === '') {
- console.log("update rank/time for runner " + data.startnum + " OK!");
- }
- else {
- alert('Error: ' + response.msg);
- }
-
- });
-
-}
-
-// function fillEstimatedTime() { ... }
-function getAidstationNames(aid) {
- var a = [], n;
- for (n in aid) {
- a.push(aid[n].name);
- }
- return a;
-}
// FIXME: quite some large piece of function code...
@@ -288,7 +151,8 @@ T1(hh:mm): Zeit zwischen VPn-1Tout und VPn
T2(hh:mm): Zeit zwischen Start und VPnTin, \
P1(mm:ss/km): Ø Pace zwischen VPn-1Tout und VPnTin, \
P2(mm:ss/km): Ø Pace zwischen Start und VPnTin, \
-kursiv (roter Hintergrund) Hochrechnung basierend auf avg. pace';
+kursiv (roter Hintergrund) Hochrechnung basierend auf avg. pace. \
+
Note: T2 und P2 basieren entweder auf der IN-Zeit oder wenn vorhanden auf der OUT-Zeit (pause)!';
tableHeader += '';
tableHeader += 'Rang | ';
@@ -307,245 +171,294 @@ kursiv (roter Hintergrund) Hochrechnung basierend auf avg. pace';
return false;
}
aidStations.push(this);
+ console.log("results.js: push to aidstations:" + this.name);
+ var osmLink = 'https://www.openstreetmap.org/?mlat=' + this.lat + '&mlon=' + this.lng + '#map=18/' + this.lat + '/' + this.lng
+ console.log("osmlink: " + osmLink);
if ('START' === this.name) { return true; }
if ('FINISH' === this.name) {
- // colspan number of cells in finish coloumn: in, last pace, avg. pace, last time
- tableHeader += '' + this.name + ' ' + this.directions +
- ', @' + this.totalDistance.toFixed(1) + ', Δ ' + this.legDistance.toFixed(1) + ' | ';
+ // colspan number of cells in finish coloumn: in, last pace, avg. pace, last time
+ tableHeader += '' + this.name + ' ' + this.directions +
+ ', @' + this.totalDistance.toFixed(1) + ', Δ ' + this.legDistance.toFixed(1) + ' | ';
return true;
}
- tableHeader += '' + this.name + ' '
+ tableHeader += ' | ' + '' + this.name + ' '
+ this.directions + ', @km ' + this.totalDistance.toFixed(1) + ', Δ ' + this.legDistance.toFixed(1) + ' | ';
return true;
});
- });
- console.log(aidStations);
- // FIXME check input validation (using database input)
- $.getJSON('/runners', function(data) {
- $.each(data, function() {
- var intimeValid = false;
- var outtimeValid = false;
- var resultsList = [];
- var intime = "n/a";
- var indate = "n/a";
- var outtime = "n/a";
- var outdate = "n/a";
- var pause = "n/a";
- var lastpace = "n/a";
- var avgpace = "n/a";
- var lasttime = "n/a";
- var totaltime = "n/a";
- var rank = "n/a";
- var totalpause = "0:00";
- var curStarter = this.startnum;
- var rankId = "rank_" + this.startnum;
- var k;
- var startTime, startDate;
- var aidEstimates = getAidstationNames(aidStations);
- console.log(aidEstimates);
-
- tableContent += '
';
- tableContent += '' + rank + ' | '; // rank
- tableContent += '' + this.startnum + ' | ';
- tableContent += '' + this.firstname + ' ' + this.lastname + ' | ';
-
- console.log("=== checking runner #" + this.startnum + " ===");
-
- // check the results field if we have valid times for this runner/aid
- var results = this.results;
- //console.log(results);
-
- // sort result list ... if VPn data for some reason was entered before VPn-1 (eg. entering data after the run)
- resultsList = sortResultObject(results);
-
- $.each(resultsList, function(index, res) {
- var aidId = isValidAid(res[0]) ? res[0] : "INVALID"; // validate aidId
- var times = res[1];
-
- console.log("AIDID: " + aidId + ":"); //console.log(times); // note: only log single obj to view in chrome dev tool
- pause = "n/a";
-
- if (results[aidId]) {
- aidEstimates.shift(); // remove this aidstation
-
- // make sure valids are really only true or false
- intimeValid = ((typeof results[aidId].intime_valid !== 'undefined')
- && (true === results[aidId].intime_valid)) ? true : false;
- outtimeValid = ((typeof results[aidId].outtime_valid !== 'undefined')
- && (true === results[aidId].outtime_valid)) ? true : false;
-
- if (true === intimeValid) {
- intime = isValidTime(results[aidId].intime) ? results[aidId].intime : "n/a";
- indate = isValidDate(results[aidId].indate) ? results[aidId].indate : "n/a";
- }
- else {
- intime = "n/a";
- }
- if (true === outtimeValid) {
- outtime = isValidTime(results[aidId].outtime) ? results[aidId].outtime : "n/a";
- outdate = isValidDate(results[aidId].outdate) ? results[aidId].outdate : "n/a";
+ })
+ .then(function(aidstations) {
+ console.log("results.js: aidstations: " + aidStations); // aidStations are objects!
+
+ // FIXME check input validation (using database input)
+ $.getJSON('/runners', function(data) {
+ $.each(data, function() {
+ var intimeValid = false;
+ var outtimeValid = false;
+ var resultsList = [];
+ var intime = "n/a";
+ var indate = "n/a";
+ var outtime = "n/a";
+ var outdate = "n/a";
+ var pause = "n/a";
+ var lastpace = "n/a";
+ var avgpace = "n/a";
+ var lasttime = "n/a";
+ var totaltime = "n/a";
+ var finishTime = "n/a";
+ var rank = "n/a";
+ var totalpause = "0:00";
+ var curStarter = this.startnum;
+ var rankId = "rank_" + this.startnum;
+ var k;
+ var startTime, startDate;
+ var aidEstimates = getAidstationNames(aidStations);
+ console.log(aidEstimates);
+
+ tableContent += '
';
+ tableContent += '' + rank + ' | '; // rank
+ tableContent += '' + this.startnum + ' | ';
+ tableContent += '' + this.firstname + ' ' + this.lastname + ' | ';
+
+ console.log("=== checking runner #" + this.startnum + " ===");
+
+ // check the results field if we have valid times for this runner/aid
+ var results = this.results;
+ //console.log(results);
+
+ // sort result list ... if VPn data for some reason was entered before VPn-1 (eg. entering data after the run)
+ resultsList = sortResultObject(results);
+ console.log('num results: ' + resultsList.length);
+
+ $.each(resultsList, function(index, res) {
+ var aidId = isValidAid(res[0]) ? res[0] : "INVALID"; // validate aidId
+ var times = res[1];
+ var prevAidIdx = aidStations.findIndex(x => x.name === aidId) - 1;
+
+ console.log("AIDID: " + aidId + ":"); //console.log(times); // note: only log single obj to view in chrome dev tool
+ console.log("index: " + index);
+ pause = "n/a";
+ if ("INVALID" === aidId) {
+ console.log('Error invalid data for runner: ' + curStarter);
+ return true;
}
- else {
- outtime = "n/a";
+ // sanity check if number of results index of this aidstation (name)
+ if ((prevAidIdx + 1) != index) {
+ console.log('Error invalid result data for runner: ' + curStarter + ' (prevAidIdx: ' + prevAidIdx + ')');
+ return true;
}
- console.log('fillStarterTable: ' + aidId + ' in valid: ' + intimeValid);
- console.log('fillStarterTable: ' + aidId + ' in time: ' + intime);
- console.log('fillStarterTable: ' + aidId + ' in date: ' + indate);
- console.log('fillStarterTable: ' + aidId + ' out valid: ' + outtimeValid);
- console.log('fillStarterTable: ' + aidId + ' out time: ' + outtime);
- console.log('fillStarterTable: ' + aidId + ' out date: ' + outdate);
+ if (results[aidId]) {
+ //if ("FINISH" === aidId)
+ // return;
+ aidEstimates.shift(); // remove this aidstation (fixme: only if outtime?)
+ // make sure valids are really only true or false
+ intimeValid = ((typeof results[aidId].intime_valid !== 'undefined')
+ && (true === results[aidId].intime_valid)) ? true : false;
+ outtimeValid = ((typeof results[aidId].outtime_valid !== 'undefined')
+ && (true === results[aidId].outtime_valid)) ? true : false;
- if ((true === outtimeValid) && (true === intimeValid)) {
- pause = substractTimeDate2Str(intime, indate, outtime, outdate);
- //console.log("pause=" + pause + ", total pause=" + totalpause);
- var zzz = totalpause;
- totalpause = calcTotalPause(zzz, pause);
- console.log('calc total pause: ' + totalpause);
- console.log('fillStarterTable: ' + aidId + ' pause: ' + pause);
- }
+ // if (outtimeValid || (("FINISH" === aidId) && intimeValid)) {
+ // aidEstimates.shift(); // remove this aidstation, only if outtime
+ // }
- // get last time (between last aid out and this aid in)
- var prevAidIdx = aidStations.findIndex(x => x.name === aidId) - 1;
- console.log('prevAidIdx: ' + prevAidIdx);
- if (prevAidIdx >= 0) { // index=0 means START
- var prevAid = aidStations[prevAidIdx];
- console.log('prevAid.name=' + prevAid.name);
- console.log('prev aid results: ' + results[prevAid.name]);
- if ((typeof prevAid !== 'undefined') &&
- (typeof results[prevAid.name] !== 'undefined') &&
- (true === results[prevAid.name].outtime_valid) &&
- (true === results[aidId].intime_valid)) {
- // FIXME: check if prev time valid, validate time
- var prevOutTime = isValidTime(results[prevAid.name].outtime) ? results[prevAid.name].outtime : "n/a";
- var prevOutDate = isValidDate(results[prevAid.name].outdate) ? results[prevAid.name].outdate : "n/a";
- startTime = isValidTime(results["START"].outtime) ? results["START"].outtime : "n/a";
- startDate = isValidDate(results["START"].outdate) ? results["START"].outdate : "n/a";
-
- console.log('last out time: ' + prevOutTime);
- console.log('last out date: ' + prevOutDate);//results[prevAid.name].outdate);
- console.log('this in time: ' + intime);
- console.log('this in date: ' + indate);
- console.log('start time: ' + startTime);
- console.log('start date: ' + startDate);
-
- lasttime = substractTimeDate2Str(prevOutTime, prevOutDate, intime, indate);
- totaltime = substractTimeDate2Str(startTime, startDate, intime, indate);
- setRunnerList(curStarter, aidId, totaltime);
- //console.log(runnerList);
+ if (true === intimeValid) {
+ intime = isValidTime(results[aidId].intime) ? results[aidId].intime : "n/a";
+ indate = isValidDate(results[aidId].indate) ? results[aidId].indate : "n/a";
}
else {
- lasttime = "n/a";
- totaltime = "n/a";
+ intime = "n/a";
}
+ if (true === outtimeValid) {
+ outtime = isValidTime(results[aidId].outtime) ? results[aidId].outtime : "n/a";
+ outdate = isValidDate(results[aidId].outdate) ? results[aidId].outdate : "n/a";
+ }
+ else {
+ outtime = "n/a";
+ }
+
+ console.log('fillResultTable: ' + aidId + ' in valid: ' + intimeValid);
+ console.log('fillResultTable: ' + aidId + ' in time: ' + intime);
+ console.log('fillResultTable: ' + aidId + ' in date: ' + indate);
+ console.log('fillResultTable: ' + aidId + ' out valid: ' + outtimeValid);
+ console.log('fillResultTable: ' + aidId + ' out time: ' + outtime);
+ console.log('fillResultTable: ' + aidId + ' out date: ' + outdate);
+
+ if ((true === outtimeValid) && (true === intimeValid)) {
+ pause = substractTimeDate2Str(intime, indate, outtime, outdate);
+ //console.log("pause=" + pause + ", total pause=" + totalpause);
+ var zzz = totalpause;
+ totalpause = calcTotalPause(zzz, pause);
+ console.log('calc total pause: ' + totalpause);
+ console.log('fillResultTable: ' + aidId + ' pause: ' + pause);
+ }
+
+ // get last time (between last aid out and this aid in)
+ //var prevAidIdx = aidStations.findIndex(x => x.name === aidId) - 1;
+ console.log('prevAidIdx: ' + prevAidIdx);
+ if (prevAidIdx >= 0) { // index=0 means START
+ var prevAid = aidStations[prevAidIdx];
+ console.log('prevAid.name=' + prevAid.name);
+ console.log('prev aid results: ' + results[prevAid.name]);
+ // FIXME: if results[prevAid.name] == undefined then this result cannot be valid either
+
+ if ((typeof prevAid !== 'undefined') &&
+ (typeof results[prevAid.name] !== 'undefined') &&
+ (true === results[prevAid.name].outtime_valid) &&
+ (true === results[aidId].intime_valid)) {
+ // FIXME: check if prev time valid, validate time
+ var prevOutTime = isValidTime(results[prevAid.name].outtime) ? results[prevAid.name].outtime : "n/a";
+ var prevOutDate = isValidDate(results[prevAid.name].outdate) ? results[prevAid.name].outdate : "n/a";
+ startTime = isValidTime(results["START"].outtime) ? results["START"].outtime : "n/a";
+ startDate = isValidDate(results["START"].outdate) ? results["START"].outdate : "n/a";
+
+ console.log('last out time: ' + prevOutTime);
+ console.log('last out date: ' + prevOutDate);//results[prevAid.name].outdate);
+ console.log('this in time: ' + intime);
+ console.log('this in date: ' + indate);
+ console.log('this out time: ' + outtime);
+ console.log('this out date: ' + outdate);
+ console.log('start time: ' + startTime);
+ console.log('start date: ' + startDate);
+
+ lasttime = substractTimeDate2Str(prevOutTime, prevOutDate, intime, indate);
+ // total time includes pause time at aidstation once the out-time is valid (saved)
+ if (true === outtimeValid)
+ totaltime = substractTimeDate2Str(startTime, startDate, outtime, outdate);
+ else
+ totaltime = substractTimeDate2Str(startTime, startDate, intime, indate);
+
+ setRunnerList(curStarter, aidId, totaltime);
+ //console.log(runnerList);
+ }
+ else {
+ lasttime = "n/a";
+ totaltime = "n/a";
+ }
+ }
+ // calc pace ...
+ // P1: avg. pace between aid stations
+ var lastDist = aidStations.find(x => x.name === aidId).legDistance;
+ var totalDist = aidStations.find(x => x.name === aidId).totalDistance;
+ //console.log("lasttime=" + lasttime + ", totaltime=" + totaltime);
+ //console.log("lastDist=" + lastDist + ", totalDist=" + totalDist);
+ lastpace = calcPace(lasttime, lastDist);
+ // P2: avg between start and current aidstation in or out
+ avgpace = calcPace(totaltime, totalDist);
}
- // calc pace ...
- // P1: avg. pace between aid stations
- var lastDist = aidStations.find(x => x.name === aidId).legDistance;
- var totalDist = aidStations.find(x => x.name === aidId).totalDistance;
- //console.log("lasttime=" + lasttime + ", totaltime=" + totaltime);
- //console.log("lastDist=" + lastDist + ", totalDist=" + totalDist);
- lastpace = calcPace(lasttime, lastDist);
- // P2: avg between start and current aidstation in
- avgpace = calcPace(totaltime, totalDist);
- }
- if ("START" === aidId) {
- tableContent += '' + outtime + ' | ';
- return true;
- }
- if ("FINISH" === aidId) {
- tableContent += '' + intime + ' | ';
- tableContent += '' + lasttime + ' | ';
- tableContent += '' + totaltime + ' | '; // -> Ziel/Gesamtzeit
- tableContent += '' + lastpace + ' | ';
- tableContent += '' + avgpace + ' | ';
+
+ if ("START" === aidId) {
+ tableContent += '' + outtime + ' | ';
+ return true;
+ }
+ if ("FINISH" === aidId) {
+ finishTime = totaltime;
+ tableContent += '' + intime + ' | ';
+ tableContent += '' + lasttime + ' | ';
+ tableContent += '' + totaltime + ' | '; // -> Ziel/Gesamtzeit
+ tableContent += '' + lastpace + ' | ';
+ tableContent += '' + avgpace + ' | ';
+ return true;
+ }
+ tableContent += '' + intime + ' | ';
+ tableContent += '' + outtime + ' | ';
+ tableContent += '' + pause + ' | ';
+ tableContent += '' + lasttime + ' | ';
+ tableContent += '' + totaltime + ' | ';
+ tableContent += '' + lastpace + ' | ';
+ tableContent += '' + avgpace + ' | ';
return true;
+ });
+
+ // iterate over remaining aidstations/finish for this runner
+ // and fill in estimated in-time and total-time (T2)
+ // estimated arrival at next VPs and Finish with current avg. pace
+ console.log("last avg pace = " + avgpace);
+ var atFinish = false;
+ for (k in aidEstimates) {
+ console.log("--- ESTIMATE for aid: " + aidEstimates[k]);
+ if ('START' === aidEstimates[k]) break;
+ if ('FINISH' === aidEstimates[k]) {
+ atFinish = true;
+ }
+ var aidIdx = aidStations.findIndex(x => x.name === aidEstimates[k]);
+ var thisTotalDist = aidStations[aidIdx].totalDistance;
+ var estTotalTime = calcTotalTime(thisTotalDist, avgpace); //(min/km) -> min
+ var estIntime = addTimeDate2Str(startTime, startDate, estTotalTime); // start date/time + estTotalTime hh:mm
+
+ console.log("aidIdx=" + aidIdx);
+ console.log("totaldist=" + thisTotalDist);
+ console.log("estTotalTime=" + estTotalTime);
+ console.log('total pause: ' + totalpause);
+ console.log("Arrive at FINISH ..." + atFinish);
+
+ tableContent += '' + estIntime + ' | ';
+ if (false === atFinish) { // out and pause fields not present at finish
+ tableContent += ' | ';
+ tableContent += ' | ';
+ }
+ tableContent += ' | ';
+ tableContent += '' + estTotalTime + ' | ';
+ tableContent += ' | ';
+ tableContent += ' | ';
+ if (true === atFinish) { // out and pause fields not present at finish
+ tableContent += ' | ';
+ tableContent += ' | ';
+ }
}
- tableContent += '' + intime + ' | ';
- tableContent += '' + outtime + ' | ';
- tableContent += '' + pause + ' | ';
- tableContent += '' + lasttime + ' | ';
- tableContent += '' + totaltime + ' | ';
- tableContent += '' + lastpace + ' | ';
- tableContent += '' + avgpace + ' | ';
- return true;
- });
-
- // iterate over remaining aidstations/finish for this runner
- // and fill in estimated in-time and total-time (T2)
- // estimated arrival at next VPs and Finish with current avg. pace
- console.log("last avg pace = " + avgpace);
- for (k in aidEstimates) {
- console.log("estimate for aid: " + aidEstimates[k]);
- if ('START' === aidEstimates[k]) break;
- var aidIdx = aidStations.findIndex(x => x.name === aidEstimates[k]);
- var thisTotalDist = aidStations[aidIdx].totalDistance;
- console.log("totaldist=" + thisTotalDist);
- var estTotalTime = calcTotalTime(thisTotalDist, avgpace); //(min/km) -> min
- console.log("estTotalTime=" + estTotalTime);
-
-
- var estIntime = addTimeDate2Str(startTime, startDate, estTotalTime); // start date/time + estTotalTime hh:mm
-
- tableContent += '' + estIntime + ' | ';
- tableContent += ' | ';
- tableContent += ' | ';
- tableContent += ' | ';
- tableContent += '' + estTotalTime + ' | ';
- tableContent += ' | ';
- tableContent += ' | ';
- }
-
- if (isFinisher(curStarter)) {
- tableContent += '' + totaltime + ' | '; // totaltime
- tableContent += '' + totalpause + ' | '; // totalpause
- }
- tableContent += '
';
+ if (isFinisher(curStarter)) {
+ runnerList[curStarter].finisher = true;
+ tableContent += '' + totaltime + ' | '; // totaltime
+ tableContent += '' + totalpause + ' | '; // totalpause
+ tableContent += 'PDF | ';
+ }
+ tableContent += '';
- });
+ }); // end each
- // Inject the whole content string into our existing HTML table
- tableHeader += 'Zeit | ';
- tableHeader += '∑ Pause | ';
- tableHeader += '';
-
- // second header line ...
- tableHeader += '';
- tableHeader += ' | ';
- tableHeader += ' | ';
- tableHeader += ' | ';
- tableHeader += ' | ';
- $.each(aidStations, function() {
- if ('START' === this.name) { return true; }
- if ('FINISH' === this.name) { return true; }
+ // Inject the whole content string into our existing HTML table
+ tableHeader += 'Zeit | ';
+ tableHeader += '∑ Pause | ';
+ tableHeader += '↓ Urkunde | ';
+ tableHeader += '
';
+
+ // second header line ...
+ tableHeader += '';
+ tableHeader += ' | ';
+ tableHeader += ' | ';
+ tableHeader += ' | ';
+ tableHeader += ' | ';
+ $.each(aidStations, function() {
+ if ('START' === this.name) { return true; }
+ if ('FINISH' === this.name) { return true; }
+ tableHeader += 'IN | ';
+ tableHeader += 'OUT | ';
+ tableHeader += 'Pause | ';
+ tableHeader += 'T1 | ';
+ tableHeader += 'T2 | ';
+ tableHeader += 'P1 | ';
+ tableHeader += 'P2 | ';
+ });
tableHeader += 'IN | ';
- tableHeader += 'OUT | ';
- tableHeader += 'Pause | ';
- tableHeader += 'T1 | ';
- tableHeader += 'T2 | ';
- tableHeader += 'P1 | ';
- tableHeader += 'P2 | ';
+ tableHeader += 'T1 | ';
+ tableHeader += 'T2 | '; // -> Ziel/Gesamtzeit
+ tableHeader += 'P1 | ';
+ tableHeader += 'P2 | ';
+ tableHeader += ' | ';
+ tableHeader += ' | ';
+ tableHeader += '
';
+
+ $('#resultstable table thead').html(tableHeader);
+ $('#resultstable table caption').html(tableCaption);
+ $('#resultstable table tbody').html(tableContent);
+
+ //rankedRunnerList = callback(runnerList);
+ callback(runnerList);
+ console.log("rankedRunnerList:");
+ console.log(rankedRunnerList);
});
- tableHeader += 'IN | ';
- tableHeader += 'T1 | ';
- tableHeader += 'T2 | '; // -> Ziel/Gesamtzeit
- tableHeader += 'P1 | ';
- tableHeader += 'P2 | ';
- tableHeader += ' | ';
- tableHeader += ' | ';
- tableHeader += '';
-
- $('#resultstable table thead').html(tableHeader);
- $('#resultstable table caption').html(tableCaption);
- $('#resultstable table tbody').html(tableContent);
-
- rankedRunnerList = callback(runnerList);
- console.log("rankedRunnerList:");
- console.log(rankedRunnerList);
});
}
-
diff --git a/results_initial.png b/results_initial.png
new file mode 100644
index 0000000..319cb04
Binary files /dev/null and b/results_initial.png differ
diff --git a/results_with-vp2.png b/results_with-vp2.png
new file mode 100644
index 0000000..96a9c5d
Binary files /dev/null and b/results_with-vp2.png differ
diff --git a/routes/aidstation.js b/routes/aidstation.js
index ea173fb..30437ac 100644
--- a/routes/aidstation.js
+++ b/routes/aidstation.js
@@ -1,7 +1,7 @@
-var express = require('express');
-var router = express.Router();
-var assert = require('assert');
-
+const express = require('express');
+const router = express.Router();
+const assert = require('assert');
+const debug = require('debug')('ultraresult:aid');
/*
*
@@ -16,80 +16,138 @@ router.get('/', function(req, res) {
collection.find( {}, { 'sort' : ['totalDistance', 'asc']}, function(err, docs) {
console.log("find in collection ...");
if (err === null) {
- res.json(docs);
- //res.render('aid', { title: 'Aidstation: ' + docs.directions });
- }
+ return res.json(docs);
+ }
else {
- res.json({msg: 'error: ' + err});
- }
-
- console.log(docs);
- //res.json(docs);
+ debug(docs);
+ return res.json({msg: 'error: ' + err});
+ }
});
+});
+
+/*
+ *
+ */
+router.get('/list', function(req, res) {
+ res.render('aidlist', { title: 'Liste Verpflegungsposten (VP/Aid)' });
});
-
+
+
/*
* GET entry page for specific aidstation by their (short) name.
* note: directions field hold long descriptive name (P Rittweg)
* name field is the short id (vp1,k1,...)
*/
+// establish session / go to login page if not authenticated
router.get('/:id', function(req, res) {
var db = req.db;
var collection = db.get('aidstations');
var aidstationId = req.params.id.toUpperCase();
- var match = /^(((VP|K)\d\d?)|START|FINISH)$/;
- var found = match.test(aidstationId);
-
- // validate aidstation id or name
- // right now allow VPn or Kn with n as single or double digit decimal number
- console.log("aidstation: " + aidstationId);
- console.log("valid name? " + found);
- if (! found) {
+ var match_num = /^(\d\d?)$/;
+ var match_aid = /^(((VP)\d\d?)|START|FINISH|DNF)$/;
+ var found_num = match_num.test(aidstationId);
+ var vps = [];
+
+ // if only one or two digit then prepend 'VP'
+ if (found_num) {
+ aidstationId = 'VP' + aidstationId;
+ }
+
+ var found_aid = match_aid.test(aidstationId);
+
+ if (found_aid) {
+ req.session.aidurl = '/aid/' + aidstationId;
+ }
+ else {
+ // render error
+ return;
+ }
+
+ debug("aidstation: " + aidstationId);
+ debug("valid name? " + found_aid);
+ debug(req.session);
+
+ if (! req.session.loggedIn) {
+ res.redirect('/login');
+ return;
+ }
+
+ if (! found_aid) {
// FIXME:
- res.render('aid', { params : {
+ return res.render('aid', { params : {
title : 'aidstation not found',
id : aidstationId,
type : 'undef', totalDistance : 'undef', legDistance : 'undef'
}});
- return;
}
- collection.findOne({ name: aidstationId }, function(err, docs) {
- if (err === null && docs !== null) {
- //res.json(docs);
- console.log(docs);
+ // DNF / reset results
+ if (found_aid && req.session.isAdmin && aidstationId === 'DNF') {
+ return res.render('aid', { params : {
+ title : 'DNF / Reset Results',
+ id : 'DNF',
+ type : 'DNF',
+ legDistance : 'n/a',
+ legVpDistance : '',
+ totalDistance : 'n/a',
+ loc : { lat : '', lng : '' }
+ }});
+ }
- var numCheckpoints = 0;
-
- if (docs.checkpoints) {
- numCheckpoints = docs.checkpoints.length;
- console.log(docs.name + " has " + numCheckpoints + " previous checkpoints: " + docs.checkpoints);
- }
-
- res.render('aid', { params : {
- title : docs.name + ': ' + docs.directions,
- id : docs.name,
- type : docs.pointType,
- legDistance : docs.legDistance,
- legVpDistance : docs.legVpDistance,
- totalDistance : docs.totalDistance,
- loc : { lat : docs.lat, lng : docs.lng }
- }});
- }
- else {
- //res.json({msg: 'error: ' + err});
- res.render('aid', { params : {
- title : 'aidstation not found',
- id : aidstationId,
- type : 'undef', totalDistance : 'undef', legDistance : 'undef'
- }});
- return;
- }
- });
+ // get all aidstation names for aidstation links
+ // const promise1 = find aidstation names ();
+ // const promise2 = promise1.then(successCb, failCb);
+
+ // collection.find({}, {projections: { _id: 0, name: 1 }, sort: ['totalDistance', 'asc']})
+ // .then(function(result) {
+
+ const ret = collection.find({}, {projections: { _id: 0, name: 1 }, sort: ['totalDistance', 'asc']});
+ //if (ret) {
+ ret.each((vp, {close, pause, resume}) => {
+ debug('collection.find vp name: ' + vp.name);
+ vps.push(vp.name);
+ }).then(() => {
+ // I think now we can get rid of one ".then()" level
+ if (req.session.isAdmin)
+ vps.push('DNF');
+ }).then(() => {
+ debug('vps: ' + vps);
+ collection.findOne({ name: aidstationId }, function(err, docs) {
+ if (err === null && docs !== null) {
+ debug("findOne name: " + aidstationId);
+ debug(docs);
+ debug('vps: ' + vps);
-// res.render('index', { title: 'Express aidstation ' + req.params.id });
+ var numCheckpoints = 0;
+
+ if (docs.checkpoints) {
+ numCheckpoints = docs.checkpoints.length;
+ console.log(docs.name + " has " + numCheckpoints + " previous checkpoints: " + docs.checkpoints);
+ }
+
+ res.render('aid', { aid: vps, params : {
+ title : docs.name + ': ' + docs.directions,
+ id : docs.name,
+ type : docs.pointType,
+ legDistance : docs.legDistance,
+ legVpDistance : docs.legVpDistance,
+ totalDistance : docs.totalDistance,
+ loc : { lat : docs.lat, lng : docs.lng }
+ }});
+ }
+ else {
+ //res.json({msg: 'error: ' + err});
+ res.render('aid', { params : {
+ title : 'aidstation not found',
+ id : aidstationId,
+ type : 'undef', totalDistance : 'undef', legDistance : 'undef'
+ }});
+ return;
+ }
+ });
+ }); // then(() => {... after ret.each.
});
diff --git a/routes/auth.js b/routes/auth.js
new file mode 100644
index 0000000..533f285
--- /dev/null
+++ b/routes/auth.js
@@ -0,0 +1,77 @@
+const express = require('express');
+const router = express.Router();
+const assert = require('assert');
+const bcrypt = require('bcryptjs');
+const debug = require('debug')('ultraresult:auth');
+
+
+
+
+router.post('/auth', (req, res, next) => {
+ var db = req.db;
+ var collection = db.get('users');
+ var reUser = /^\w{2,32}$/;
+ var rePass = /^.{4,32}$/;
+ var user = '';
+ var pass = '';
+ var hash = '';
+
+ if (reUser.test(req.body.username)) {
+ user = req.body.username;
+ }
+ if (rePass.test(req.body.password)) {
+ pass = req.body.password;
+ }
+
+ if (user === "" || pass === "" ) {
+ debug("error: no user and/or pass!");
+ return res.sendStatus(401);
+ }
+
+ // lookup user
+ collection.findOne({user: user}).then((doc) => {
+ if (doc === null)
+ return res.sendStatus(401);
+
+ //debug('compare pass: ' + pass + ' with hash: ' + doc.pass);
+ if (bcrypt.compareSync(pass, doc.pass)) {
+ if ('admin' === doc.role) {
+ req.session.isAdmin = true;
+ }
+ debug("OK ... authenticated!");
+ next();
+ }
+ else {
+ return res.sendStatus(401);
+ }
+ });
+
+}, (req, res) => {
+ req.session.loggedIn = true;
+
+ if (true === req.session.isAdmin)
+ debug('admin session:');
+ debug(req.session);
+
+ aid_url = req.session.aidurl || '/';
+ debug("auth aid_url: " + aid_url);
+
+ return res.redirect(aid_url);
+});
+
+router.get('/login', (req, res) => {
+ if (req.session.loggedIn) {
+ return res.send('already logged in!');
+ }
+ return res.render('login');
+});
+
+
+router.get('/logout',(req,res) => {
+ req.session.destroy((err) => {});
+ return res.redirect('/');
+});
+
+
+
+module.exports = router;
diff --git a/routes/certificate.js b/routes/certificate.js
new file mode 100644
index 0000000..6b0909a
--- /dev/null
+++ b/routes/certificate.js
@@ -0,0 +1,68 @@
+const express = require('express');
+const router = express.Router();
+const debug = require('debug')('ultraresult:urkunde');
+const fs = require('fs');
+const certificate = require('../services/certificate-pdf.js');
+
+
+
+router.get('/:id', function(req, res) {
+ var db = req.db;
+ var runnerlist = db.get('runnerlist');
+ var startnum = req.params.id.toUpperCase();
+ var match_num = /^(\d\d?)$/;
+ var found_num = match_num.test(startnum);
+ var validData = false;
+
+ let name;
+ let time;
+ let rank;
+ let cert_date;
+ let cert_logo;
+
+ if (! found_num) {
+ return res.sendStatus(400);
+ }
+ if (! req.conf_certlinks) {
+ return res.sendStatus(400);
+ }
+ cert_date = `${req.conf_cert_days} ${req.conf_cert_year}`;
+ cert_logo = req.conf_cert_logo;
+
+ debug("generate certificate for runner: " + startnum);
+ debug("query database for runner stats...");
+ debug("cert logo file: " + cert_logo);
+ debug("cert date: " + cert_date);
+
+ // todo: provide template name via config or database collection
+ // or even better in the future store the gpx track in the database and generate svg from there on the fly
+
+// runnerlist.findOne({'startnum': parseInt(startnum)},
+// { projection: { firstname:1, lastname: 1, catger: 1, rank_all: 1, finish_time: 1 }}).then((data) => {
+// same thing but shorter without the projection keyword
+ runnerlist.findOne({'startnum': parseInt(startnum)},
+ 'firstname lastname catger rank_cat rank_all finish_time').then((data) => {
+ debug(data);
+ if (null !== data && data.finish_time) {
+ name = `${data.firstname} ${data.lastname}`;
+ time = data.finish_time;
+ rank = `${data.rank_all}. Platz`;
+
+ const stream = res.writeHead(200, {
+ 'Content-Type': 'application/pdf',
+ 'Content-Disposition': `attachment;filename=urkunde_${startnum}.pdf`,
+ });
+
+ certificate.createPdf(
+ {name, time, rank, cert_logo, cert_date},
+ (chunk) => stream.write(chunk),
+ () => stream.end()
+ );
+ }
+ else {
+ return res.sendStatus(400);
+ }
+ });
+});
+
+module.exports = router;
diff --git a/routes/index.js b/routes/index.js
index 466bad7..1d0f5a2 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -2,8 +2,9 @@ var express = require('express');
var router = express.Router();
/* GET home page. */
-router.get('/', function(req, res) {
- res.render('index', { title: 'Ultra-Result 2.0 (c) 2017 andreas loeffler' });
+router.get('/', (req, res) => {
+ var progver = req.progver;
+ res.render('index', { title: progver, progver: progver });
});
module.exports = router;
diff --git a/routes/results.js b/routes/results.js
index c018e9e..f6af63c 100644
--- a/routes/results.js
+++ b/routes/results.js
@@ -1,26 +1,48 @@
-var express = require('express');
-var router = express.Router();
-var assert = require('assert');
+const express = require('express');
+const router = express.Router();
+const debug = require('debug')('ultraresult:results');
/*
*
*/
router.get('/', function(req, res) {
var progver = req.progver;
- //var collection = db.get('runnerlist');
- res.render('results', { title: 'SUT 100 - Live Results', progver: progver });
-
- // collection.find({}, {fields: { _id: 0,
- // startnum : 1,
- // firstname : 1,
- // lastname : 1,
- // results : 1
- // }, sort : {startnum : 1}
- // }, function(err, docs) {
- // console.log(docs);
- // //res.json(docs);
- // });
+ var db = req.db;
+ var collection = db.get('aidstations');
+ var vps = [];
+ debug("is admin: " + req.session.isAdmin);
+ debug("finding aidstations ...");
+
+ if (! req.conf_aidlinks) {
+ return res.render('results', {
+ title: 'SUT 100 - Live Results', progver: progver, aid: vps, trackinglinks: req.conf_trackinglinks
+ });
+ }
+
+ collection.find({name: {$regex: '^vp[0-9]{1,2}$', $options: 'i'}}, {sort: {name: 1}}).each((aid, {close, pause, resume}) => {
+ // aidstations are streaming here
+ // call close() to stop the stream
+ debug('aid.name: ' + aid.name);
+ vps.push(aid.name);
+ }).then(() => {
+ // stream is over
+ vps.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
+ vps.unshift('START');
+ vps.push('FINISH');
+ if (req.session.isAdmin)
+ vps.push('DNF');
+
+ debug('aid array: ' + vps);
+
+ return res.render('results', {
+ title: 'SUT 100 - Live Results', progver: progver, aid: vps, trackinglinks: req.conf_trackinglinks
+ });
+ })
+
+ //collection.find( {}, { 'sort' : ['totalDistance', 'asc']}, function(err, docs) {
+ // ohh ok ... I used some other way of sorting here earlier ;) so maybe replace the compare function again
+ // ... or do both and make a sanity check
});
diff --git a/routes/runners.js b/routes/runners.js
index df99473..1baf132 100644
--- a/routes/runners.js
+++ b/routes/runners.js
@@ -1,6 +1,6 @@
-var express = require('express');
-var router = express.Router();
-
+const express = require('express');
+const router = express.Router();
+const debug = require('debug')('ultraresult:runners');
function isValidNum(num) {
var reNum = /^\d{1,4}$/;
@@ -106,34 +106,61 @@ router.get('/starterlist', function(req, res) {
* 'finish_time'
*/
router.put('/update/rank/:num', function(req, res) {
+ if (! req.session.loggedIn) {
+ res.sendStatus(418);
+ return;
+ }
+
var db = req.db;
- var collection = db.get('runnerlist');
+ var runnerlist = db.get('runnerlist');
+ var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID";
+ // res.send('invalid input') // FIXME parseInt -> store startnum as in not string
- var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID"; // res.send('invalid input') // FIXME parseInt -> store startnum as in not string
+ //var rankCat = isValidRank(req.body.rankcat) ? req.body.rankcat : "INVALID";
+ var rankAll = isValidRank(req.body.rankall) ? req.body.rankall : "INVALID";
+ var finishTime = isValidTime(req.body.finishtime) ? req.body.finishtime : "INVALID";
- var rankCat = isValidRank(req.body.rankcat) ? req.body.rankcat : "INVALID";
- var rankAll = isValidRank(req.body.rankall) ? req.body.rankall : "INVALID";
- var finishTime = isValidTime(req.body.finishtime) ? req.body.time : "INVALID";
+ debug("runner: " + startnum + ", finishTime: " + finishTime +
+ ", rankAll: " + rankAll); // + ", rankCat: " + rankCat);
- console.log("runner: " + startnum + ", finish_time: " + finishTime +
- ", rank_all: " + rankAll + ", rank_cat: " + rankCat);
+ var resultData = { 'rank_all': rankAll, 'finish_time': finishTime }; // rank_cat
- var resultData = { 'rank_all': rankAll, 'rank_cat': rankCat, 'finish_time': finishTime };
- //var resultData = { 'rank_all': 1, 'rank_cat': 1, 'finish_time': "99:99" };
+ // todo: check again if this does the same as below and only updates
+ //.. well maybe does not work if rank_all and/or finish_time does not exist yet
+ // runnerlist.update({ 'startnum' : startnum },
+ // { $set : resultData },
+ // function(err, cnt, stat) {
+ // res.send((err === null) ? { msg: '' } : { msg: 'error: ' + err });
+ // console.log("update count=" + cnt.nModified);
+ // console.log("update status=" + stat);
+ // });
+
+ runnerlist.findOneAndUpdate({'startnum' : startnum},
+ { $set: resultData },
+ //{ upsert: true },
+ function(err, cnt, stat) {
+ debug("update cnt: " + JSON.stringify(cnt));
+ debug("update stat: " + stat);
+
+ if (err === null) {
+ res.send('');
+ return;
+ }
+ debug('collection update error: ' + err);
+ //res.send({ msg: 'error: ' + err });
+ });
- collection.updateOne({ 'startnum' : startnum },
- { $set : resultDate },
- function(err, cnt, stat) {
- res.send((err === null) ? { msg: '' } : { msg: 'error: ' + err });
- console.log("update count=" + cnt.nModified);
- console.log("update status=" + stat);
- });
});
/*
* update in/out time
*/
router.put('/update/:num', function(req, res) {
+ if (! req.session.loggedIn) {
+ res.sendStatus(418);
+ return;
+ }
+
var db = req.db;
var collection = db.get('runnerlist');
@@ -141,7 +168,8 @@ router.put('/update/:num', function(req, res) {
var isIn = (req.body.inout === "tin") ? true : false;
var timeValid = (req.body.time_valid === 'true') ? true : false; // -> false !?
- var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID"; // res.sed('invalid input') // FIXME parseInt -> store startnum as in not string
+ var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID";
+ // res.sed('invalid input') // FIXME parseInt -> store startnum as in not string
// we get startnum via param and req.body.startnum -> can use as sanity check
var aidName = isValidAid(req.body.aid) ? req.body.aid : "INVALID";
@@ -180,7 +208,58 @@ router.put('/update/:num', function(req, res) {
console.log("update count=" + cnt.nModified);
console.log("update status=" + stat);
});
+});
+
+
+/*
+ * reset all in/out times
+ */
+router.put('/resetresult/:num', function(req, res) {
+ if (! (req.session.loggedIn && req.session.isAdmin)) {
+ res.sendStatus(418);
+ return;
+ }
+ var db = req.db;
+ var collection = db.get('runnerlist');
+ var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID";
+ console.log("reset all results for runner with startnum=" + startnum);
+
+ collection.update({ 'startnum' : startnum },
+ { $unset: {results: 1}}, //false, true,
+ function(err, cnt, stat) {
+ console.log("update count=" + cnt.nModified);
+ console.log("update status=" + stat);
+ res.send((err === null) ? { msg: '' } : { msg: 'error: ' + err });
+ return;
+ });
});
+/*
+ * set runner status to DNF
+ */
+router.put('/setdnf/:num', function(req, res) {
+ if (! (req.session.loggedIn && req.session.isAdmin)) {
+ res.sendStatus(418);
+ return;
+ }
+
+ var db = req.db;
+ var collection = db.get('runnerlist');
+ var startnum = isValidNum(req.params.num) ? parseInt(req.params.num) : "INVALID";
+ console.log("set DNF status for runner with startnum=" + startnum);
+ // FIXME
+ // collection.update({ 'startnum' : startnum },
+ // { $unset: {results: 1}}, //false, true,
+ // function(err, cnt, stat) {
+ // console.log("update count=" + cnt.nModified);
+ // console.log("update status=" + stat);
+ // res.send((err === null) ? { msg: '' } : { msg: 'error: ' + err });
+ // return;
+ // });
+ res.send({ msg: 'NOT IMPLEMENTED YET!' });
+ return;
+});
+
+
module.exports = router;
diff --git a/routes/tracking.js b/routes/tracking.js
new file mode 100644
index 0000000..714360f
--- /dev/null
+++ b/routes/tracking.js
@@ -0,0 +1,110 @@
+const express = require('express');
+const router = express.Router();
+const debug = require('debug')('ultraresult:tracking');
+
+function isValidUrl(url) {
+ var isClear = /^CLEAR$/.test(url);
+ var valid = /^(http|https):\/\/[^ "]+$/.test(url);
+ if (valid || isClear) {
+ return true;
+ }
+ return false;
+}
+// unsafe url chars: []{}|\"%~#<>
+//var re = /[a-z0-9-\.]+\.[a-z]{2,4}\/?([^\s<>\#%"\,\{\}\\|\\\^\[\]`]+)?$/;
+
+
+function isValidName(name) {
+ var valid = /^[\w _-]{2,32}$/.test(name);
+ if (! valid) {
+ return false;
+ }
+ return true;
+}
+
+
+router.get('/', function(req, res) {
+ // if (! req.session.loggedIn) {
+ // res.sendStatus(418);
+ // return;
+ // }
+
+ if (! req.conf_trackinglinks)
+ return res.json({});
+
+ var db = req.db;
+ var trackinglinks = db.get('trackinglinks');
+
+ trackinglinks.find({}, {projections: { _id: 0,
+ name : 1,
+ url : 1,
+ }, sort : {name : 1}
+ }, function(err, docs) {
+ debug(docs);
+ return res.json(docs);
+ });
+});
+
+router.get('/add', function(req, res) {
+ if (! req.session.loggedIn) {
+ req.session.aidurl = '/tracking/add';
+ res.redirect('/login');
+ return;
+ }
+ res.render('post-tracking-link');
+});
+
+router.post('/add', function(req, res) {
+ debug("add tracking link, url: " + req.body.url);
+ debug("add tracking link, name: " + req.body.name);
+ debug('config: show tracking links: ' + req.conf_trackinglinks)
+
+ if (! req.session.loggedIn) {
+ req.session.aidurl = '/tracking/add';
+ res.redirect('/login');
+ return;
+ }
+ var db = req.db;
+ var trackinglinks = db.get('trackinglinks');
+
+ if (! isValidName(req.body.name)) {
+ res.render('post-tracking-link', { msg: 'invalid name' });
+ return;
+ }
+
+ if (! isValidUrl(req.body.url)) {
+ res.render('post-tracking-link', { msg: 'invalid url' });
+ return;
+ }
+
+ // find name and update if it exists or insert otherwise
+ trackinglinks.findOne({name: req.body.name}).then((doc) => {
+ if (doc === null) {
+ // not found, insert
+ debug("name not found")
+ }
+ else {
+ // found ... update link for this name
+ debug("name already has a link, update!")
+ // check if isClear ... then drop from db
+ // right now the url 'CLEAR' won't get displayed anyway...
+ }
+ });
+
+ trackinglinks.update({'name': req.body.name},
+ { 'name': req.body.name, 'url': req.body.url },
+ { replaceOne: true, upsert: true},
+ function(err, cnt, stat) {
+ debug('update: ' + JSON.stringify(cnt));
+
+ if (err === null) {
+ res.redirect('/');
+ return;
+ }
+ debug('collection update error: ' + err);
+ res.render('post-tracking-link', { msg: 'db error' });
+ });
+});
+
+
+module.exports = router;
diff --git a/services/certificate-pdf.js b/services/certificate-pdf.js
new file mode 100644
index 0000000..fee2cfe
--- /dev/null
+++ b/services/certificate-pdf.js
@@ -0,0 +1,34 @@
+
+const PDFDocument = require('pdfkit');
+const debug = require('debug')('ultraresult:certificate-pdf');
+
+const dateY = 550
+
+
+// othter fonts: Courier, ...
+function createPdf(data, dataCallback, endCallback) {
+ const doc = new PDFDocument({bufferPages: true, font: 'Helvetica', size: 'A4'});
+
+ doc.on('data', dataCallback);
+ doc.on('end', endCallback);
+
+ doc.fontSize(72).text('URKUNDE', 100, 50, { align: 'center'});
+ doc.image(data.cert_logo, {
+ cover: [395, 395],
+ align: 'center',
+ });
+
+ // data.cert_year
+ doc.fontSize(28).text(data.cert_date, 100, dateY, { align: 'center'})
+
+ doc.fontSize(32).text(data.name, 100, dateY + 60, {align: 'center'})
+ doc.fontSize(8).moveDown();
+ doc.fontSize(32).text(data.time, {align: 'center'});
+ doc.fontSize(8).moveDown();
+ doc.fontSize(32).text(data.rank, {align: 'center'});
+
+ doc.end();
+ debug("create PDF end");
+}
+
+module.exports = { createPdf };
diff --git a/ultraresult.conf.sample b/ultraresult.conf.sample
new file mode 100644
index 0000000..cfffc78
--- /dev/null
+++ b/ultraresult.conf.sample
@@ -0,0 +1,15 @@
+{
+ "database": {
+ "name" : "test",
+ "host" : "mongodb.de",
+ "port" : 1234,
+ "username" : "test",
+ "password" : "********",
+ "sslcafile": "CA.pem",
+ "sslkeyfile": "client.pem",
+ "authdb": "test_auth"
+ },
+
+ "trackinglinks": false,
+ "aidlinks": false
+}
diff --git a/views/aid.pug b/views/aid.pug
index 6e9c813..de945b9 100644
--- a/views/aid.pug
+++ b/views/aid.pug
@@ -1,6 +1,14 @@
extends aidlayout
block content
+ a#results(href='/' title="Results") Back to Results
+ br
+ if aid
+ each id in aid
+ a(href='/aid/' + id class="btn btn-sm aidlink")= id
+ |
+
+
h2= params.title
// div.control-section
@@ -20,15 +28,20 @@ block content
th(class="sorttable_numeric") Start No.
th Name
th Lastname
- unless params.type == 'Start'
+ unless params.type === 'Start' || params.type === 'DNF'
th(class="sorttable_nosort") In (date)
th(class="sorttable_nosort") In (hh:mm)
th(class="sorttable_nosort") Save
th(class="sorttable_nosort") Edit
- unless params.type == 'Finish'
+ unless params.type === 'Finish' || params.type === 'DNF'
th(class="sorttable_nosort") Out (date)
th(class="sorttable_nosort") Out (hh:mm)
th(class="sorttable_nosort") Save
th(class="sorttable_nosort") Edit
+ if params.type === 'DNF'
+ th(class="sorttable_nosort") (date)
+ th(class="sorttable_nosort") (hh:mm)
+ th(class="sorttable_nosort") DNF
+ th(class="sorttable_nosort") RESET
tbody
diff --git a/views/aidlayout.pug b/views/aidlayout.pug
index 19c7c36..4dbaa3a 100644
--- a/views/aidlayout.pug
+++ b/views/aidlayout.pug
@@ -7,10 +7,10 @@ html
meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="viewport" content="width=device-width, initial-scale=1")
- link(rel='stylesheet', href='/css/style.css')
link(rel='stylesheet', href='/css/bootstrap.min.css')
link(rel='stylesheet', href='/css/materialize.min.css')
link(rel="stylesheet", href="/css/font-awesome.min.css")
+ link(rel='stylesheet', href='/css/style.css')
body
block content
@@ -19,4 +19,4 @@ html
script(src='/js/bootstrap.min.js')
script(src='/js/materialize.min.js')
script(src='/js/sorttable.js')
- script(src='/js/aidstation.js')
+ script(type="module" src='/js/aidstation.js')
diff --git a/views/aidlist.pug b/views/aidlist.pug
new file mode 100644
index 0000000..2bcc4a9
--- /dev/null
+++ b/views/aidlist.pug
@@ -0,0 +1,32 @@
+doctype html
+html
+ head
+ title= title
+
+ meta(charset="utf-8")
+ meta(http-equiv="X-UA-Compatible" content="IE=edge")
+ meta(name="viewport" content="width=device-width, initial-scale=1")
+
+ link(rel='stylesheet', href='/css/style.css')
+ link(rel='stylesheet', href='/css/bootstrap.min.css')
+ link(rel='stylesheet', href='/css/materialize.min.css')
+
+ body
+ block content
+
+ #aidstationListTable
+ table(class="")
+ thead
+ th Aid (VP) No.
+ th Name
+ th Total Distance (km)
+ th Leg Distance (km)
+ th Lat / Long
+ th Elevation
+ tbody
+
+ script(src='/js/jquery-3.1.1.min.js')
+ script(src='/js/bootstrap.min.js')
+ script(src='/js/materialize.min.js')
+ script(src='/js/sorttable.js')
+ script(src='/js/aidstationlist.js')
diff --git a/views/index.pug b/views/index.pug
index f85325e..563d76a 100644
--- a/views/index.pug
+++ b/views/index.pug
@@ -1,5 +1,7 @@
extends layout
block content
- h2= title
- Hallo
\ No newline at end of file
+ h1= title
+ h2 © 2017,2018,2021 andreas loeffler
+
+ address #{progver}
diff --git a/views/login.pug b/views/login.pug
new file mode 100644
index 0000000..c19ea88
--- /dev/null
+++ b/views/login.pug
@@ -0,0 +1,19 @@
+extends layout
+
+block content
+ div.text-center
+
+ h1(class=h1Classes) Please login
+
+ form(class="form-signin" method="POST" action="/auth")
+
+ label(for="inputUsername" class="sr-only") Username
+ input(type="text" name="username" id="inputUsername" class="form-control" placeholder="Username" required autofocus)
+ label(for="inputPassword" class="sr-only") Password
+ input(type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required)
+ div(class=divClasses)
+ label
+ input(type="checkbox" value="remember-me")
+ span Remember me
+
+ button(class=buttonClass type="submit") Login
diff --git a/views/post-tracking-link.pug b/views/post-tracking-link.pug
new file mode 100644
index 0000000..ecbbb18
--- /dev/null
+++ b/views/post-tracking-link.pug
@@ -0,0 +1,20 @@
+extends layout
+
+block content
+ a#results(href='/' title="Results") Back to Results
+
+ div.text-center
+
+ h2#errormsg= msg
+
+ h1(class=h1Classes) Post Link below
+
+ form(class="form-tracking-link" method="POST" action="/tracking/add")
+
+ label(for="inputName" class="sr-only") Name
+ input(type="text" name="name" id="inputName" class="form-control" placeholder="Name" required autofocus)
+ br
+ label(for="inputLink" class="sr-only") Link
+ input(type="text" name="url" id="inputLink" class="form-control" placeholder="Traking URL" required)
+ br
+ button(class=buttonClass type="submit") Submit
diff --git a/views/results.pug b/views/results.pug
index 1a4ec72..bb57dd6 100644
--- a/views/results.pug
+++ b/views/results.pug
@@ -2,6 +2,11 @@ extends resultslayout
block content
+ if aid
+ each id in aid
+ a(href='/aid/' + id class="btn btn-sm aidlink")= id
+ |
+
h1= title
//p Welcome to #{title}
@@ -13,4 +18,11 @@ block content
tbody
br
+
+ if trackinglinks
+ h3#trackerlinks
+ ul#trackerlinks
+ a(href='/tracking/add') Add tracking link
+
+ hr
address #{progver}
diff --git a/views/resultslayout.pug b/views/resultslayout.pug
index 1d7fda2..a5996c8 100644
--- a/views/resultslayout.pug
+++ b/views/resultslayout.pug
@@ -7,9 +7,9 @@ html
meta(http-equiv="X-UA-Compatible" content="IE=edge")
meta(name="viewport" content="width=device-width, initial-scale=1")
- link(rel='stylesheet', href='/css/style.css')
link(rel='stylesheet', href='/css/bootstrap.min.css')
link(rel='stylesheet', href='/css/materialize.min.css')
+ link(rel='stylesheet', href='/css/style.css')
body
block content
@@ -18,4 +18,4 @@ html
script(src='/js/bootstrap.min.js')
script(src='/js/materialize.min.js')
script(src='/js/sorttable.js')
- script(src='/js/results.js')
+ script(type="module" src='/js/results.js')