├── .ecrc ├── .editorconfig ├── .gitignore ├── .htmlhintrc ├── .stylelintignore ├── .stylelintrc.json ├── .vscode ├── extensions.json └── snippets │ └── html.json ├── README.md ├── gulp ├── config │ └── paths.js └── tasks │ ├── cache.js │ ├── clean.js │ ├── html-include.js │ ├── html-minify.js │ ├── images.js │ ├── resources.js │ ├── rewrite.js │ ├── scripts-backend.js │ ├── scripts.js │ ├── sprite.js │ ├── styles-backend.js │ ├── styles.js │ ├── webp.js │ └── zip.js ├── gulpfile.js ├── package.json └── src ├── img ├── cover.jpg └── svg │ └── icon.svg ├── index.html ├── js ├── _components.js ├── _functions.js ├── _vars.js ├── components │ └── ex.js ├── functions │ ├── burger.js │ ├── check-viewport.js │ ├── disable-scroll.js │ ├── enable-scroll.js │ ├── fix-fullheight.js │ ├── header-height.js │ ├── mobile-check.js │ ├── throttle.js │ └── validate-forms.js └── main.js ├── partials ├── footer.html ├── head.html └── header.html ├── resources ├── favicon.ico ├── fonts │ └── MullerRegular.woff2 ├── mail.php └── phpmailer │ ├── Exception.php │ ├── PHPMailer.php │ └── SMTP.php └── scss ├── _fonts.scss ├── _mixins.scss ├── _settings.scss ├── _vars.scss ├── components └── _header.scss ├── main.scss ├── mixins ├── _breakpoint.scss ├── _burger.scss ├── _checkbox.scss ├── _disable-mob-hover.scss ├── _flex.scss ├── _font-face.scss ├── _layout.scss ├── _mini.scss └── _tabs.scss ├── vendor.scss └── vendor └── normalize.css /.ecrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "app", "vendor", "gulpfile.js", "package.json", ".stylelintrc", "\\.md$", "\\.php$"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /app 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "attr-lowercase": false, 3 | "doctype-first": false 4 | } 5 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | app/**/*.css 2 | src/scss/vendor/**/* 3 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard-scss", 3 | "plugins": [ 4 | "stylelint-order" 5 | ], 6 | "rules": { 7 | "order/order": [ 8 | "custom-properties", 9 | "dollar-variables", 10 | "at-rules" 11 | ], 12 | "order/properties-alphabetical-order": null, 13 | "order/properties-order": [ 14 | [ 15 | "all", 16 | "print-color-adjust", 17 | "appearance", 18 | "counter-increment", 19 | "counter-reset", 20 | "content", 21 | "quotes", 22 | "position", 23 | "left", 24 | "right", 25 | "top", 26 | "bottom", 27 | "inset", 28 | "z-index", 29 | "display", 30 | "columns", 31 | "column-width", 32 | "column-count", 33 | "column-fill", 34 | "column-gap", 35 | "column-rule", 36 | "column-rule-style", 37 | "column-rule-width", 38 | "column-rule-color", 39 | "column-span", 40 | "break-after", 41 | "break-before", 42 | "break-inside", 43 | "page-break-after", 44 | "page-break-before", 45 | "page-break-inside", 46 | "orphans", 47 | "widows", 48 | "flex", 49 | "flex-grow", 50 | "flex-shrink", 51 | "flex-basis", 52 | "flex-flow", 53 | "flex-direction", 54 | "flex-wrap", 55 | "place-content", 56 | "place-items", 57 | "place-self", 58 | "align-content", 59 | "align-items", 60 | "align-self", 61 | "justify-content", 62 | "justify-items", 63 | "justify-self", 64 | "order", 65 | "clear", 66 | "float", 67 | "grid", 68 | "grid-area", 69 | "grid-auto-columns", 70 | "grid-auto-flow", 71 | "grid-auto-rows", 72 | "grid-column", 73 | "grid-column-end", 74 | "grid-column-gap", 75 | "grid-column-start", 76 | "grid-gap", 77 | "grid-row", 78 | "grid-row-end", 79 | "grid-row-gap", 80 | "grid-row-start", 81 | "grid-template", 82 | "grid-template-areas", 83 | "grid-template-columns", 84 | "grid-template-rows", 85 | "list-style", 86 | "list-style-type", 87 | "list-style-position", 88 | "list-style-image", 89 | "caption-side", 90 | "empty-cells", 91 | "table-layout", 92 | "vertical-align", 93 | "clip-path", 94 | "mask", 95 | "mask-clip", 96 | "mask-composite", 97 | "mask-image", 98 | "mask-mode", 99 | "mask-origin", 100 | "mask-position", 101 | "mask-position-x", 102 | "mask-position-y", 103 | "mask-repeat", 104 | "mask-repeat-x", 105 | "mask-repeat-y", 106 | "mask-size", 107 | "mask-type", 108 | "shape-image-threshold", 109 | "shape-margin", 110 | "shape-outside", 111 | "contain", 112 | "overflow", 113 | "overflow-x", 114 | "overflow-y", 115 | "overflow-anchor", 116 | "overflow-wrap", 117 | "margin", 118 | "margin-top", 119 | "margin-right", 120 | "margin-bottom", 121 | "margin-left", 122 | "margin-before", 123 | "margin-end", 124 | "margin-after", 125 | "margin-start", 126 | "margin-collapse", 127 | "margin-top-collapse", 128 | "margin-bottom-collapse", 129 | "margin-before-collapse", 130 | "margin-after-collapse", 131 | "outline", 132 | "outline-style", 133 | "outline-width", 134 | "outline-color", 135 | "outline-offset", 136 | "outline-radius", 137 | "outline-radius-topleft", 138 | "outline-radius-topright", 139 | "outline-radius-bottomright", 140 | "outline-radius-bottomleft", 141 | "border", 142 | "border-style", 143 | "border-width", 144 | "border-color", 145 | "border-top", 146 | "border-top-style", 147 | "border-top-width", 148 | "border-top-color", 149 | "border-right", 150 | "border-right-style", 151 | "border-right-width", 152 | "border-right-color", 153 | "border-bottom", 154 | "border-bottom-style", 155 | "border-bottom-width", 156 | "border-bottom-color", 157 | "border-left", 158 | "border-left-style", 159 | "border-left-width", 160 | "border-left-color", 161 | "border-before", 162 | "border-before-style", 163 | "border-before-width", 164 | "border-before-color", 165 | "border-end", 166 | "border-end-style", 167 | "border-end-width", 168 | "border-end-color", 169 | "border-after", 170 | "border-after-style", 171 | "border-after-width", 172 | "border-after-color", 173 | "border-start", 174 | "border-start-style", 175 | "border-start-width", 176 | "border-start-color", 177 | "border-collapse", 178 | "border-image", 179 | "border-image-source", 180 | "border-image-slice", 181 | "border-image-width", 182 | "border-image-outset", 183 | "border-image-repeat", 184 | "border-radius", 185 | "border-top-left-radius", 186 | "border-top-right-radius", 187 | "border-bottom-right-radius", 188 | "border-bottom-left-radius", 189 | "border-spacing", 190 | "padding", 191 | "padding-top", 192 | "padding-right", 193 | "padding-bottom", 194 | "padding-left", 195 | "padding-before", 196 | "padding-end", 197 | "padding-after", 198 | "padding-start", 199 | "width", 200 | "height", 201 | "min-width", 202 | "min-height", 203 | "max-width", 204 | "max-height", 205 | "box-decoration-break", 206 | "box-shadow", 207 | "box-sizing", 208 | "src", 209 | "font", 210 | "font-family", 211 | "font-weight", 212 | "font-style", 213 | "font-display", 214 | "font-feature-settings", 215 | "font-kerning", 216 | "font-smoothing", 217 | "font-stretch", 218 | "font-synthesis", 219 | "font-variant", 220 | "font-variant-alternates", 221 | "font-variant-caps", 222 | "font-variant-east-asian", 223 | "font-variant-ligatures", 224 | "font-variant-numeric", 225 | "font-variant-position", 226 | "font-size", 227 | "font-size-adjust", 228 | "unicode-bidi", 229 | "unicode-range", 230 | "line-break", 231 | "line-height", 232 | "letter-spacing", 233 | "word-break", 234 | "word-spacing", 235 | "word-wrap", 236 | "white-space", 237 | "hyphens", 238 | "tab-size", 239 | "text-align", 240 | "text-align-last", 241 | "text-combine-upright", 242 | "text-decoration", 243 | "text-decoration-style", 244 | "text-decoration-line", 245 | "text-decoration-color", 246 | "text-decoration-skip", 247 | "text-emphasis", 248 | "text-emphasis-style", 249 | "text-emphasis-color", 250 | "text-emphasis-position", 251 | "text-fill-color", 252 | "text-indent", 253 | "text-justify", 254 | "text-orientation", 255 | "text-overflow", 256 | "text-rendering", 257 | "text-security", 258 | "text-shadow", 259 | "text-size-adjust", 260 | "text-stroke", 261 | "text-stroke-width", 262 | "text-stroke-color", 263 | "text-transform", 264 | "text-underline-position", 265 | "direction", 266 | "writing-mode", 267 | "ruby-align", 268 | "ruby-position", 269 | "color", 270 | "caret-color", 271 | "tap-highlight-color", 272 | "d", 273 | "x", 274 | "y", 275 | "cx", 276 | "cy", 277 | "r", 278 | "rx", 279 | "ry", 280 | "fill", 281 | "fill-opacity", 282 | "fill-rule", 283 | "stroke", 284 | "stroke-dasharray", 285 | "stroke-dashoffset", 286 | "stroke-linecap", 287 | "stroke-linejoin", 288 | "stroke-miterlimit", 289 | "stroke-opacity", 290 | "stroke-width", 291 | "alignment-baseline", 292 | "baseline-shift", 293 | "dominant-baseline", 294 | "clip-rule", 295 | "color-interpolation", 296 | "color-interpolation-filters", 297 | "color-rendering", 298 | "flood-color", 299 | "flood-opacity", 300 | "lighting-color", 301 | "marker", 302 | "marker-end", 303 | "marker-mid", 304 | "marker-start", 305 | "paint-order", 306 | "shape-rendering", 307 | "stop-color", 308 | "stop-opacity", 309 | "text-anchor", 310 | "offset", 311 | "offset-position", 312 | "offset-path", 313 | "offset-distance", 314 | "offset-anchor", 315 | "offset-rotate", 316 | "background", 317 | "background-image", 318 | "background-position", 319 | "background-position-x", 320 | "background-position-y", 321 | "background-size", 322 | "background-repeat", 323 | "background-repeat-x", 324 | "background-repeat-y", 325 | "background-origin", 326 | "background-clip", 327 | "background-attachment", 328 | "background-color", 329 | "background-blend-mode", 330 | "image-orientation", 331 | "image-rendering", 332 | "object-fit", 333 | "object-position", 334 | "opacity", 335 | "visibility", 336 | "filter", 337 | "isolation", 338 | "mix-blend-mode", 339 | "zoom", 340 | "backface-visibility", 341 | "perspective", 342 | "perspective-origin", 343 | "perspective-origin-x", 344 | "perspective-origin-y", 345 | "transform", 346 | "transform-box", 347 | "transform-origin", 348 | "transform-origin-x", 349 | "transform-origin-y", 350 | "transform-origin-z", 351 | "transform-style", 352 | "transition", 353 | "transition-property", 354 | "transition-duration", 355 | "transition-delay", 356 | "transition-timing-function", 357 | "animation", 358 | "animation-name", 359 | "animation-duration", 360 | "animation-delay", 361 | "animation-timing-function", 362 | "animation-iteration-count", 363 | "animation-direction", 364 | "animation-fill-mode", 365 | "animation-play-state", 366 | "will-change", 367 | "cursor", 368 | "pointer-events", 369 | "touch-action", 370 | "user-drag", 371 | "user-focus", 372 | "user-select", 373 | "user-zoom", 374 | "resize", 375 | "scroll-behavior", 376 | "scroll-snap-coordinate", 377 | "scroll-snap-destination", 378 | "scroll-snap-type", 379 | "scroll-snap-type-x", 380 | "scroll-snap-type-y" 381 | ], 382 | { 383 | "unspecified": "bottomAlphabetical" 384 | } 385 | ], 386 | "at-rule-empty-line-before": [ 387 | "always", 388 | { 389 | "except": [ 390 | "blockless-after-same-name-blockless", 391 | "first-nested" 392 | ], 393 | "ignore": [ 394 | "after-comment" 395 | ], 396 | "ignoreAtRules": [ 397 | "else" 398 | ] 399 | } 400 | ], 401 | "at-rule-no-unknown": null, 402 | "at-rule-no-vendor-prefix": true, 403 | "declaration-no-important": true, 404 | "color-function-notation": null, 405 | "alpha-value-notation": null, 406 | "property-no-vendor-prefix": null, 407 | "custom-property-pattern": null, 408 | "selector-class-pattern": null, 409 | "selector-id-pattern": null, 410 | "color-named": "never", 411 | "scss/double-slash-comment-inline": null, 412 | "scss/double-slash-comment-whitespace-inside": "always", 413 | "scss/media-feature-value-dollar-variable": null, 414 | "scss/operator-no-newline-after": null, 415 | "scss/operator-no-newline-before": null, 416 | "scss/operator-no-unspaced": true, 417 | "scss/partial-no-import": null, 418 | "scss/percent-placeholder-pattern": null, 419 | "scss/selector-no-redundant-nesting-selector": true, 420 | "scss/dollar-variable-no-missing-interpolation": null, 421 | "scss/dollar-variable-pattern": null, 422 | "scss/double-slash-comment-empty-line-before": [ 423 | "always", 424 | { 425 | "except": [ 426 | "first-nested" 427 | ], 428 | "ignore": [ 429 | "between-comments", 430 | "stylelint-commands" 431 | ] 432 | } 433 | ], 434 | "value-keyword-case": "lower", 435 | "value-no-vendor-prefix": true, 436 | "scss/at-else-closing-brace-newline-after": "always-last-in-chain", 437 | "scss/at-else-closing-brace-space-after": "always-intermediate", 438 | "scss/at-else-empty-line-before": "never", 439 | "scss/at-else-if-parentheses-space-before": "always", 440 | "scss/at-extend-no-missing-placeholder": null, 441 | "scss/at-function-parentheses-space-before": "never", 442 | "scss/at-function-pattern": null, 443 | "scss/at-if-closing-brace-newline-after": "always-last-in-chain", 444 | "scss/at-if-closing-brace-space-after": "always-intermediate", 445 | "scss/at-import-partial-extension-blacklist": null, 446 | "scss/at-mixin-argumentless-call-parentheses": "never", 447 | "scss/at-mixin-parentheses-space-before": "never", 448 | "scss/at-mixin-pattern": null, 449 | "scss/at-rule-no-unknown": true, 450 | "scss/declaration-nested-properties": "never", 451 | "scss/declaration-nested-properties-no-divided-groups": null, 452 | "scss/dollar-variable-colon-newline-after": "always-multi-line", 453 | "scss/dollar-variable-colon-space-after": "always-single-line", 454 | "scss/dollar-variable-colon-space-before": "never", 455 | "scss/dollar-variable-empty-line-before": [ 456 | "always", 457 | { 458 | "except": [ 459 | "after-dollar-variable", 460 | "first-nested" 461 | ], 462 | "ignore": [ 463 | "after-comment", 464 | "inside-single-line-block" 465 | ] 466 | } 467 | ], 468 | "selector-attribute-quotes": "always", 469 | "selector-max-attribute": null, 470 | "selector-max-class": null, 471 | "selector-max-combinators": null, 472 | "selector-max-compound-selectors": null, 473 | "selector-max-id": 1, 474 | "selector-max-specificity": null, 475 | "selector-max-type": null, 476 | "selector-max-universal": null, 477 | "selector-nested-pattern": null, 478 | "selector-no-qualifying-type": null, 479 | "selector-no-vendor-prefix": true, 480 | "selector-pseudo-class-no-unknown": true, 481 | "selector-pseudo-element-colon-notation": "double", 482 | "selector-pseudo-element-no-unknown": true, 483 | "selector-type-case": "lower", 484 | "selector-type-no-unknown": true, 485 | "shorthand-property-no-redundant-values": true, 486 | "string-no-newline": true, 487 | "time-min-milliseconds": 10, 488 | "block-no-empty": [ 489 | true, 490 | { 491 | "severity": "warning" 492 | } 493 | ], 494 | "color-hex-length": "short", 495 | "color-no-hex": null, 496 | "color-no-invalid-hex": true, 497 | "comment-empty-line-before": [ 498 | "always", 499 | { 500 | "except": [ 501 | "first-nested" 502 | ], 503 | "ignore": [ 504 | "after-comment", 505 | "stylelint-commands" 506 | ] 507 | } 508 | ], 509 | "comment-no-empty": null, 510 | "comment-whitespace-inside": "always", 511 | "custom-media-pattern": null, 512 | "custom-property-empty-line-before": [ 513 | "always", 514 | { 515 | "except": [ 516 | "after-custom-property", 517 | "first-nested" 518 | ], 519 | "ignore": [ 520 | "after-comment", 521 | "inside-single-line-block" 522 | ] 523 | } 524 | ], 525 | "font-family-name-quotes": "always-unless-keyword", 526 | "font-family-no-duplicate-names": true, 527 | "font-weight-notation": "numeric", 528 | "keyframe-declaration-no-important": true, 529 | "length-zero-no-unit": true, 530 | "max-nesting-depth": null, 531 | "no-descending-specificity": null, 532 | "no-duplicate-selectors": true, 533 | "no-empty-source": [ 534 | true, 535 | { 536 | "severity": "warning" 537 | } 538 | ], 539 | "number-max-precision": 5, 540 | "property-no-unknown": [ 541 | true, 542 | { 543 | "checkPrefixed": true 544 | } 545 | ], 546 | "rule-empty-line-before": [ 547 | "always-multi-line", 548 | { 549 | "except": [ 550 | "first-nested" 551 | ], 552 | "ignore": [ 553 | "after-comment" 554 | ] 555 | } 556 | ] 557 | }, 558 | "overrides": [{ 559 | "files": ["src/scss/*/**.scss"], 560 | "customSyntax": "postcss-scss" 561 | }] 562 | } 563 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Syler.sass-indented","stylelint.vscode-stylelint","jerryhong.autofilename","EditorConfig.EditorConfig", "htmlhint.vscode-htmlhint", "ritwickdey.LiveServer", "dbaeumer.vscode-eslint", "rebornix.project-snippets"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/snippets/html.json: -------------------------------------------------------------------------------- 1 | { 2 | "svguse": { 3 | "scope": "html", 4 | "prefix": "g-use", 5 | "body": [ 6 | "", 7 | " ", 8 | "" 9 | ], 10 | "description": "Snippet for use svg sprites" 11 | }, 12 | "burger": { 13 | "scope": "html", 14 | "prefix": "g-burger", 15 | "body": [ 16 | "" 19 | ], 20 | "description": "Snippet for burger btn" 21 | }, 22 | "checkbox": { 23 | "scope": "html", 24 | "prefix": "g-checkbox", 25 | "body": [ 26 | "" 30 | ], 31 | "description": "Snippet for custom checkbox" 32 | }, 33 | "navigation": { 34 | "scope": "html", 35 | "prefix": "g-nav", 36 | "body": [ 37 | "" 44 | ], 45 | "description": "Snippet for navigation" 46 | }, 47 | "social": { 48 | "scope": "html", 49 | "prefix": "g-social", 50 | "body": [ 51 | "" 56 | ], 57 | "description": "Snippet for social links" 58 | }, 59 | "basicform": { 60 | "prefix": "g-form", 61 | "body": [ 62 | "
", 63 | " ", 66 | " ", 67 | "
" 68 | ], 69 | "description": "Snippet for basic form" 70 | }, 71 | "basicswiper": { 72 | "prefix": "g-swiper", 73 | "body": [ 74 | "
", 75 | "
", 76 | "
", 77 | "
", 78 | "
", 79 | "
" 80 | ], 81 | "description": "Snippet for basic swiper structure" 82 | }, 83 | "graph modal Btn": { 84 | "prefix": "g-graph-btn", 85 | "body": [ 86 | "" 87 | ], 88 | "description": "Snippet for graph-modal btn" 89 | }, 90 | "graph modal Structure": { 91 | "prefix": "g-graph-modal", 92 | "body": [ 93 | "
", 94 | "
", 95 | " ", 96 | "
$2
", 97 | "
", 98 | "
" 99 | ], 100 | "description": "Snippet for graph-modal basic structure" 101 | }, 102 | "picture": { 103 | "prefix": "g-picture", 104 | "body": [ 105 | "", 106 | " ", 107 | " \"$3\"", 108 | "" 109 | ], 110 | "description": "Snippet for basic logo structure" 111 | }, 112 | "svgimg": { 113 | "scope": "html", 114 | "prefix": "g-svgimg", 115 | "body": [ 116 | "\"$3\"" 117 | ], 118 | "description": "Snippet for simple html svg images" 119 | }, 120 | "tooltip": { 121 | "prefix": "g-tooltip", 122 | "body": [ 123 | "", 124 | " ", 125 | " ", 126 | " Тултип: ", 127 | " $3", 128 | " ", 129 | "" 130 | ], 131 | "description": "Snippet for tooltip" 132 | }, 133 | "tabs html sctructure": { 134 | "prefix": "g-tabs", 135 | "body": [ 136 | "
", 137 | " ", 142 | "
", 143 | "
$5
", 144 | "
$6
", 145 | "
$7
", 146 | "
", 147 | "
" 148 | ], 149 | "description": "Snippet for basic tabs structure" 150 | }, 151 | "transparent video (need special video-file)": { 152 | "prefix": "g-tr-video", 153 | "body": [ 154 | "" 158 | ], 159 | "description": "Snippet for transparent video" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gulp — сборка MaxGraph 2 | 3 | > Используется Gulp 5. Тестировалось на node.js 20.12.2 4 | 5 | ## Начало работы 6 | 7 | Для работы с данной сборкой в новом проекте, склонируйте все содержимое репозитория
8 | `git clone ` 9 | Затем, находясь в корне проекта, запустите команду `npm i`, которая установит все находящиеся в package.json зависимости. 10 | После этого вы можете использовать любую из предложенных команд сборки (подробнее - ниже, в разделе __npm-скрипты__). 11 | 12 | ## Структура папок и файлов 13 | 14 | ``` 15 | ├── gulp/ # Все настройки Gulp-сборки, разделенные по отдельным файлам 16 | ├── src/ # Исходники 17 | │ ├── js # Скрипты 18 | │ │ └── main.js # Главный скрипт 19 | │ │ ├── _vars.js # файл с переменными проекта 20 | │ │ ├── _functions.js # файл с готовыми функциями на js 21 | │ │ ├── _components.js # файл с подключениями компонентов 22 | │ │ ├── components # js-компоненты 23 | │ ├── scss # Стили сайта (препроцессор sass в scss-синтаксисе) 24 | │ │ └── main.scss # Главный файл стилей 25 | │ │ └── vendor.scss # Файл для подключения стилей библиотек из папки vendor 26 | │ │ └── _fonts.scss # Файл для подключения шрифтов (можно использовать миксин) 27 | │ │ └── _mixins.scss # Файл для подключения миксинов из папки mixins 28 | │ │ └── _vars.scss # Файл для написания css- или scss-переменных 29 | │ │ └── _settings.scss # Файл для написания глобальных стилей 30 | │ │ ├── components # scss-компоненты 31 | │ │ ├── mixins # папка для сохранения готовых scss-компонентов 32 | │ │ ├── vendor # папка для хранения локальных css-стилей библиотек 33 | │ ├── partials # папка для хранения html-частей страницы 34 | │ ├── img # папка для хранения картинок 35 | │ │ ├── svg # специальная папка для преобразования svg в спрайт 36 | │ ├── resources # папка для хранения иных ассетов - php, видео-файлы, favicon и т.д. 37 | │ │ ├── fonts # папка для хранения шрифтов в формате woff2 38 | │ └── index.html # Главный html-файл 39 | └── gulpfile.js # Результирующий файл с настройками Gulp 40 | └── package.json # файл с настройками сборки и установленными пакетами 41 | └── .editorconfig # файл с настройками форматирования кода 42 | └── .ecrc # файл с настройками пакета editorconfig-checker (исключает ненужные папки) 43 | └── .stylelintrc.json # файл с настройками stylelint 44 | └── .stylelintignore # файл с исключениями для stylelint 45 | └── .htmlhintrc # файл с настройками htmlhint 46 | └── README.md # документация сборки 47 | ``` 48 | 49 | ## Оглавление 50 | 1. [npm-скрипты](#npm-скрипты) 51 | 2. [Работа с html](#работа-с-html) 52 | 3. [Работа с CSS](#работа-с-css) 53 | 4. [Работа с JavaScript](#работа-с-javascript) 54 | 5. [Работа со шрифтами](#работа-со-шрифтами) 55 | 6. [Работа с изображениями](#работа-с-изображениями) 56 | 7. [Работа с иными ресурсами](#работа-с-иными-ресурсами) 57 | 8. [Типограф](#типограф) 58 | 9. [Рекомендуемые плагины VS Code](#рекомендуемые-плагины-для-vs-code) 59 | 10. [Локальные сниппеты](#локальные-сниппеты) 60 | 11. [Готовые модули](#готовые-модули) 61 | 12. [Изменения в версии 3.0.0 (от 21.04.2024)](#изменения-в-версии-300-от-21042024) 62 | 13. [Заключение](#заключение) 63 | 64 | ## npm-скрипты 65 | 66 | Вызывать различные Gulp-задачи нужно __только__ через npm-команды, т.к. обычные Gulp-команды работают неполноценно. 67 | 68 | * `npm run stylelint` — команда, запускающая проверку всех scss-файлов на соответствие stylelint. 69 | * `npm run style-fix` — проверка и одновременный фикс scss-файлов на соответствие stylelint. Лично я сам все исправляю вручную, боясь, что автофикс что-то сломает. 70 | * `npm run code` — команда, запускающая проверку всех файлов на соответствие editorconfig. 71 | * `npm run dev` — базовая команда, которая запускает Gulp в режиме разработки. 72 | * `npm run build` — команда, запускающая продакшн-версию сборки. В эту команду также включена проверка stylelint и editorconfig, и если файлы не соответсвуют правилам - ваш проект не соберется. 73 | * `npm run cache` — команда, которую стоит запускать после npm run build__, если вам нужно загрузить новые файлы на хостинг без кэширования. 74 | * `npm run backend` — команда для бэкенд-сборки проекта. Она лишена ненужных вещей из dev-сборки, но не сжата, для удобства бэкендера. 75 | * `npm run zip` — команда собирает ваш готовый код в zip-архив. 76 | 77 | ## Работа с html 78 | 79 | Благодаря плагину __gulp-file-include__ вы можете разделять html-файл на различные шаблоны, которые должны храниться в папке __partials__. Удобно делить html-страницу на секции. 80 | 81 | > Для вставки html-частей в главный файл используйте `@include('partials/filename.html')` 82 | 83 | Если вы хотите создать многостраничный сайт - копируйте __index.html__, переименовывайте как вам нужно, и используйте. 84 | 85 | При использовании команды `npm run build`, вы получите минифицированный html-код в одну строку для всех html-файлов. 86 | 87 | ## Работа с CSS 88 | 89 | В сборке используется препроцессор __sass__ в синтаксисе __scss__. 90 | 91 | Стили, написанные в __components__, следует подключать в __main.scss__. 92 | __ВАЖНО:__ Обязательно удалить стили, которые написаны в __main.scss__ для `.page__body`. 93 | 94 | Чтобы подключить сторонние css-файлы (библиотеки) - положите их в папку __vendor__ и подключите в файле ___vendor.scss__ 95 | 96 | Если вы хотите создать свой миксин - делайте это в папке __mixins__, а затем подключайте в файл ___mixins.scss__. 97 | 98 | Если вы хотите использовать scss-переменные - подключите ___vars.scss__ также в main.scss или в любое другое место, где он нужен, но обязательно удалите __:root__. 99 | 100 | > Для подключения css-файлов используйте директиву `@import` 101 | 102 | В итоговой папке __app/css__ создаются два файла:
__main.css__ - для стилей страницы,
__vendor.css__ - для стилей всех библиотек, использующихся в проекте. 103 | 104 | При использовании команды `npm run build`, вы получите минифицированный css-код в одну строку для всех css-файлов. 105 | 106 | ## Работа с JavaScript 107 | 108 | Для сборки JS-кода используется webpack. 109 | 110 | JS-код лучше делить на компоненты - небольшие js-файлы, которые содержат свою, изолированную друг от друга реализацию. Такие файлы помещайте в папку __components__, а потом импортируйте в файл ___components.js__ 111 | 112 | В файле __vars.js__ должны храниться базовые переменные проекта, вроде нахождения элементов и т.д. 113 | 114 | В файле __main.js__ ничего менять не нужно, он сделан просто как результирующий. 115 | 116 | Подключать библиотеки в сборку можно только с помощью npm. Для этого установите нужный пакет с помощью его команды, создайте в папке __components__ файлик, импортируйте туда библиотеку и работайте с ней. Не забудьте импортировать этот файл в файл ___components.js__. 117 | 118 | При использовании команды `npm run build`, вы получите минифицированный js-код в одну строку для всех js-файлов. 119 | 120 | ## Работа со шрифтами 121 | 122 | В сборке реализована поддержка только формата __woff2__, т.к. другие форматы шрифтов не актуальны (это значит, что в миксине подключения шрифтов используется только данный формат). 123 | 124 | Загружайте файлы __woff2__ в папку __resources/fonts__, а затем вызывайте миксин `@font-face` в файле ___fonts.scss__. 125 | 126 | Также не забудьте прописать эти же шрифты в `` в html. 127 | 128 | ## Работа с изображениями 129 | 130 | Любые изображения, кроме __favicon__ кладите в папку __img__. 131 | 132 | Если вам нужно сделать svg-спрайт, кладите нужные для спрайта svg-файлы в папку __img/svg__. При этом, такие атрибуты как fill, stroke, style будут автоматически удаляться. Иные svg-файлы просто оставляйте в папке __img__. 133 | 134 | При использовании команды `npm run build`, вы получите минифицированные изображения в итоговой папке __img__. 135 | 136 | В сборке доступна поддержка __webp__ формата. Подключить его вы можете через тег `picture`. Для background можно использовать обычные __jpg__ или __png__, либо использовать `image-set` там, где это возможно. 137 | 138 | ## Работа с иными ресурсами 139 | 140 | Любые ресурсы (ассеты) проекта, под которые не отведена соответствующая папка, должны храниться в папке __resources__. Это могут быть видео-файлы, php-файлы (как, например, файл отправки формы), favicon и прочие. 141 | 142 | ## Типограф 143 | 144 | Для корректного отображения текста на странице был подключен плагин типограф, которые автоматически добавит неразрывные пробелы и иные символы, чтобы текст везде отображался по всем правилам русского языка. 145 | 146 | ## Рекомендуемые плагины для VS Code 147 | 148 | Я рекомендую использовать именно VS Code, и в сборке реализовано взаимодействие именно с этим редактором. Так, при открытии папки со сборкой в VS Code, редактор предложит вам ранее не установленные плагины, которые подойдут для корректной работы сборки. 149 | 150 | Самый важный из них – __projects snippets__, этот плагин "включает" локально написанные сниппеты для сборки. Данный плагин не всегда работает корректно, в этом случае просто перезапустите VS Code. 151 | 152 | ## Локальные сниппеты 153 | 154 | Для удобства и быстроты разработки были добавление локальные сниппеты (находятся в папке .vscode/snippets), которые работают благодаря плагину, описанному выше. Все сниппеты начинаются с префикса __g-__. В сниппетах пока только html (быстрое создание навигации, соцсетей, корректного тега picture с webp и avif и так далее). 155 | 156 | Некоторые сниппеты тесно связаны с scss-миксинами, например кнопка-бургер. Сниппет __g-burger__ создаст вам html-разметку бургера, а подключение миксина __@include burger__ добавит для него стили, что крайне удобно. 157 | 158 | ## Готовые модули 159 | 160 | В сборке присутствуют готовые, часто-используемые модули под различные задачи. Ниже будет перечислен уже добавленный функционал. 161 | 162 | __Внимание!__ В файле _functions.js_ описаны лишь подключения всех нужных модулей. Рекомендуется использовать все это в отдельных файлах. Например, если вам нужно создать модальное окно, создаете файл _modal.js_ в папке components, подключаете его в файл components.js и уже в файле _modal.js_ используете код подключения. 163 | 164 | ### Бургер меню 165 | 166 | Вы можете очень быстро добавить рабочий бургер к себе на страницу, для этого нужно: 167 | 168 | 1. В html вызвать сниппет `g-burger` 169 | 2. На ваше потенциальное меню в html добавить атрибут `data-menu` 170 | 3. В scss вызвать миксин `burger` 171 | 172 | ```scss 173 | .burger { @include burger } 174 | ``` 175 | 176 | 4. Зайти в файл js/_functions.js и скопировать строку с подключением js-файла бургера, после подключить в вашем файле для бургера. 177 | 5. Настроить стили показа меню под себя с помощью класса `menu--active` 178 | 179 | ### Модальное окно 180 | 181 | Вы можете очень быстро добавить рабочее модальное окно к себе на страницу, для этого нужно: 182 | 183 | 1. В html вызвать сниппет `g-graph-btn`. Он создаст кнопку для модального окна, ваша задача лишь заполнить атрибут `data-graph-path` 184 | 2. Далее вызвать сниппет `g-graph-modal`. Он создаст базовую разметку окна. Ваша задача - сделать окно по макету, заполнить контент и обязательно обозначить атрибут `data-graph-target` с тем же значением, что и у `data-graph-path` 185 | 3. Зайти в файл vendor.scss и раскомментировать строку с подключением файла graph-modal.min.css 186 | 4. Зайти в файл js/_functions.js и скопировать строку с импортом и подключением библиотеки `GraphModal`, после подключить в вашем файле для модального окна. 187 | 188 | ### Управление скроллом 189 | 190 | Вы можете отключать\включать скролл на странице (работает даже на iPhone). Для этого нужно: 191 | 192 | 1. Зайти в файл js/_functions.js и скопировать строку с импортом функций `disableScroll`, `enableScroll`. После подключить в вашем файле для отключения/включения скролла. 193 | __Важно!__. Если на странице присутствуют блоки с фиксированным позиционированием (например, шапка), добавьте ей класс `fixed-block`, чтобы этот блок не прыгал при отключении скролла. 194 | 195 | ### Табы 196 | 197 | Вы можете очень быстро добавить рабочие табы к себе на страницу, для этого нужно: 198 | 199 | 1. В html вызвать сниппет `g-tabs`. Он создаст разметку для табов, ваша задача лишь заполнить атрибут `data-tabs` 200 | 2. Для класса `.tabs` вызвать миксин `tabs` в scss (или же использовать подключение скрипта библиотеки из npm в файле vendor.scss) 201 | 4. Зайти в файл js/_functions.js и скопировать строку с импортом и подключением библиотеки `GraphTabs`, после подключить в вашем файле для табов. 202 | 203 | ### Валидация и отправка данных на почту 204 | 205 | Вы можете быстро настроить валидацию и отправку данных на почту. Как это использовать: 206 | 207 | 1. Создать форму, указав у нее уникальный класс. Также указать уникальные классы для полей ввода.
208 | 2. Создать массив, в котором будут переданы правила плагина just-validate, например: 209 | ```js 210 | const rules1 = [ 211 | { 212 | ruleSelector: '.input-name', 213 | rules: [ 214 | { 215 | rule: 'minLength', 216 | value: 3 217 | }, 218 | { 219 | rule: 'required', 220 | value: true, 221 | errorMessage: 'Заполните имя!' 222 | } 223 | ] 224 | }, 225 | { 226 | ruleSelector: '.input-tel', 227 | tel: true, 228 | telError: 'Введите корректный телефон', 229 | rules: [ 230 | { 231 | rule: 'required', 232 | value: true, 233 | errorMessage: 'Заполните телефон!' 234 | } 235 | ] 236 | }, 237 | ]; 238 | ``` 239 | __ВАЖНО__. Если в вашей форме есть поле с телефоном, обязательно пропишите в массиве с правилами новые поля: `tel: true, telError: 'Ошибка при вводе телефона'`. 240 | 3. Подключить функцию `validateForms`, она находится в _functions.js_, передав туда три параметра: 241 | 3.1. Строку с классом формы 242 | 3.2. Массив правил 243 | 3.3. Если нужно, можно создать свою функцию, которая выполнится после отправки, тогда ее тоже нужно будет передать как аргумент функции `validateForms`. 244 | 4. Также эта функция поддерживает работы с множественными чекбоксами/радиокнопками. Третьим параметром в функцию можно передать массив с настройками: 245 | ```js 246 | const checks = [ 247 | { 248 | selector: ".checkbox-group", 249 | errorMessage: "Выберите чекбоксы", 250 | } 251 | ]; 252 | ``` 253 | Пример кода: 254 | ```js 255 | import { validateForms } from './functions/validate-forms'; 256 | const checks = [ 257 | { 258 | selector: ".checkbox-group", 259 | errorMessage: "Выберите чекбоксы", 260 | } 261 | ]; 262 | const rules1 = [ 263 | { 264 | ruleSelector: '.input-name', 265 | rules: [ 266 | { 267 | rule: 'minLength', 268 | value: 3 269 | }, 270 | { 271 | rule: 'required', 272 | value: true, 273 | errorMessage: 'Заполните имя!' 274 | } 275 | ] 276 | }, 277 | { 278 | ruleSelector: '.input-tel', 279 | tel: true, 280 | telError: 'Введите корректный телефон', 281 | rules: [ 282 | { 283 | rule: 'required', 284 | value: true, 285 | errorMessage: 'Заполните телефон!' 286 | } 287 | ] 288 | }, 289 | ]; 290 | 291 | const afterForm = () => { 292 | console.log('Произошла отправка, тут можно писать любые действия'); 293 | }; 294 | 295 | validateForms('.form-1', rules1, checks, afterForm); 296 | ``` 297 | 298 | ### Throttle-функция 299 | 300 | Чтобы сгладить управление частоиспользуемыми событиями, вы можете использовать готовую функцию __throttle__. Для этого нужно: 301 | 302 | 1. В нужном месте импортировать функцию __throttle()__ 303 | 2. Написать нужную вам функцию, например, __func()__ 304 | 3. Создать переменную, в которую поместить вызов вашей фукнции внутри throttle, например: `let f = throttle(func)` 305 | 4. Использовать эту переменную как функцию в вызове, например: `window.addEventListener('resize', f)` 306 | 307 | ### Фикс фулскрин блоков 308 | 309 | Нередко блоки с высотой 100vh вызывают проблемы в мобильных браузерах. Решить это поможет готовый модуль fix-fullheight: 310 | 311 | 1. Раскомментируйте строку с импортом файла __fix-fullheight.js__ 312 | 2. Назначьте на нужный вам блок высоту не 100vh, а `var(--vh)` 313 | 314 | Для этой функции используется ранее упомянутый throttle. Вы можете убрать его, либо изменить время внутри файла __fix-fullheight.js__. 315 | 316 | ### Получение высоты шапки 317 | 318 | Иногда требуется получить точную высоту шапки, если она сделана абсолютным или фиксированным позиционированием, и для этого есть функция `getHeaderHeght`. Как ее использовать: 319 | 320 | 1. Раскомментируйте строку с импортом файла __header-height.js__ 321 | 2. Используйте css-переменную `--header-height` в нужном вам месте 322 | 323 | Необязательно использовать функции именно в файле __functions__, делайте как удобно вам. 324 | 325 | ### Кастомный скролл 326 | 327 | Для реализации кастомного скролла в сборку установлен плагин __simplebar.js__. Как его использовать: 328 | 329 | 1. Раскомментируйте строку с импортом плагина __simplebar__ 330 | 2. Добавьте нужному блоку максимальную высоту и атрибут `data-simplebar` 331 | 332 | ### Функции определения вьюпорта 333 | 334 | Вы можете запускать скрипты на определенной ширине (пока что поддержка ресайза не реализована) с помощью готовых функций `isMobile()`, `isTablet()`, `isDesktop()`. Для этого нужно лишь подключить нужную из них из файла, а затем использовать внутри условия `if`. 335 | 336 | ### Тултипы 337 | 338 | Вы можете быстро создать рабочий, доступный тултип, который к тому же будет сам рассчитывать отступы с помощью js. Как это использовать: 339 | 340 | 1. В html вызвать сниппет `g-tooltip`. Он создаст кнопку для модального окна, ваша задача лишь заполнить атрибуты `aria-describedby` и `id`. 341 | 2. Далее нужно подключить тултипы (код в файле _functions.js_), и вместо el передать id или class кнопки тултипа, а вместо tooltip передать id или class самого тултипа. 342 | 3. После этого можете стилизовать тултип как вам угодно. 343 | 344 | ### Слайдер 345 | 346 | Вы можете быстро создать рабочий swiper-слайдер. Как это использовать: 347 | 348 | 1. В html вызвать сниппет `g-swiper`. Он создаст базовую структуру свайпер-слайдера, вам нужно добавить свой класс для свайпер-контейнера. 349 | 2. Раскомментировать строку с подключением стилей в файле _vendor.scss_ 350 | 3. Подключить сам свайпер (код в файле _functions.js_) и использовать его, следуя документации. 351 | 352 | ### Анимации по скроллу 353 | 354 | Вы можете быстро набросать анимаций по скроллу с помощью плагина. Как это использовать: 355 | 356 | 1. Подключить код библиотеки AOS.js (код в файле _functions.js_) и инициализировать его. 357 | 2. С помощью атрибутов из документации плагина вызывать те или иные анимации, или написать свою. 358 | 359 | ### Параллакс по скроллу 360 | 361 | Вы можете быстро набросать параллакс элементов по скроллу с помощью плагина. Как это использовать: 362 | 363 | 1. Подключить код библиотеки rellax.js (код в файле _functions.js_) и инициализировать его, передав класс элемента (элементов). 364 | 2. Задать этот класс нужным элементам, а также использовать атрибуты из документации для кастомизации анимаций. 365 | 366 | ### Свайпы на мобильных устройствах 367 | 368 | Вы можете реализовывать различные взаимодействия со страницей через свайпы на мобильных устройствах с помощью плагина. Как это использовать: 369 | 370 | 1. Подключить код библиотеки swiped-events.js (код в файле _functions.js_). 371 | 2. Использовать различные события из библиотеки плагина. 372 | 373 | ### Миксин для flex-расстановки элементов. 374 | 375 | В последней версии сборки я добавил миксин flex-layout (можно найти в папке mixins), в котором реализована типичная сетка для карточек. Вы можете выбирать нужные вам настройки, чтобы сделать сетку быстро и без проблем. Например: 376 | 377 | ```html 378 |
379 |
Текст
380 |
Текст
381 |
Текст
382 |
Текст
383 |
Текст
384 |
Текст
385 |
386 | 387 | $options: ( 388 | parentClass: "cards", 389 | itemsClass: "cards__item", 390 | desktopGap: 30px, 391 | desktopElems: 3, 392 | tablet: "1024px", 393 | tabletElems: 2, 394 | tabletGap: 30px, 395 | mobile: "600px", 396 | mobileElems: 1, 397 | mobileGap: 20px 398 | ); 399 | 400 | @include flex-layout($options); 401 | ``` 402 | 403 | В опциях можно выбрать класс-родитель (или же флекс-контейнер, класс для потомков, какой у них будет отступ, на каких разрешениях будет меняться кол-во элементов). 404 | 405 | ## Изменения в версии 3.0.0 (от 21.04.2024) 406 | 407 | 1. Версия Gulp изменена на пятую. 408 | 2. Обновлены все нужные для работы пакеты. 409 | 3. Теперь сборка разделена на отдельные файлы, которые хранятся в папке __gulp__. 410 | 4. Удалены некоторые пакеты, такие как __smooth-scroll__ или __js-focus-visible__, т.к. уже не нужны в 2024 году. 411 | 5. Обновлен конфиг-файл __stylelint__, т.к. старый не работал с новой версией. 412 | 6. Немного обновлены базовые стили сборки: 413 | 6.1. min-width по умолчанию теперь 360, т.к. телефонов меньше почти нет. 414 | 6.2. __box-sizing: border-box__ задан глобально (без inherit), т.к. за все время использования сборки понял, что это лишено смысла. 415 | 6.3. по умолчанию у .page добавлено свойство __scroll-behavior__. 416 | 7. Немного обновлены скрипты и модули: 417 | 7.1. Изменился метод подключения __swiper__. 418 | 7.2. Изменился метод подключения __Inputmask__. 419 | 7.3. Изменилась функция валидации. 420 | 8. Все команды сборки теперь должны запускаться только через npm-команды. 421 | 9. Прочие мелочи. 422 | 423 | Тестировал сборку на своих рабочих проектах, все запускалось без проблем. 424 | 425 | ## Заключение 426 | 427 | Спасибо всем, кто использует сборку! Если вы заметили какую-либо ошибку, пришлите пожалуйста issue с подробным описанием проблемы, я все смотрю и постараюсь решить. Спасибо! 428 | -------------------------------------------------------------------------------- /gulp/config/paths.js: -------------------------------------------------------------------------------- 1 | const srcFolder = './src'; 2 | const buildFolder = './app'; 3 | 4 | export const paths = { 5 | base: { 6 | src: srcFolder, 7 | build: buildFolder, 8 | }, 9 | srcSvg: `${srcFolder}/img/svg/**.svg`, 10 | srcImgFolder: `${srcFolder}/img`, 11 | buildImgFolder: `${buildFolder}/img`, 12 | srcScss: `${srcFolder}/scss/**/*.scss`, 13 | buildCssFolder: `${buildFolder}/css`, 14 | srcFullJs: `${srcFolder}/js/**/*.js`, 15 | srcMainJs: `${srcFolder}/js/main.js`, 16 | buildJsFolder: `${buildFolder}/js`, 17 | srcPartialsFolder: `${srcFolder}/partials`, 18 | resourcesFolder: `${srcFolder}/resources`, 19 | }; 20 | -------------------------------------------------------------------------------- /gulp/tasks/cache.js: -------------------------------------------------------------------------------- 1 | import rev from "gulp-rev"; 2 | import revDel from "gulp-rev-delete-original"; 3 | 4 | export const cacheTask = () => { 5 | return app.gulp.src(`${app.paths.base.build}/**/*.{css,js,svg,png,jpg,jpeg,webp,woff2}`, { 6 | base: app.paths.base.build, 7 | encoding: false, 8 | }) 9 | .pipe(rev()) 10 | .pipe(revDel()) 11 | .pipe(app.gulp.dest(app.paths.base.build)) 12 | .pipe(rev.manifest('rev.json')) 13 | .pipe(app.gulp.dest(app.paths.base.build)); 14 | }; 15 | -------------------------------------------------------------------------------- /gulp/tasks/clean.js: -------------------------------------------------------------------------------- 1 | import { deleteAsync } from 'del'; 2 | 3 | export const clean = () => { 4 | return deleteAsync(app.paths.base.build); 5 | } 6 | -------------------------------------------------------------------------------- /gulp/tasks/html-include.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import fileInclude from "gulp-file-include"; 3 | import typograf from "gulp-typograf"; 4 | 5 | export const htmlInclude = () => { 6 | return app.gulp.src([`${app.paths.base.src}/*.html`]) 7 | .pipe(fileInclude({ 8 | prefix: '@', 9 | basepath: '@file', 10 | maxRecursion: 100 11 | })) 12 | .pipe(typograf({ 13 | locale: ['ru', 'en-US'] 14 | })) 15 | .pipe(app.gulp.dest(app.paths.base.build)) 16 | .pipe(browserSync.stream()); 17 | } 18 | -------------------------------------------------------------------------------- /gulp/tasks/html-minify.js: -------------------------------------------------------------------------------- 1 | import htmlmin from "gulp-htmlmin"; 2 | 3 | export const htmlMinify = () => { 4 | return app.gulp.src(`${app.paths.base.build}/**/*.html`) 5 | .pipe(htmlmin({ 6 | collapseWhitespace: true 7 | })) 8 | .pipe(app.gulp.dest(app.paths.base.build)); 9 | } 10 | -------------------------------------------------------------------------------- /gulp/tasks/images.js: -------------------------------------------------------------------------------- 1 | import gulpif from 'gulp-if'; 2 | import imagemin, { gifsicle, mozjpeg, optipng, svgo } from 'gulp-imagemin'; 3 | import newer from "gulp-newer"; 4 | 5 | export const images = () => { 6 | return app.gulp.src([`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png,svg}`], { encoding: false }) 7 | // .pipe(newer(app.paths.buildImgFolder)) 8 | // .pipe(gulpif(app.isProd, imagemin([ 9 | // gifsicle({ interlaced: true }), 10 | // mozjpeg({ quality: 75, progressive: true }), 11 | // optipng({ optimizationLevel: 2 }), 12 | // ]))) 13 | .pipe(app.gulp.dest(app.paths.buildImgFolder)) 14 | }; 15 | -------------------------------------------------------------------------------- /gulp/tasks/resources.js: -------------------------------------------------------------------------------- 1 | export const resources = () => { 2 | return app.gulp.src(`${app.paths.resourcesFolder}/**`, { encoding: false }) 3 | .pipe(app.gulp.dest(app.paths.base.build)) 4 | } 5 | -------------------------------------------------------------------------------- /gulp/tasks/rewrite.js: -------------------------------------------------------------------------------- 1 | import revRewrite from "gulp-rev-rewrite"; 2 | import { readFileSync } from "fs"; 3 | 4 | export const rewrite = () => { 5 | const manifest = readFileSync('app/rev.json'); 6 | 7 | app.gulp.src(`${app.paths.buildCssFolder}/*.css`) 8 | .pipe(revRewrite({ 9 | manifest 10 | })) 11 | .pipe(app.gulp.dest(app.paths.buildCssFolder)); 12 | return app.gulp.src(`${app.paths.base.build}/**/*.html`) 13 | .pipe(revRewrite({ 14 | manifest 15 | })) 16 | .pipe(app.gulp.dest(app.paths.base.build)); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /gulp/tasks/scripts-backend.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import webpackStream from 'webpack-stream'; 3 | import plumber from 'gulp-plumber'; 4 | import notify from 'gulp-notify'; 5 | 6 | export const scriptsBackend = () => { 7 | return app.gulp.src(app.paths.srcMainJs) 8 | .pipe(plumber( 9 | notify.onError({ 10 | title: "JS", 11 | message: "Error: <%= error.message %>" 12 | }) 13 | )) 14 | .pipe(webpackStream({ 15 | mode: 'development', 16 | output: { 17 | filename: 'main.js', 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.m?js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | ['@babel/preset-env', { 28 | targets: "defaults" 29 | }] 30 | ] 31 | } 32 | } 33 | }] 34 | }, 35 | devtool: false 36 | })) 37 | .on('error', function (err) { 38 | console.error('WEBPACK ERROR', err); 39 | this.emit('end'); 40 | }) 41 | .pipe(app.gulp.dest(app.paths.buildJsFolder)) 42 | .pipe(browserSync.stream()); 43 | } 44 | -------------------------------------------------------------------------------- /gulp/tasks/scripts.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import webpackStream from 'webpack-stream'; 3 | import plumber from 'gulp-plumber'; 4 | import notify from 'gulp-notify'; 5 | 6 | export const scripts = () => { 7 | return app.gulp.src(app.paths.srcMainJs) 8 | .pipe(plumber( 9 | notify.onError({ 10 | title: "JS", 11 | message: "Error: <%= error.message %>" 12 | }) 13 | )) 14 | .pipe(webpackStream({ 15 | mode: app.isProd ? 'production' : 'development', 16 | output: { 17 | filename: 'main.js', 18 | }, 19 | module: { 20 | rules: [{ 21 | test: /\.m?js$/, 22 | exclude: /node_modules/, 23 | use: { 24 | loader: 'babel-loader', 25 | options: { 26 | presets: [ 27 | ['@babel/preset-env', { 28 | targets: "defaults" 29 | }] 30 | ] 31 | } 32 | } 33 | }] 34 | }, 35 | devtool: !app.isProd ? 'source-map' : false 36 | })) 37 | .on('error', function (err) { 38 | console.error('WEBPACK ERROR', err); 39 | this.emit('end'); 40 | }) 41 | .pipe(app.gulp.dest(app.paths.buildJsFolder)) 42 | .pipe(browserSync.stream()); 43 | } 44 | -------------------------------------------------------------------------------- /gulp/tasks/sprite.js: -------------------------------------------------------------------------------- 1 | import svgSprite from "gulp-svg-sprite"; 2 | import svgmin from "gulp-svgmin"; 3 | import cheerio from 'gulp-cheerio'; 4 | import replace from 'gulp-replace'; 5 | 6 | export const svgSprites = () => { 7 | return app.gulp.src(app.paths.srcSvg, { encoding: false }) 8 | .pipe( 9 | svgmin({ 10 | js2svg: { 11 | pretty: true, 12 | }, 13 | }) 14 | ) 15 | .pipe( 16 | cheerio({ 17 | run: function ($) { 18 | $('[fill]').removeAttr('fill'); 19 | $('[stroke]').removeAttr('stroke'); 20 | $('[style]').removeAttr('style'); 21 | }, 22 | parserOptions: { 23 | xmlMode: true 24 | }, 25 | }) 26 | ) 27 | .pipe(replace('>', '>')) 28 | .pipe(svgSprite({ 29 | mode: { 30 | stack: { 31 | sprite: "../sprite.svg" 32 | } 33 | }, 34 | })) 35 | .pipe(app.gulp.dest(app.paths.buildImgFolder)); 36 | } 37 | -------------------------------------------------------------------------------- /gulp/tasks/styles-backend.js: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import * as dartSass from 'sass'; 3 | import gulpSass from 'gulp-sass'; 4 | import plumber from 'gulp-plumber'; 5 | import autoprefixer from 'gulp-autoprefixer'; 6 | import notify from 'gulp-notify'; 7 | 8 | const sass = gulpSass(dartSass); 9 | 10 | export const stylesBackend = () => { 11 | return app.gulp.src(app.paths.srcScss) 12 | .pipe(plumber( 13 | notify.onError({ 14 | title: "SCSS", 15 | message: "Error: <%= error.message %>" 16 | }) 17 | )) 18 | .pipe(sass()) 19 | .pipe(autoprefixer({ 20 | cascade: false, 21 | grid: true, 22 | overrideBrowserslist: ["last 5 versions"] 23 | })) 24 | .pipe(app.gulp.dest(app.paths.buildCssFolder)) 25 | .pipe(browserSync.stream()); 26 | }; 27 | -------------------------------------------------------------------------------- /gulp/tasks/styles.js: -------------------------------------------------------------------------------- 1 | import gulpif from 'gulp-if'; 2 | import browserSync from 'browser-sync'; 3 | import cleanCSS from 'gulp-clean-css'; 4 | import * as dartSass from 'sass'; 5 | import gulpSass from 'gulp-sass'; 6 | import plumber from 'gulp-plumber'; 7 | import autoprefixer from 'gulp-autoprefixer'; 8 | import notify from 'gulp-notify'; 9 | 10 | const sass = gulpSass(dartSass); 11 | 12 | export const styles = () => { 13 | return app.gulp.src(app.paths.srcScss, { sourcemaps: !app.isProd }) 14 | .pipe(plumber( 15 | notify.onError({ 16 | title: "SCSS", 17 | message: "Error: <%= error.message %>" 18 | }) 19 | )) 20 | .pipe(sass()) 21 | .pipe(autoprefixer({ 22 | cascade: false, 23 | grid: true, 24 | overrideBrowserslist: ["last 5 versions"] 25 | })) 26 | .pipe(gulpif(app.isProd, cleanCSS({ 27 | level: 2 28 | }))) 29 | .pipe(app.gulp.dest(app.paths.buildCssFolder, { sourcemaps: '.' })) 30 | .pipe(browserSync.stream()); 31 | }; 32 | -------------------------------------------------------------------------------- /gulp/tasks/webp.js: -------------------------------------------------------------------------------- 1 | import webp from 'gulp-webp'; 2 | 3 | export const webpImages = () => { 4 | return app.gulp.src([`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png}`], { encoding: false }) 5 | .pipe(webp()) 6 | .pipe(app.gulp.dest(app.paths.buildImgFolder)) 7 | }; 8 | -------------------------------------------------------------------------------- /gulp/tasks/zip.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import zip from 'gulp-zip'; 3 | import { deleteAsync } from 'del'; 4 | import notify from 'gulp-notify'; 5 | import plumber from 'gulp-plumber'; 6 | 7 | const rootFolder = path.basename(path.resolve()); 8 | 9 | export const zipFiles = () => { 10 | deleteAsync([`${app.paths.base.build}/*.zip`]); 11 | return app.gulp.src(`${app.paths.base.build}/**/*.*`, { encoding: false }) 12 | .pipe(plumber( 13 | notify.onError({ 14 | title: "ZIP", 15 | message: "Error: <%= error.message %>" 16 | }) 17 | )) 18 | .pipe(zip(`${rootFolder}.zip`)) 19 | .pipe(app.gulp.dest(app.paths.base.build)); 20 | } 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp'; 2 | import browserSync from 'browser-sync'; 3 | 4 | import { paths } from './gulp/config/paths.js'; 5 | import { clean } from './gulp/tasks/clean.js'; 6 | import { svgSprites } from './gulp/tasks/sprite.js'; 7 | import { styles } from './gulp/tasks/styles.js'; 8 | import { stylesBackend } from './gulp/tasks/styles-backend.js'; 9 | import { scripts } from './gulp/tasks/scripts.js'; 10 | import { scriptsBackend } from './gulp/tasks/scripts-backend.js'; 11 | import { resources } from './gulp/tasks/resources.js'; 12 | import { images } from './gulp/tasks/images.js'; 13 | import { webpImages } from './gulp/tasks/webp.js'; 14 | import { htmlInclude } from './gulp/tasks/html-include.js'; 15 | import { cacheTask } from './gulp/tasks/cache.js'; 16 | import { rewrite } from './gulp/tasks/rewrite.js'; 17 | import { htmlMinify } from './gulp/tasks/html-minify.js'; 18 | import { zipFiles } from './gulp/tasks/zip.js'; 19 | 20 | global.app = { 21 | gulp, 22 | isProd: process.argv.includes('--build'), 23 | paths, 24 | } 25 | 26 | const watcher = () => { 27 | browserSync.init({ 28 | server: { 29 | baseDir: `${app.paths.base.build}` 30 | }, 31 | notify: false, 32 | port: 3000, 33 | }); 34 | 35 | gulp.watch(app.paths.srcScss, styles); 36 | gulp.watch(app.paths.srcFullJs, scripts); 37 | gulp.watch(`${app.paths.srcPartialsFolder}/*.html`, htmlInclude); 38 | gulp.watch(`${app.paths.base.src}/*.html`, htmlInclude); 39 | gulp.watch(`${app.paths.resourcesFolder}/**`, resources); 40 | gulp.watch(`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png,svg}`, images); 41 | gulp.watch(`${app.paths.srcImgFolder}/**/**.{jpg,jpeg,png}`, webpImages); 42 | gulp.watch(app.paths.srcSvg, svgSprites); 43 | } 44 | 45 | const dev = gulp.series(clean, htmlInclude, scripts, styles, resources, images, webpImages, svgSprites, watcher); 46 | const backend = gulp.series(clean, htmlInclude, scriptsBackend, stylesBackend, resources, images, webpImages, svgSprites); 47 | const build = gulp.series(clean, htmlInclude, scripts, styles, resources, images, webpImages, svgSprites, htmlMinify); 48 | const cache = gulp.series(cacheTask, rewrite); 49 | const zip = zipFiles; 50 | 51 | export { dev } 52 | export { build } 53 | export { backend } 54 | export { cache } 55 | export { zip } 56 | 57 | gulp.task('default', dev); 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-builder", 3 | "version": "3.0.0", 4 | "description": "gulp-builder for weblayout", 5 | "main": "gulpfile.js", 6 | "type": "module", 7 | "scripts": { 8 | "stylelint": "npx stylelint **/*.scss", 9 | "style-fix": "npx stylelint **/*.scss --fix", 10 | "code": "editorconfig-checker", 11 | "dev": "gulp", 12 | "build": "npm run stylelint && npm run code && gulp build --build", 13 | "cache": "gulp cache", 14 | "backend": "gulp backend", 15 | "zip": "gulp zip --build" 16 | }, 17 | "author": "MaxGraph", 18 | "license": "ISC", 19 | "devDependencies": { 20 | "@babel/core": "^7.24.4", 21 | "@babel/preset-env": "^7.24.4", 22 | "babel-loader": "^9.1.3", 23 | "browser-sync": "^3.0.2", 24 | "del": "^7.1.0", 25 | "editorconfig-checker": "^5.1.5", 26 | "gulp": "^5.0.0", 27 | "gulp-autoprefixer": "^9.0.0", 28 | "gulp-cheerio": "^1.0.0", 29 | "gulp-clean-css": "^4.3.0", 30 | "gulp-cli": "^3.0.0", 31 | "gulp-file-include": "^2.3.0", 32 | "gulp-htmlmin": "^5.0.1", 33 | "gulp-if": "^3.0.0", 34 | "gulp-imagemin": "^9.0.0", 35 | "gulp-newer": "^1.4.0", 36 | "gulp-notify": "^4.0.0", 37 | "gulp-plumber": "^1.2.1", 38 | "gulp-replace": "^1.1.4", 39 | "gulp-rev": "^11.0.0", 40 | "gulp-rev-delete-original": "^0.2.3", 41 | "gulp-rev-rewrite": "^6.0.0", 42 | "gulp-sass": "^5.1.0", 43 | "gulp-svg-sprite": "^2.0.3", 44 | "gulp-svgmin": "^4.1.0", 45 | "gulp-typograf": "^4.1.0", 46 | "gulp-webp": "^5.0.0", 47 | "gulp-zip": "^6.0.0", 48 | "postcss": "^8.4.38", 49 | "postcss-scss": "^4.0.9", 50 | "sass": "^1.75.0", 51 | "stylelint": "^16.3.1", 52 | "stylelint-config-standard-scss": "^13.1.0", 53 | "stylelint-order": "^6.0.4", 54 | "typograf": "^7.4.0", 55 | "webpack": "^5.91.0", 56 | "webpack-stream": "^7.0.0" 57 | }, 58 | "dependencies": { 59 | "@popperjs/core": "^2.11.8", 60 | "aos": "^2.3.4", 61 | "graph-modal": "^1.0.7", 62 | "graph-tabs": "^1.0.2", 63 | "inputmask": "^5.0.8", 64 | "just-validate": "^4.3.0", 65 | "rellax": "^1.12.1", 66 | "simplebar": "^6.2.5", 67 | "swiped-events": "^1.1.9", 68 | "swiper": "^11.1.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/img/cover.jpg -------------------------------------------------------------------------------- /src/img/svg/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @include('partials/head.html') 5 | 6 | 7 |
8 | @include('partials/header.html') 9 |
10 | 11 | 12 | Обложка канала MaxGraph 13 | 14 |
15 | @include('partials/footer.html') 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/js/_components.js: -------------------------------------------------------------------------------- 1 | console.log('components'); 2 | -------------------------------------------------------------------------------- /src/js/_functions.js: -------------------------------------------------------------------------------- 1 | // Данный файл - лишь собрание подключений готовых компонентов. 2 | // Рекомендуется создавать отдельный файл в папке components и подключать все там 3 | 4 | // Определение операционной системы на мобильных 5 | // import { mobileCheck } from "./functions/mobile-check.js"; 6 | // console.log(mobileCheck()) 7 | 8 | // Определение ширины экрана 9 | // import { isMobile, isTablet, isDesktop } from './functions/check-viewport'; 10 | // if (isDesktop()) { 11 | // console.log('...') 12 | // } 13 | 14 | // Троттлинг функции (для ресайза, ввода в инпут, скролла и т.д.) 15 | // import { throttle } from './functions/throttle'; 16 | // let yourFunc = () => { console.log('throttle') }; 17 | // let func = throttle(yourFunc); 18 | // window.addEventListener('resize', func); 19 | 20 | // Фикс фулскрин-блоков 21 | // import './functions/fix-fullheight'; 22 | 23 | // Реализация бургер-меню 24 | // import { burger } from './functions/burger'; 25 | 26 | // Реализация остановки скролла (не забудьте вызвать функцию) 27 | // import { disableScroll } from './functions/disable-scroll'; 28 | 29 | // Реализация включения скролла (не забудьте вызвать функцию) 30 | // import { enableScroll } from './functions/enable-scroll'; 31 | 32 | // Реализация модального окна 33 | // import GraphModal from 'graph-modal'; 34 | // const modal = new GraphModal(); 35 | 36 | // Реализация табов 37 | // import GraphTabs from 'graph-tabs'; 38 | // const tabs = new GraphTabs('tab'); 39 | 40 | // Получение высоты шапки сайта (не забудьте вызвать функцию) 41 | // import { getHeaderHeight } from './functions/header-height'; 42 | 43 | // Подключение плагина кастом-скролла 44 | // import 'simplebar'; 45 | 46 | // Подключение плагина для позиционирования тултипов 47 | // import { createPopper, right} from '@popperjs/core'; 48 | // createPopper(el, tooltip, { 49 | // placement: 'right' 50 | // }); 51 | 52 | // Подключение свайпера 53 | // import Swiper from 'swiper'; 54 | // import { Navigation, Pagination } from 'swiper/modules'; 55 | // Swiper.use([Navigation, Pagination]); 56 | // const swiper = new Swiper(el, { 57 | // slidesPerView: 'auto', 58 | // }); 59 | 60 | // Подключение анимаций по скроллу 61 | // import AOS from 'aos'; 62 | // AOS.init(); 63 | 64 | // Подключение параллакса блоков при скролле 65 | // import Rellax from 'rellax'; 66 | // const rellax = new Rellax('.rellax'); 67 | 68 | // Подключение плавной прокрутки к якорям 69 | // import SmoothScroll from 'smooth-scroll'; 70 | // const scroll = new SmoothScroll('a[href*="#"]'); 71 | 72 | // Подключение событий свайпа на мобильных 73 | // import 'swiped-events'; 74 | // document.addEventListener('swiped', function(e) { 75 | // console.log(e.target); 76 | // console.log(e.detail); 77 | // console.log(e.detail.dir); 78 | // }); 79 | 80 | // import { validateForms } from './functions/validate-forms'; 81 | // const rules1 = [...]; 82 | 83 | // const afterForm = () => { 84 | // console.log('Произошла отправка, тут можно писать любые действия'); 85 | // }; 86 | 87 | // validateForms('.form-1', rules1, afterForm); 88 | -------------------------------------------------------------------------------- /src/js/_vars.js: -------------------------------------------------------------------------------- 1 | export default { 2 | windowEl: window, 3 | documentEl: document, 4 | htmlEl: document.documentElement, 5 | bodyEl: document.body, 6 | } 7 | -------------------------------------------------------------------------------- /src/js/components/ex.js: -------------------------------------------------------------------------------- 1 | console.log('maxgraph'); 2 | -------------------------------------------------------------------------------- /src/js/functions/burger.js: -------------------------------------------------------------------------------- 1 | import { disableScroll } from '../functions/disable-scroll.js'; 2 | import { enableScroll } from '../functions/enable-scroll.js'; 3 | 4 | (function(){ 5 | const burger = document?.querySelector('[data-burger]'); 6 | const menu = document?.querySelector('[data-menu]'); 7 | const menuItems = document?.querySelectorAll('[data-menu-item]'); 8 | const overlay = document?.querySelector('[data-menu-overlay]'); 9 | 10 | burger?.addEventListener('click', (e) => { 11 | burger?.classList.toggle('burger--active'); 12 | menu?.classList.toggle('menu--active'); 13 | 14 | if (menu?.classList.contains('menu--active')) { 15 | burger?.setAttribute('aria-expanded', 'true'); 16 | burger?.setAttribute('aria-label', 'Закрыть меню'); 17 | disableScroll(); 18 | } else { 19 | burger?.setAttribute('aria-expanded', 'false'); 20 | burger?.setAttribute('aria-label', 'Открыть меню'); 21 | enableScroll(); 22 | } 23 | }); 24 | 25 | overlay?.addEventListener('click', () => { 26 | burger?.setAttribute('aria-expanded', 'false'); 27 | burger?.setAttribute('aria-label', 'Открыть меню'); 28 | burger.classList.remove('burger--active'); 29 | menu.classList.remove('menu--active'); 30 | enableScroll(); 31 | }); 32 | 33 | menuItems?.forEach(el => { 34 | el.addEventListener('click', () => { 35 | burger?.setAttribute('aria-expanded', 'false'); 36 | burger?.setAttribute('aria-label', 'Открыть меню'); 37 | burger.classList.remove('burger--active'); 38 | menu.classList.remove('menu--active'); 39 | enableScroll(); 40 | }); 41 | }); 42 | })(); 43 | -------------------------------------------------------------------------------- /src/js/functions/check-viewport.js: -------------------------------------------------------------------------------- 1 | export const isMobile = () => { 2 | if (window.innerWidth < 768) { 3 | return true; 4 | } 5 | 6 | return false; 7 | }; 8 | 9 | export const isTablet = () => { 10 | if (window.innerWidth >= 769 && window.innerWidth <= 1024) { 11 | return true; 12 | } 13 | 14 | return false; 15 | }; 16 | 17 | export const isDesktop = () => { 18 | if (window.innerWidth > 1025) { 19 | return true; 20 | } 21 | 22 | return false; 23 | }; 24 | -------------------------------------------------------------------------------- /src/js/functions/disable-scroll.js: -------------------------------------------------------------------------------- 1 | import vars from '../_vars.js'; 2 | 3 | export const disableScroll = () => { 4 | const fixBlocks = document?.querySelectorAll('.fixed-block'); 5 | const pagePosition = window.scrollY; 6 | const paddingOffset = `${(window.innerWidth - vars.bodyEl.offsetWidth)}px`; 7 | 8 | vars.htmlEl.style.scrollBehavior = 'none'; 9 | fixBlocks.forEach(el => { el.style.paddingRight = paddingOffset; }); 10 | vars.bodyEl.style.paddingRight = paddingOffset; 11 | vars.bodyEl.classList.add('dis-scroll'); 12 | vars.bodyEl.dataset.position = pagePosition; 13 | vars.bodyEl.style.top = `-${pagePosition}px`; 14 | } 15 | -------------------------------------------------------------------------------- /src/js/functions/enable-scroll.js: -------------------------------------------------------------------------------- 1 | import vars from '../_vars.js'; 2 | 3 | export const enableScroll = () => { 4 | const fixBlocks = document?.querySelectorAll('.fixed-block'); 5 | const body = document.body; 6 | const pagePosition = parseInt(vars.bodyEl.dataset.position, 10); 7 | fixBlocks.forEach(el => { el.style.paddingRight = '0px'; }); 8 | vars.bodyEl.style.paddingRight = '0px'; 9 | 10 | vars.bodyEl.style.top = 'auto'; 11 | vars.bodyEl.classList.remove('dis-scroll'); 12 | window.scroll({ 13 | top: pagePosition, 14 | left: 0 15 | }); 16 | vars.bodyEl.removeAttribute('data-position'); 17 | vars.htmlEl.style.scrollBehavior = 'smooth'; 18 | } 19 | -------------------------------------------------------------------------------- /src/js/functions/fix-fullheight.js: -------------------------------------------------------------------------------- 1 | import { throttle } from './throttle.js'; 2 | 3 | const fixFullheight = () => { 4 | let vh = window.innerHeight; 5 | document.documentElement.style.setProperty('--vh', `${vh}px`); 6 | }; 7 | 8 | let fixHeight = throttle(fixFullheight); 9 | 10 | fixHeight(); 11 | 12 | window.addEventListener('resize', fixHeight); 13 | -------------------------------------------------------------------------------- /src/js/functions/header-height.js: -------------------------------------------------------------------------------- 1 | export const getHeaderHeight = () => { 2 | const headerHeight = document?.querySelector('.header').offsetHeight; 3 | document.querySelector(':root').style.setProperty('--header-height', `${headerHeight}px`); 4 | } 5 | -------------------------------------------------------------------------------- /src/js/functions/mobile-check.js: -------------------------------------------------------------------------------- 1 | import vars from '../_vars.js'; 2 | 3 | export const mobileCheck = () => { 4 | const userAgent = navigator.userAgent || navigator.vendor || window.opera; 5 | 6 | if (/android/i.test(userAgent)) { 7 | vars.htmlEl.classList.add('page--android'); 8 | return "Android"; 9 | } 10 | 11 | if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) { 12 | vars.htmlEl.classList.add('page--ios'); 13 | return "iOS"; 14 | } 15 | 16 | return "unknown"; 17 | }; 18 | -------------------------------------------------------------------------------- /src/js/functions/throttle.js: -------------------------------------------------------------------------------- 1 | export const throttle = (func, delay = 250) => { 2 | let isThrottled = false; 3 | let savedArgs = null; 4 | let savedThis = null; 5 | 6 | return function wrap(...args) { 7 | if (isThrottled) { 8 | savedArgs = args, 9 | savedThis = this; 10 | return; 11 | } 12 | 13 | func.apply(this, args); 14 | isThrottled = true; 15 | 16 | setTimeout(() => { 17 | isThrottled = false; 18 | 19 | if (savedThis) { 20 | wrap.apply(savedThis, savedArgs); 21 | savedThis = null; 22 | savedArgs = null; 23 | } 24 | 25 | }, delay); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/js/functions/validate-forms.js: -------------------------------------------------------------------------------- 1 | import JustValidate from 'just-validate'; 2 | import Inputmask from "../../../node_modules/inputmask/dist/inputmask.es6.js"; 3 | 4 | export const validateForms = (selector, rules, checkboxes = [], afterSend) => { 5 | const form = document?.querySelector(selector); 6 | const telSelector = form?.querySelector('input[type="tel"]'); 7 | 8 | if (!form) { 9 | console.error('Нет такого селектора!'); 10 | return false; 11 | } 12 | 13 | if (!rules) { 14 | console.error('Вы не передали правила валидации!'); 15 | return false; 16 | } 17 | 18 | if (telSelector) { 19 | const inputMask = new Inputmask('+7 (999) 999-99-99'); 20 | inputMask.mask(telSelector); 21 | 22 | for (let item of rules) { 23 | if (item.tel) { 24 | item.rules.push({ 25 | rule: 'function', 26 | validator: function() { 27 | const phone = telSelector.inputmask.unmaskedvalue(); 28 | return phone.length === 10; 29 | }, 30 | errorMessage: item.telError 31 | }); 32 | } 33 | } 34 | } 35 | 36 | const validation = new JustValidate(selector); 37 | 38 | for (let item of rules) { 39 | validation 40 | .addField(item.ruleSelector, item.rules); 41 | } 42 | 43 | if (checkboxes.length) { 44 | for (let item of checkboxes) { 45 | validation 46 | .addRequiredGroup( 47 | `${item.selector}`, 48 | `${item.errorMessage}` 49 | ) 50 | } 51 | } 52 | 53 | validation.onSuccess((ev) => { 54 | let formData = new FormData(ev.target); 55 | 56 | let xhr = new XMLHttpRequest(); 57 | 58 | xhr.onreadystatechange = function () { 59 | if (xhr.readyState === 4) { 60 | if (xhr.status === 200) { 61 | if (afterSend) { 62 | afterSend(); 63 | } 64 | console.log('Отправлено'); 65 | } 66 | } 67 | } 68 | 69 | xhr.open('POST', 'mail.php', true); 70 | xhr.send(formData); 71 | 72 | ev.target.reset(); 73 | }) 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import './_components.js'; 2 | -------------------------------------------------------------------------------- /src/partials/footer.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/partials/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MaxGraph New Site 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/partials/header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /src/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/resources/favicon.ico -------------------------------------------------------------------------------- /src/resources/fonts/MullerRegular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxdenaro/gulp-maxgraph/2dc43cdb23ed1726c30df61325ccae381846eeda/src/resources/fonts/MullerRegular.woff2 -------------------------------------------------------------------------------- /src/resources/mail.php: -------------------------------------------------------------------------------- 1 | $value ) { 14 | if ( $value != "" && $key != "project_name" && $key != "admin_email" && $key != "form_subject" ) { 15 | $body .= " 16 | " . ( ($c = !$c) ? '':'' ) . " 17 | $key 18 | $value 19 | 20 | "; 21 | } 22 | } 23 | 24 | $body = "$body
"; 25 | 26 | // Настройки PHPMailer 27 | $mail = new PHPMailer\PHPMailer\PHPMailer(); 28 | 29 | try { 30 | $mail->isSMTP(); 31 | $mail->CharSet = "UTF-8"; 32 | $mail->SMTPAuth = true; 33 | 34 | // Настройки вашей почты 35 | $mail->Host = 'smtp.gmail.com'; // SMTP сервера вашей почты 36 | $mail->Username = ''; // Логин на почте 37 | $mail->Password = ''; // Пароль на почте 38 | $mail->SMTPSecure = 'ssl'; 39 | $mail->Port = 465; 40 | 41 | $mail->setFrom('', 'Заявка с вашего сайта'); // Адрес самой почты и имя отправителя 42 | 43 | // Получатель письма 44 | $mail->addAddress(''); 45 | 46 | // Прикрипление файлов к письму 47 | if (!empty($file['name'][0])) { 48 | for ($ct = 0; $ct < count($file['tmp_name']); $ct++) { 49 | $uploadfile = tempnam(sys_get_temp_dir(), sha1($file['name'][$ct])); 50 | $filename = $file['name'][$ct]; 51 | if (move_uploaded_file($file['tmp_name'][$ct], $uploadfile)) { 52 | $mail->addAttachment($uploadfile, $filename); 53 | $rfile[] = "Файл $filename прикреплён"; 54 | } else { 55 | $rfile[] = "Не удалось прикрепить файл $filename"; 56 | } 57 | } 58 | } 59 | 60 | // Отправка сообщения 61 | $mail->isHTML(true); 62 | $mail->Subject = $title; 63 | $mail->Body = $body; 64 | 65 | $mail->send(); 66 | 67 | } catch (Exception $e) { 68 | $status = "Сообщение не было отправлено. Причина ошибки: {$mail->ErrorInfo}"; 69 | } 70 | -------------------------------------------------------------------------------- /src/resources/phpmailer/Exception.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | /** 25 | * PHPMailer exception handler. 26 | * 27 | * @author Marcus Bointon 28 | */ 29 | class Exception extends \Exception 30 | { 31 | /** 32 | * Prettify error message output. 33 | * 34 | * @return string 35 | */ 36 | public function errorMessage() 37 | { 38 | return '' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "
\n"; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/resources/phpmailer/SMTP.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Jim Jagielski (jimjag) 11 | * @author Andy Prevost (codeworxtech) 12 | * @author Brent R. Matzelle (original founder) 13 | * @copyright 2012 - 2020 Marcus Bointon 14 | * @copyright 2010 - 2012 Jim Jagielski 15 | * @copyright 2004 - 2009 Andy Prevost 16 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License 17 | * @note This program is distributed in the hope that it will be useful - WITHOUT 18 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 19 | * FITNESS FOR A PARTICULAR PURPOSE. 20 | */ 21 | 22 | namespace PHPMailer\PHPMailer; 23 | 24 | /** 25 | * PHPMailer RFC821 SMTP email transport class. 26 | * Implements RFC 821 SMTP commands and provides some utility methods for sending mail to an SMTP server. 27 | * 28 | * @author Chris Ryan 29 | * @author Marcus Bointon 30 | */ 31 | class SMTP 32 | { 33 | /** 34 | * The PHPMailer SMTP version number. 35 | * 36 | * @var string 37 | */ 38 | const VERSION = '6.5.3'; 39 | 40 | /** 41 | * SMTP line break constant. 42 | * 43 | * @var string 44 | */ 45 | const LE = "\r\n"; 46 | 47 | /** 48 | * The SMTP port to use if one is not specified. 49 | * 50 | * @var int 51 | */ 52 | const DEFAULT_PORT = 25; 53 | 54 | /** 55 | * The maximum line length allowed by RFC 5321 section 4.5.3.1.6, 56 | * *excluding* a trailing CRLF break. 57 | * 58 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6 59 | * 60 | * @var int 61 | */ 62 | const MAX_LINE_LENGTH = 998; 63 | 64 | /** 65 | * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5, 66 | * *including* a trailing CRLF line break. 67 | * 68 | * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5 69 | * 70 | * @var int 71 | */ 72 | const MAX_REPLY_LENGTH = 512; 73 | 74 | /** 75 | * Debug level for no output. 76 | * 77 | * @var int 78 | */ 79 | const DEBUG_OFF = 0; 80 | 81 | /** 82 | * Debug level to show client -> server messages. 83 | * 84 | * @var int 85 | */ 86 | const DEBUG_CLIENT = 1; 87 | 88 | /** 89 | * Debug level to show client -> server and server -> client messages. 90 | * 91 | * @var int 92 | */ 93 | const DEBUG_SERVER = 2; 94 | 95 | /** 96 | * Debug level to show connection status, client -> server and server -> client messages. 97 | * 98 | * @var int 99 | */ 100 | const DEBUG_CONNECTION = 3; 101 | 102 | /** 103 | * Debug level to show all messages. 104 | * 105 | * @var int 106 | */ 107 | const DEBUG_LOWLEVEL = 4; 108 | 109 | /** 110 | * Debug output level. 111 | * Options: 112 | * * self::DEBUG_OFF (`0`) No debug output, default 113 | * * self::DEBUG_CLIENT (`1`) Client commands 114 | * * self::DEBUG_SERVER (`2`) Client commands and server responses 115 | * * self::DEBUG_CONNECTION (`3`) As DEBUG_SERVER plus connection status 116 | * * self::DEBUG_LOWLEVEL (`4`) Low-level data output, all messages. 117 | * 118 | * @var int 119 | */ 120 | public $do_debug = self::DEBUG_OFF; 121 | 122 | /** 123 | * How to handle debug output. 124 | * Options: 125 | * * `echo` Output plain-text as-is, appropriate for CLI 126 | * * `html` Output escaped, line breaks converted to `
`, appropriate for browser output 127 | * * `error_log` Output to error log as configured in php.ini 128 | * Alternatively, you can provide a callable expecting two params: a message string and the debug level: 129 | * 130 | * ```php 131 | * $smtp->Debugoutput = function($str, $level) {echo "debug level $level; message: $str";}; 132 | * ``` 133 | * 134 | * Alternatively, you can pass in an instance of a PSR-3 compatible logger, though only `debug` 135 | * level output is used: 136 | * 137 | * ```php 138 | * $mail->Debugoutput = new myPsr3Logger; 139 | * ``` 140 | * 141 | * @var string|callable|\Psr\Log\LoggerInterface 142 | */ 143 | public $Debugoutput = 'echo'; 144 | 145 | /** 146 | * Whether to use VERP. 147 | * 148 | * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path 149 | * @see http://www.postfix.org/VERP_README.html Info on VERP 150 | * 151 | * @var bool 152 | */ 153 | public $do_verp = false; 154 | 155 | /** 156 | * The timeout value for connection, in seconds. 157 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. 158 | * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. 159 | * 160 | * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 161 | * 162 | * @var int 163 | */ 164 | public $Timeout = 300; 165 | 166 | /** 167 | * How long to wait for commands to complete, in seconds. 168 | * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. 169 | * 170 | * @var int 171 | */ 172 | public $Timelimit = 300; 173 | 174 | /** 175 | * Patterns to extract an SMTP transaction id from reply to a DATA command. 176 | * The first capture group in each regex will be used as the ID. 177 | * MS ESMTP returns the message ID, which may not be correct for internal tracking. 178 | * 179 | * @var string[] 180 | */ 181 | protected $smtp_transaction_id_patterns = [ 182 | 'exim' => '/[\d]{3} OK id=(.*)/', 183 | 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', 184 | 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', 185 | 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', 186 | 'Amazon_SES' => '/[\d]{3} Ok (.*)/', 187 | 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', 188 | 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', 189 | 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 190 | 'Mailjet' => '/[\d]{3} OK queued as (.*)/', 191 | ]; 192 | 193 | /** 194 | * The last transaction ID issued in response to a DATA command, 195 | * if one was detected. 196 | * 197 | * @var string|bool|null 198 | */ 199 | protected $last_smtp_transaction_id; 200 | 201 | /** 202 | * The socket for the server connection. 203 | * 204 | * @var ?resource 205 | */ 206 | protected $smtp_conn; 207 | 208 | /** 209 | * Error information, if any, for the last SMTP command. 210 | * 211 | * @var array 212 | */ 213 | protected $error = [ 214 | 'error' => '', 215 | 'detail' => '', 216 | 'smtp_code' => '', 217 | 'smtp_code_ex' => '', 218 | ]; 219 | 220 | /** 221 | * The reply the server sent to us for HELO. 222 | * If null, no HELO string has yet been received. 223 | * 224 | * @var string|null 225 | */ 226 | protected $helo_rply; 227 | 228 | /** 229 | * The set of SMTP extensions sent in reply to EHLO command. 230 | * Indexes of the array are extension names. 231 | * Value at index 'HELO' or 'EHLO' (according to command that was sent) 232 | * represents the server name. In case of HELO it is the only element of the array. 233 | * Other values can be boolean TRUE or an array containing extension options. 234 | * If null, no HELO/EHLO string has yet been received. 235 | * 236 | * @var array|null 237 | */ 238 | protected $server_caps; 239 | 240 | /** 241 | * The most recent reply received from the server. 242 | * 243 | * @var string 244 | */ 245 | protected $last_reply = ''; 246 | 247 | /** 248 | * Output debugging info via a user-selected method. 249 | * 250 | * @param string $str Debug string to output 251 | * @param int $level The debug level of this message; see DEBUG_* constants 252 | * 253 | * @see SMTP::$Debugoutput 254 | * @see SMTP::$do_debug 255 | */ 256 | protected function edebug($str, $level = 0) 257 | { 258 | if ($level > $this->do_debug) { 259 | return; 260 | } 261 | //Is this a PSR-3 logger? 262 | if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { 263 | $this->Debugoutput->debug($str); 264 | 265 | return; 266 | } 267 | //Avoid clash with built-in function names 268 | if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { 269 | call_user_func($this->Debugoutput, $str, $level); 270 | 271 | return; 272 | } 273 | switch ($this->Debugoutput) { 274 | case 'error_log': 275 | //Don't output, just log 276 | error_log($str); 277 | break; 278 | case 'html': 279 | //Cleans up output a bit for a better looking, HTML-safe output 280 | echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( 281 | preg_replace('/[\r\n]+/', '', $str), 282 | ENT_QUOTES, 283 | 'UTF-8' 284 | ), "
\n"; 285 | break; 286 | case 'echo': 287 | default: 288 | //Normalize line breaks 289 | $str = preg_replace('/\r\n|\r/m', "\n", $str); 290 | echo gmdate('Y-m-d H:i:s'), 291 | "\t", 292 | //Trim trailing space 293 | trim( 294 | //Indent for readability, except for trailing break 295 | str_replace( 296 | "\n", 297 | "\n \t ", 298 | trim($str) 299 | ) 300 | ), 301 | "\n"; 302 | } 303 | } 304 | 305 | /** 306 | * Connect to an SMTP server. 307 | * 308 | * @param string $host SMTP server IP or host name 309 | * @param int $port The port number to connect to 310 | * @param int $timeout How long to wait for the connection to open 311 | * @param array $options An array of options for stream_context_create() 312 | * 313 | * @return bool 314 | */ 315 | public function connect($host, $port = null, $timeout = 30, $options = []) 316 | { 317 | //Clear errors to avoid confusion 318 | $this->setError(''); 319 | //Make sure we are __not__ connected 320 | if ($this->connected()) { 321 | //Already connected, generate error 322 | $this->setError('Already connected to a server'); 323 | 324 | return false; 325 | } 326 | if (empty($port)) { 327 | $port = self::DEFAULT_PORT; 328 | } 329 | //Connect to the SMTP server 330 | $this->edebug( 331 | "Connection: opening to $host:$port, timeout=$timeout, options=" . 332 | (count($options) > 0 ? var_export($options, true) : 'array()'), 333 | self::DEBUG_CONNECTION 334 | ); 335 | 336 | $this->smtp_conn = $this->getSMTPConnection($host, $port, $timeout, $options); 337 | 338 | if ($this->smtp_conn === false) { 339 | //Error info already set inside `getSMTPConnection()` 340 | return false; 341 | } 342 | 343 | $this->edebug('Connection: opened', self::DEBUG_CONNECTION); 344 | 345 | //Get any announcement 346 | $this->last_reply = $this->get_lines(); 347 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); 348 | $responseCode = (int)substr($this->last_reply, 0, 3); 349 | if ($responseCode === 220) { 350 | return true; 351 | } 352 | //Anything other than a 220 response means something went wrong 353 | //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error 354 | //https://tools.ietf.org/html/rfc5321#section-3.1 355 | if ($responseCode === 554) { 356 | $this->quit(); 357 | } 358 | //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down) 359 | $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION); 360 | $this->close(); 361 | return false; 362 | } 363 | 364 | /** 365 | * Create connection to the SMTP server. 366 | * 367 | * @param string $host SMTP server IP or host name 368 | * @param int $port The port number to connect to 369 | * @param int $timeout How long to wait for the connection to open 370 | * @param array $options An array of options for stream_context_create() 371 | * 372 | * @return false|resource 373 | */ 374 | protected function getSMTPConnection($host, $port = null, $timeout = 30, $options = []) 375 | { 376 | static $streamok; 377 | //This is enabled by default since 5.0.0 but some providers disable it 378 | //Check this once and cache the result 379 | if (null === $streamok) { 380 | $streamok = function_exists('stream_socket_client'); 381 | } 382 | 383 | $errno = 0; 384 | $errstr = ''; 385 | if ($streamok) { 386 | $socket_context = stream_context_create($options); 387 | set_error_handler([$this, 'errorHandler']); 388 | $connection = stream_socket_client( 389 | $host . ':' . $port, 390 | $errno, 391 | $errstr, 392 | $timeout, 393 | STREAM_CLIENT_CONNECT, 394 | $socket_context 395 | ); 396 | } else { 397 | //Fall back to fsockopen which should work in more places, but is missing some features 398 | $this->edebug( 399 | 'Connection: stream_socket_client not available, falling back to fsockopen', 400 | self::DEBUG_CONNECTION 401 | ); 402 | set_error_handler([$this, 'errorHandler']); 403 | $connection = fsockopen( 404 | $host, 405 | $port, 406 | $errno, 407 | $errstr, 408 | $timeout 409 | ); 410 | } 411 | restore_error_handler(); 412 | 413 | //Verify we connected properly 414 | if (!is_resource($connection)) { 415 | $this->setError( 416 | 'Failed to connect to server', 417 | '', 418 | (string) $errno, 419 | $errstr 420 | ); 421 | $this->edebug( 422 | 'SMTP ERROR: ' . $this->error['error'] 423 | . ": $errstr ($errno)", 424 | self::DEBUG_CLIENT 425 | ); 426 | 427 | return false; 428 | } 429 | 430 | //SMTP server can take longer to respond, give longer timeout for first read 431 | //Windows does not have support for this timeout function 432 | if (strpos(PHP_OS, 'WIN') !== 0) { 433 | $max = (int)ini_get('max_execution_time'); 434 | //Don't bother if unlimited, or if set_time_limit is disabled 435 | if (0 !== $max && $timeout > $max && strpos(ini_get('disable_functions'), 'set_time_limit') === false) { 436 | @set_time_limit($timeout); 437 | } 438 | stream_set_timeout($connection, $timeout, 0); 439 | } 440 | 441 | return $connection; 442 | } 443 | 444 | /** 445 | * Initiate a TLS (encrypted) session. 446 | * 447 | * @return bool 448 | */ 449 | public function startTLS() 450 | { 451 | if (!$this->sendCommand('STARTTLS', 'STARTTLS', 220)) { 452 | return false; 453 | } 454 | 455 | //Allow the best TLS version(s) we can 456 | $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT; 457 | 458 | //PHP 5.6.7 dropped inclusion of TLS 1.1 and 1.2 in STREAM_CRYPTO_METHOD_TLS_CLIENT 459 | //so add them back in manually if we can 460 | if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) { 461 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; 462 | $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; 463 | } 464 | 465 | //Begin encrypted connection 466 | set_error_handler([$this, 'errorHandler']); 467 | $crypto_ok = stream_socket_enable_crypto( 468 | $this->smtp_conn, 469 | true, 470 | $crypto_method 471 | ); 472 | restore_error_handler(); 473 | 474 | return (bool) $crypto_ok; 475 | } 476 | 477 | /** 478 | * Perform SMTP authentication. 479 | * Must be run after hello(). 480 | * 481 | * @see hello() 482 | * 483 | * @param string $username The user name 484 | * @param string $password The password 485 | * @param string $authtype The auth type (CRAM-MD5, PLAIN, LOGIN, XOAUTH2) 486 | * @param OAuth $OAuth An optional OAuth instance for XOAUTH2 authentication 487 | * 488 | * @return bool True if successfully authenticated 489 | */ 490 | public function authenticate( 491 | $username, 492 | $password, 493 | $authtype = null, 494 | $OAuth = null 495 | ) { 496 | if (!$this->server_caps) { 497 | $this->setError('Authentication is not allowed before HELO/EHLO'); 498 | 499 | return false; 500 | } 501 | 502 | if (array_key_exists('EHLO', $this->server_caps)) { 503 | //SMTP extensions are available; try to find a proper authentication method 504 | if (!array_key_exists('AUTH', $this->server_caps)) { 505 | $this->setError('Authentication is not allowed at this stage'); 506 | //'at this stage' means that auth may be allowed after the stage changes 507 | //e.g. after STARTTLS 508 | 509 | return false; 510 | } 511 | 512 | $this->edebug('Auth method requested: ' . ($authtype ?: 'UNSPECIFIED'), self::DEBUG_LOWLEVEL); 513 | $this->edebug( 514 | 'Auth methods available on the server: ' . implode(',', $this->server_caps['AUTH']), 515 | self::DEBUG_LOWLEVEL 516 | ); 517 | 518 | //If we have requested a specific auth type, check the server supports it before trying others 519 | if (null !== $authtype && !in_array($authtype, $this->server_caps['AUTH'], true)) { 520 | $this->edebug('Requested auth method not available: ' . $authtype, self::DEBUG_LOWLEVEL); 521 | $authtype = null; 522 | } 523 | 524 | if (empty($authtype)) { 525 | //If no auth mechanism is specified, attempt to use these, in this order 526 | //Try CRAM-MD5 first as it's more secure than the others 527 | foreach (['CRAM-MD5', 'LOGIN', 'PLAIN', 'XOAUTH2'] as $method) { 528 | if (in_array($method, $this->server_caps['AUTH'], true)) { 529 | $authtype = $method; 530 | break; 531 | } 532 | } 533 | if (empty($authtype)) { 534 | $this->setError('No supported authentication methods found'); 535 | 536 | return false; 537 | } 538 | $this->edebug('Auth method selected: ' . $authtype, self::DEBUG_LOWLEVEL); 539 | } 540 | 541 | if (!in_array($authtype, $this->server_caps['AUTH'], true)) { 542 | $this->setError("The requested authentication method \"$authtype\" is not supported by the server"); 543 | 544 | return false; 545 | } 546 | } elseif (empty($authtype)) { 547 | $authtype = 'LOGIN'; 548 | } 549 | switch ($authtype) { 550 | case 'PLAIN': 551 | //Start authentication 552 | if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { 553 | return false; 554 | } 555 | //Send encoded username and password 556 | if ( 557 | //Format from https://tools.ietf.org/html/rfc4616#section-2 558 | //We skip the first field (it's forgery), so the string starts with a null byte 559 | !$this->sendCommand( 560 | 'User & Password', 561 | base64_encode("\0" . $username . "\0" . $password), 562 | 235 563 | ) 564 | ) { 565 | return false; 566 | } 567 | break; 568 | case 'LOGIN': 569 | //Start authentication 570 | if (!$this->sendCommand('AUTH', 'AUTH LOGIN', 334)) { 571 | return false; 572 | } 573 | if (!$this->sendCommand('Username', base64_encode($username), 334)) { 574 | return false; 575 | } 576 | if (!$this->sendCommand('Password', base64_encode($password), 235)) { 577 | return false; 578 | } 579 | break; 580 | case 'CRAM-MD5': 581 | //Start authentication 582 | if (!$this->sendCommand('AUTH CRAM-MD5', 'AUTH CRAM-MD5', 334)) { 583 | return false; 584 | } 585 | //Get the challenge 586 | $challenge = base64_decode(substr($this->last_reply, 4)); 587 | 588 | //Build the response 589 | $response = $username . ' ' . $this->hmac($challenge, $password); 590 | 591 | //send encoded credentials 592 | return $this->sendCommand('Username', base64_encode($response), 235); 593 | case 'XOAUTH2': 594 | //The OAuth instance must be set up prior to requesting auth. 595 | if (null === $OAuth) { 596 | return false; 597 | } 598 | $oauth = $OAuth->getOauth64(); 599 | 600 | //Start authentication 601 | if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { 602 | return false; 603 | } 604 | break; 605 | default: 606 | $this->setError("Authentication method \"$authtype\" is not supported"); 607 | 608 | return false; 609 | } 610 | 611 | return true; 612 | } 613 | 614 | /** 615 | * Calculate an MD5 HMAC hash. 616 | * Works like hash_hmac('md5', $data, $key) 617 | * in case that function is not available. 618 | * 619 | * @param string $data The data to hash 620 | * @param string $key The key to hash with 621 | * 622 | * @return string 623 | */ 624 | protected function hmac($data, $key) 625 | { 626 | if (function_exists('hash_hmac')) { 627 | return hash_hmac('md5', $data, $key); 628 | } 629 | 630 | //The following borrowed from 631 | //http://php.net/manual/en/function.mhash.php#27225 632 | 633 | //RFC 2104 HMAC implementation for php. 634 | //Creates an md5 HMAC. 635 | //Eliminates the need to install mhash to compute a HMAC 636 | //by Lance Rushing 637 | 638 | $bytelen = 64; //byte length for md5 639 | if (strlen($key) > $bytelen) { 640 | $key = pack('H*', md5($key)); 641 | } 642 | $key = str_pad($key, $bytelen, chr(0x00)); 643 | $ipad = str_pad('', $bytelen, chr(0x36)); 644 | $opad = str_pad('', $bytelen, chr(0x5c)); 645 | $k_ipad = $key ^ $ipad; 646 | $k_opad = $key ^ $opad; 647 | 648 | return md5($k_opad . pack('H*', md5($k_ipad . $data))); 649 | } 650 | 651 | /** 652 | * Check connection state. 653 | * 654 | * @return bool True if connected 655 | */ 656 | public function connected() 657 | { 658 | if (is_resource($this->smtp_conn)) { 659 | $sock_status = stream_get_meta_data($this->smtp_conn); 660 | if ($sock_status['eof']) { 661 | //The socket is valid but we are not connected 662 | $this->edebug( 663 | 'SMTP NOTICE: EOF caught while checking if connected', 664 | self::DEBUG_CLIENT 665 | ); 666 | $this->close(); 667 | 668 | return false; 669 | } 670 | 671 | return true; //everything looks good 672 | } 673 | 674 | return false; 675 | } 676 | 677 | /** 678 | * Close the socket and clean up the state of the class. 679 | * Don't use this function without first trying to use QUIT. 680 | * 681 | * @see quit() 682 | */ 683 | public function close() 684 | { 685 | $this->setError(''); 686 | $this->server_caps = null; 687 | $this->helo_rply = null; 688 | if (is_resource($this->smtp_conn)) { 689 | //Close the connection and cleanup 690 | fclose($this->smtp_conn); 691 | $this->smtp_conn = null; //Makes for cleaner serialization 692 | $this->edebug('Connection: closed', self::DEBUG_CONNECTION); 693 | } 694 | } 695 | 696 | /** 697 | * Send an SMTP DATA command. 698 | * Issues a data command and sends the msg_data to the server, 699 | * finalizing the mail transaction. $msg_data is the message 700 | * that is to be send with the headers. Each header needs to be 701 | * on a single line followed by a with the message headers 702 | * and the message body being separated by an additional . 703 | * Implements RFC 821: DATA . 704 | * 705 | * @param string $msg_data Message data to send 706 | * 707 | * @return bool 708 | */ 709 | public function data($msg_data) 710 | { 711 | //This will use the standard timelimit 712 | if (!$this->sendCommand('DATA', 'DATA', 354)) { 713 | return false; 714 | } 715 | 716 | /* The server is ready to accept data! 717 | * According to rfc821 we should not send more than 1000 characters on a single line (including the LE) 718 | * so we will break the data up into lines by \r and/or \n then if needed we will break each of those into 719 | * smaller lines to fit within the limit. 720 | * We will also look for lines that start with a '.' and prepend an additional '.'. 721 | * NOTE: this does not count towards line-length limit. 722 | */ 723 | 724 | //Normalize line breaks before exploding 725 | $lines = explode("\n", str_replace(["\r\n", "\r"], "\n", $msg_data)); 726 | 727 | /* To distinguish between a complete RFC822 message and a plain message body, we check if the first field 728 | * of the first line (':' separated) does not contain a space then it _should_ be a header and we will 729 | * process all lines before a blank line as headers. 730 | */ 731 | 732 | $field = substr($lines[0], 0, strpos($lines[0], ':')); 733 | $in_headers = false; 734 | if (!empty($field) && strpos($field, ' ') === false) { 735 | $in_headers = true; 736 | } 737 | 738 | foreach ($lines as $line) { 739 | $lines_out = []; 740 | if ($in_headers && $line === '') { 741 | $in_headers = false; 742 | } 743 | //Break this line up into several smaller lines if it's too long 744 | //Micro-optimisation: isset($str[$len]) is faster than (strlen($str) > $len), 745 | while (isset($line[self::MAX_LINE_LENGTH])) { 746 | //Working backwards, try to find a space within the last MAX_LINE_LENGTH chars of the line to break on 747 | //so as to avoid breaking in the middle of a word 748 | $pos = strrpos(substr($line, 0, self::MAX_LINE_LENGTH), ' '); 749 | //Deliberately matches both false and 0 750 | if (!$pos) { 751 | //No nice break found, add a hard break 752 | $pos = self::MAX_LINE_LENGTH - 1; 753 | $lines_out[] = substr($line, 0, $pos); 754 | $line = substr($line, $pos); 755 | } else { 756 | //Break at the found point 757 | $lines_out[] = substr($line, 0, $pos); 758 | //Move along by the amount we dealt with 759 | $line = substr($line, $pos + 1); 760 | } 761 | //If processing headers add a LWSP-char to the front of new line RFC822 section 3.1.1 762 | if ($in_headers) { 763 | $line = "\t" . $line; 764 | } 765 | } 766 | $lines_out[] = $line; 767 | 768 | //Send the lines to the server 769 | foreach ($lines_out as $line_out) { 770 | //Dot-stuffing as per RFC5321 section 4.5.2 771 | //https://tools.ietf.org/html/rfc5321#section-4.5.2 772 | if (!empty($line_out) && $line_out[0] === '.') { 773 | $line_out = '.' . $line_out; 774 | } 775 | $this->client_send($line_out . static::LE, 'DATA'); 776 | } 777 | } 778 | 779 | //Message data has been sent, complete the command 780 | //Increase timelimit for end of DATA command 781 | $savetimelimit = $this->Timelimit; 782 | $this->Timelimit *= 2; 783 | $result = $this->sendCommand('DATA END', '.', 250); 784 | $this->recordLastTransactionID(); 785 | //Restore timelimit 786 | $this->Timelimit = $savetimelimit; 787 | 788 | return $result; 789 | } 790 | 791 | /** 792 | * Send an SMTP HELO or EHLO command. 793 | * Used to identify the sending server to the receiving server. 794 | * This makes sure that client and server are in a known state. 795 | * Implements RFC 821: HELO 796 | * and RFC 2821 EHLO. 797 | * 798 | * @param string $host The host name or IP to connect to 799 | * 800 | * @return bool 801 | */ 802 | public function hello($host = '') 803 | { 804 | //Try extended hello first (RFC 2821) 805 | if ($this->sendHello('EHLO', $host)) { 806 | return true; 807 | } 808 | 809 | //Some servers shut down the SMTP service here (RFC 5321) 810 | if (substr($this->helo_rply, 0, 3) == '421') { 811 | return false; 812 | } 813 | 814 | return $this->sendHello('HELO', $host); 815 | } 816 | 817 | /** 818 | * Send an SMTP HELO or EHLO command. 819 | * Low-level implementation used by hello(). 820 | * 821 | * @param string $hello The HELO string 822 | * @param string $host The hostname to say we are 823 | * 824 | * @return bool 825 | * 826 | * @see hello() 827 | */ 828 | protected function sendHello($hello, $host) 829 | { 830 | $noerror = $this->sendCommand($hello, $hello . ' ' . $host, 250); 831 | $this->helo_rply = $this->last_reply; 832 | if ($noerror) { 833 | $this->parseHelloFields($hello); 834 | } else { 835 | $this->server_caps = null; 836 | } 837 | 838 | return $noerror; 839 | } 840 | 841 | /** 842 | * Parse a reply to HELO/EHLO command to discover server extensions. 843 | * In case of HELO, the only parameter that can be discovered is a server name. 844 | * 845 | * @param string $type `HELO` or `EHLO` 846 | */ 847 | protected function parseHelloFields($type) 848 | { 849 | $this->server_caps = []; 850 | $lines = explode("\n", $this->helo_rply); 851 | 852 | foreach ($lines as $n => $s) { 853 | //First 4 chars contain response code followed by - or space 854 | $s = trim(substr($s, 4)); 855 | if (empty($s)) { 856 | continue; 857 | } 858 | $fields = explode(' ', $s); 859 | if (!empty($fields)) { 860 | if (!$n) { 861 | $name = $type; 862 | $fields = $fields[0]; 863 | } else { 864 | $name = array_shift($fields); 865 | switch ($name) { 866 | case 'SIZE': 867 | $fields = ($fields ? $fields[0] : 0); 868 | break; 869 | case 'AUTH': 870 | if (!is_array($fields)) { 871 | $fields = []; 872 | } 873 | break; 874 | default: 875 | $fields = true; 876 | } 877 | } 878 | $this->server_caps[$name] = $fields; 879 | } 880 | } 881 | } 882 | 883 | /** 884 | * Send an SMTP MAIL command. 885 | * Starts a mail transaction from the email address specified in 886 | * $from. Returns true if successful or false otherwise. If True 887 | * the mail transaction is started and then one or more recipient 888 | * commands may be called followed by a data command. 889 | * Implements RFC 821: MAIL FROM: . 890 | * 891 | * @param string $from Source address of this message 892 | * 893 | * @return bool 894 | */ 895 | public function mail($from) 896 | { 897 | $useVerp = ($this->do_verp ? ' XVERP' : ''); 898 | 899 | return $this->sendCommand( 900 | 'MAIL FROM', 901 | 'MAIL FROM:<' . $from . '>' . $useVerp, 902 | 250 903 | ); 904 | } 905 | 906 | /** 907 | * Send an SMTP QUIT command. 908 | * Closes the socket if there is no error or the $close_on_error argument is true. 909 | * Implements from RFC 821: QUIT . 910 | * 911 | * @param bool $close_on_error Should the connection close if an error occurs? 912 | * 913 | * @return bool 914 | */ 915 | public function quit($close_on_error = true) 916 | { 917 | $noerror = $this->sendCommand('QUIT', 'QUIT', 221); 918 | $err = $this->error; //Save any error 919 | if ($noerror || $close_on_error) { 920 | $this->close(); 921 | $this->error = $err; //Restore any error from the quit command 922 | } 923 | 924 | return $noerror; 925 | } 926 | 927 | /** 928 | * Send an SMTP RCPT command. 929 | * Sets the TO argument to $toaddr. 930 | * Returns true if the recipient was accepted false if it was rejected. 931 | * Implements from RFC 821: RCPT TO: . 932 | * 933 | * @param string $address The address the message is being sent to 934 | * @param string $dsn Comma separated list of DSN notifications. NEVER, SUCCESS, FAILURE 935 | * or DELAY. If you specify NEVER all other notifications are ignored. 936 | * 937 | * @return bool 938 | */ 939 | public function recipient($address, $dsn = '') 940 | { 941 | if (empty($dsn)) { 942 | $rcpt = 'RCPT TO:<' . $address . '>'; 943 | } else { 944 | $dsn = strtoupper($dsn); 945 | $notify = []; 946 | 947 | if (strpos($dsn, 'NEVER') !== false) { 948 | $notify[] = 'NEVER'; 949 | } else { 950 | foreach (['SUCCESS', 'FAILURE', 'DELAY'] as $value) { 951 | if (strpos($dsn, $value) !== false) { 952 | $notify[] = $value; 953 | } 954 | } 955 | } 956 | 957 | $rcpt = 'RCPT TO:<' . $address . '> NOTIFY=' . implode(',', $notify); 958 | } 959 | 960 | return $this->sendCommand( 961 | 'RCPT TO', 962 | $rcpt, 963 | [250, 251] 964 | ); 965 | } 966 | 967 | /** 968 | * Send an SMTP RSET command. 969 | * Abort any transaction that is currently in progress. 970 | * Implements RFC 821: RSET . 971 | * 972 | * @return bool True on success 973 | */ 974 | public function reset() 975 | { 976 | return $this->sendCommand('RSET', 'RSET', 250); 977 | } 978 | 979 | /** 980 | * Send a command to an SMTP server and check its return code. 981 | * 982 | * @param string $command The command name - not sent to the server 983 | * @param string $commandstring The actual command to send 984 | * @param int|array $expect One or more expected integer success codes 985 | * 986 | * @return bool True on success 987 | */ 988 | protected function sendCommand($command, $commandstring, $expect) 989 | { 990 | if (!$this->connected()) { 991 | $this->setError("Called $command without being connected"); 992 | 993 | return false; 994 | } 995 | //Reject line breaks in all commands 996 | if ((strpos($commandstring, "\n") !== false) || (strpos($commandstring, "\r") !== false)) { 997 | $this->setError("Command '$command' contained line breaks"); 998 | 999 | return false; 1000 | } 1001 | $this->client_send($commandstring . static::LE, $command); 1002 | 1003 | $this->last_reply = $this->get_lines(); 1004 | //Fetch SMTP code and possible error code explanation 1005 | $matches = []; 1006 | if (preg_match('/^([\d]{3})[ -](?:([\d]\\.[\d]\\.[\d]{1,2}) )?/', $this->last_reply, $matches)) { 1007 | $code = (int) $matches[1]; 1008 | $code_ex = (count($matches) > 2 ? $matches[2] : null); 1009 | //Cut off error code from each response line 1010 | $detail = preg_replace( 1011 | "/{$code}[ -]" . 1012 | ($code_ex ? str_replace('.', '\\.', $code_ex) . ' ' : '') . '/m', 1013 | '', 1014 | $this->last_reply 1015 | ); 1016 | } else { 1017 | //Fall back to simple parsing if regex fails 1018 | $code = (int) substr($this->last_reply, 0, 3); 1019 | $code_ex = null; 1020 | $detail = substr($this->last_reply, 4); 1021 | } 1022 | 1023 | $this->edebug('SERVER -> CLIENT: ' . $this->last_reply, self::DEBUG_SERVER); 1024 | 1025 | if (!in_array($code, (array) $expect, true)) { 1026 | $this->setError( 1027 | "$command command failed", 1028 | $detail, 1029 | $code, 1030 | $code_ex 1031 | ); 1032 | $this->edebug( 1033 | 'SMTP ERROR: ' . $this->error['error'] . ': ' . $this->last_reply, 1034 | self::DEBUG_CLIENT 1035 | ); 1036 | 1037 | return false; 1038 | } 1039 | 1040 | $this->setError(''); 1041 | 1042 | return true; 1043 | } 1044 | 1045 | /** 1046 | * Send an SMTP SAML command. 1047 | * Starts a mail transaction from the email address specified in $from. 1048 | * Returns true if successful or false otherwise. If True 1049 | * the mail transaction is started and then one or more recipient 1050 | * commands may be called followed by a data command. This command 1051 | * will send the message to the users terminal if they are logged 1052 | * in and send them an email. 1053 | * Implements RFC 821: SAML FROM: . 1054 | * 1055 | * @param string $from The address the message is from 1056 | * 1057 | * @return bool 1058 | */ 1059 | public function sendAndMail($from) 1060 | { 1061 | return $this->sendCommand('SAML', "SAML FROM:$from", 250); 1062 | } 1063 | 1064 | /** 1065 | * Send an SMTP VRFY command. 1066 | * 1067 | * @param string $name The name to verify 1068 | * 1069 | * @return bool 1070 | */ 1071 | public function verify($name) 1072 | { 1073 | return $this->sendCommand('VRFY', "VRFY $name", [250, 251]); 1074 | } 1075 | 1076 | /** 1077 | * Send an SMTP NOOP command. 1078 | * Used to keep keep-alives alive, doesn't actually do anything. 1079 | * 1080 | * @return bool 1081 | */ 1082 | public function noop() 1083 | { 1084 | return $this->sendCommand('NOOP', 'NOOP', 250); 1085 | } 1086 | 1087 | /** 1088 | * Send an SMTP TURN command. 1089 | * This is an optional command for SMTP that this class does not support. 1090 | * This method is here to make the RFC821 Definition complete for this class 1091 | * and _may_ be implemented in future. 1092 | * Implements from RFC 821: TURN . 1093 | * 1094 | * @return bool 1095 | */ 1096 | public function turn() 1097 | { 1098 | $this->setError('The SMTP TURN command is not implemented'); 1099 | $this->edebug('SMTP NOTICE: ' . $this->error['error'], self::DEBUG_CLIENT); 1100 | 1101 | return false; 1102 | } 1103 | 1104 | /** 1105 | * Send raw data to the server. 1106 | * 1107 | * @param string $data The data to send 1108 | * @param string $command Optionally, the command this is part of, used only for controlling debug output 1109 | * 1110 | * @return int|bool The number of bytes sent to the server or false on error 1111 | */ 1112 | public function client_send($data, $command = '') 1113 | { 1114 | //If SMTP transcripts are left enabled, or debug output is posted online 1115 | //it can leak credentials, so hide credentials in all but lowest level 1116 | if ( 1117 | self::DEBUG_LOWLEVEL > $this->do_debug && 1118 | in_array($command, ['User & Password', 'Username', 'Password'], true) 1119 | ) { 1120 | $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT); 1121 | } else { 1122 | $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT); 1123 | } 1124 | set_error_handler([$this, 'errorHandler']); 1125 | $result = fwrite($this->smtp_conn, $data); 1126 | restore_error_handler(); 1127 | 1128 | return $result; 1129 | } 1130 | 1131 | /** 1132 | * Get the latest error. 1133 | * 1134 | * @return array 1135 | */ 1136 | public function getError() 1137 | { 1138 | return $this->error; 1139 | } 1140 | 1141 | /** 1142 | * Get SMTP extensions available on the server. 1143 | * 1144 | * @return array|null 1145 | */ 1146 | public function getServerExtList() 1147 | { 1148 | return $this->server_caps; 1149 | } 1150 | 1151 | /** 1152 | * Get metadata about the SMTP server from its HELO/EHLO response. 1153 | * The method works in three ways, dependent on argument value and current state: 1154 | * 1. HELO/EHLO has not been sent - returns null and populates $this->error. 1155 | * 2. HELO has been sent - 1156 | * $name == 'HELO': returns server name 1157 | * $name == 'EHLO': returns boolean false 1158 | * $name == any other string: returns null and populates $this->error 1159 | * 3. EHLO has been sent - 1160 | * $name == 'HELO'|'EHLO': returns the server name 1161 | * $name == any other string: if extension $name exists, returns True 1162 | * or its options (e.g. AUTH mechanisms supported). Otherwise returns False. 1163 | * 1164 | * @param string $name Name of SMTP extension or 'HELO'|'EHLO' 1165 | * 1166 | * @return string|bool|null 1167 | */ 1168 | public function getServerExt($name) 1169 | { 1170 | if (!$this->server_caps) { 1171 | $this->setError('No HELO/EHLO was sent'); 1172 | 1173 | return null; 1174 | } 1175 | 1176 | if (!array_key_exists($name, $this->server_caps)) { 1177 | if ('HELO' === $name) { 1178 | return $this->server_caps['EHLO']; 1179 | } 1180 | if ('EHLO' === $name || array_key_exists('EHLO', $this->server_caps)) { 1181 | return false; 1182 | } 1183 | $this->setError('HELO handshake was used; No information about server extensions available'); 1184 | 1185 | return null; 1186 | } 1187 | 1188 | return $this->server_caps[$name]; 1189 | } 1190 | 1191 | /** 1192 | * Get the last reply from the server. 1193 | * 1194 | * @return string 1195 | */ 1196 | public function getLastReply() 1197 | { 1198 | return $this->last_reply; 1199 | } 1200 | 1201 | /** 1202 | * Read the SMTP server's response. 1203 | * Either before eof or socket timeout occurs on the operation. 1204 | * With SMTP we can tell if we have more lines to read if the 1205 | * 4th character is '-' symbol. If it is a space then we don't 1206 | * need to read anything else. 1207 | * 1208 | * @return string 1209 | */ 1210 | protected function get_lines() 1211 | { 1212 | //If the connection is bad, give up straight away 1213 | if (!is_resource($this->smtp_conn)) { 1214 | return ''; 1215 | } 1216 | $data = ''; 1217 | $endtime = 0; 1218 | stream_set_timeout($this->smtp_conn, $this->Timeout); 1219 | if ($this->Timelimit > 0) { 1220 | $endtime = time() + $this->Timelimit; 1221 | } 1222 | $selR = [$this->smtp_conn]; 1223 | $selW = null; 1224 | while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { 1225 | //Must pass vars in here as params are by reference 1226 | //solution for signals inspired by https://github.com/symfony/symfony/pull/6540 1227 | set_error_handler([$this, 'errorHandler']); 1228 | $n = stream_select($selR, $selW, $selW, $this->Timelimit); 1229 | restore_error_handler(); 1230 | 1231 | if ($n === false) { 1232 | $message = $this->getError()['detail']; 1233 | 1234 | $this->edebug( 1235 | 'SMTP -> get_lines(): select failed (' . $message . ')', 1236 | self::DEBUG_LOWLEVEL 1237 | ); 1238 | 1239 | //stream_select returns false when the `select` system call is interrupted 1240 | //by an incoming signal, try the select again 1241 | if (stripos($message, 'interrupted system call') !== false) { 1242 | $this->edebug( 1243 | 'SMTP -> get_lines(): retrying stream_select', 1244 | self::DEBUG_LOWLEVEL 1245 | ); 1246 | $this->setError(''); 1247 | continue; 1248 | } 1249 | 1250 | break; 1251 | } 1252 | 1253 | if (!$n) { 1254 | $this->edebug( 1255 | 'SMTP -> get_lines(): select timed-out in (' . $this->Timelimit . ' sec)', 1256 | self::DEBUG_LOWLEVEL 1257 | ); 1258 | break; 1259 | } 1260 | 1261 | //Deliberate noise suppression - errors are handled afterwards 1262 | $str = @fgets($this->smtp_conn, self::MAX_REPLY_LENGTH); 1263 | $this->edebug('SMTP INBOUND: "' . trim($str) . '"', self::DEBUG_LOWLEVEL); 1264 | $data .= $str; 1265 | //If response is only 3 chars (not valid, but RFC5321 S4.2 says it must be handled), 1266 | //or 4th character is a space or a line break char, we are done reading, break the loop. 1267 | //String array access is a significant micro-optimisation over strlen 1268 | if (!isset($str[3]) || $str[3] === ' ' || $str[3] === "\r" || $str[3] === "\n") { 1269 | break; 1270 | } 1271 | //Timed-out? Log and break 1272 | $info = stream_get_meta_data($this->smtp_conn); 1273 | if ($info['timed_out']) { 1274 | $this->edebug( 1275 | 'SMTP -> get_lines(): stream timed-out (' . $this->Timeout . ' sec)', 1276 | self::DEBUG_LOWLEVEL 1277 | ); 1278 | break; 1279 | } 1280 | //Now check if reads took too long 1281 | if ($endtime && time() > $endtime) { 1282 | $this->edebug( 1283 | 'SMTP -> get_lines(): timelimit reached (' . 1284 | $this->Timelimit . ' sec)', 1285 | self::DEBUG_LOWLEVEL 1286 | ); 1287 | break; 1288 | } 1289 | } 1290 | 1291 | return $data; 1292 | } 1293 | 1294 | /** 1295 | * Enable or disable VERP address generation. 1296 | * 1297 | * @param bool $enabled 1298 | */ 1299 | public function setVerp($enabled = false) 1300 | { 1301 | $this->do_verp = $enabled; 1302 | } 1303 | 1304 | /** 1305 | * Get VERP address generation mode. 1306 | * 1307 | * @return bool 1308 | */ 1309 | public function getVerp() 1310 | { 1311 | return $this->do_verp; 1312 | } 1313 | 1314 | /** 1315 | * Set error messages and codes. 1316 | * 1317 | * @param string $message The error message 1318 | * @param string $detail Further detail on the error 1319 | * @param string $smtp_code An associated SMTP error code 1320 | * @param string $smtp_code_ex Extended SMTP code 1321 | */ 1322 | protected function setError($message, $detail = '', $smtp_code = '', $smtp_code_ex = '') 1323 | { 1324 | $this->error = [ 1325 | 'error' => $message, 1326 | 'detail' => $detail, 1327 | 'smtp_code' => $smtp_code, 1328 | 'smtp_code_ex' => $smtp_code_ex, 1329 | ]; 1330 | } 1331 | 1332 | /** 1333 | * Set debug output method. 1334 | * 1335 | * @param string|callable $method The name of the mechanism to use for debugging output, or a callable to handle it 1336 | */ 1337 | public function setDebugOutput($method = 'echo') 1338 | { 1339 | $this->Debugoutput = $method; 1340 | } 1341 | 1342 | /** 1343 | * Get debug output method. 1344 | * 1345 | * @return string 1346 | */ 1347 | public function getDebugOutput() 1348 | { 1349 | return $this->Debugoutput; 1350 | } 1351 | 1352 | /** 1353 | * Set debug output level. 1354 | * 1355 | * @param int $level 1356 | */ 1357 | public function setDebugLevel($level = 0) 1358 | { 1359 | $this->do_debug = $level; 1360 | } 1361 | 1362 | /** 1363 | * Get debug output level. 1364 | * 1365 | * @return int 1366 | */ 1367 | public function getDebugLevel() 1368 | { 1369 | return $this->do_debug; 1370 | } 1371 | 1372 | /** 1373 | * Set SMTP timeout. 1374 | * 1375 | * @param int $timeout The timeout duration in seconds 1376 | */ 1377 | public function setTimeout($timeout = 0) 1378 | { 1379 | $this->Timeout = $timeout; 1380 | } 1381 | 1382 | /** 1383 | * Get SMTP timeout. 1384 | * 1385 | * @return int 1386 | */ 1387 | public function getTimeout() 1388 | { 1389 | return $this->Timeout; 1390 | } 1391 | 1392 | /** 1393 | * Reports an error number and string. 1394 | * 1395 | * @param int $errno The error number returned by PHP 1396 | * @param string $errmsg The error message returned by PHP 1397 | * @param string $errfile The file the error occurred in 1398 | * @param int $errline The line number the error occurred on 1399 | */ 1400 | protected function errorHandler($errno, $errmsg, $errfile = '', $errline = 0) 1401 | { 1402 | $notice = 'Connection failed.'; 1403 | $this->setError( 1404 | $notice, 1405 | $errmsg, 1406 | (string) $errno 1407 | ); 1408 | $this->edebug( 1409 | "$notice Error #$errno: $errmsg [$errfile line $errline]", 1410 | self::DEBUG_CONNECTION 1411 | ); 1412 | } 1413 | 1414 | /** 1415 | * Extract and return the ID of the last SMTP transaction based on 1416 | * a list of patterns provided in SMTP::$smtp_transaction_id_patterns. 1417 | * Relies on the host providing the ID in response to a DATA command. 1418 | * If no reply has been received yet, it will return null. 1419 | * If no pattern was matched, it will return false. 1420 | * 1421 | * @return bool|string|null 1422 | */ 1423 | protected function recordLastTransactionID() 1424 | { 1425 | $reply = $this->getLastReply(); 1426 | 1427 | if (empty($reply)) { 1428 | $this->last_smtp_transaction_id = null; 1429 | } else { 1430 | $this->last_smtp_transaction_id = false; 1431 | foreach ($this->smtp_transaction_id_patterns as $smtp_transaction_id_pattern) { 1432 | $matches = []; 1433 | if (preg_match($smtp_transaction_id_pattern, $reply, $matches)) { 1434 | $this->last_smtp_transaction_id = trim($matches[1]); 1435 | break; 1436 | } 1437 | } 1438 | } 1439 | 1440 | return $this->last_smtp_transaction_id; 1441 | } 1442 | 1443 | /** 1444 | * Get the queue/transaction ID of the last SMTP transaction 1445 | * If no reply has been received yet, it will return null. 1446 | * If no pattern was matched, it will return false. 1447 | * 1448 | * @return bool|string|null 1449 | * 1450 | * @see recordLastTransactionID() 1451 | */ 1452 | public function getLastTransactionID() 1453 | { 1454 | return $this->last_smtp_transaction_id; 1455 | } 1456 | } 1457 | -------------------------------------------------------------------------------- /src/scss/_fonts.scss: -------------------------------------------------------------------------------- 1 | // @include font-face("Muller", "MullerRegular", 400, normal); 2 | -------------------------------------------------------------------------------- /src/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | @import "./mixins/breakpoint"; 2 | @import "./mixins/burger"; 3 | @import "./mixins/checkbox"; 4 | @import "./mixins/font-face"; 5 | @import "./mixins/flex"; 6 | @import "./mixins/mini"; 7 | @import "./mixins/tabs"; 8 | @import "./mixins/disable-mob-hover"; 9 | @import "./mixins/layout"; 10 | -------------------------------------------------------------------------------- /src/scss/_settings.scss: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | .page { 8 | height: 100%; 9 | font-family: var(--font-family, sans-serif); 10 | -webkit-text-size-adjust: 100%; 11 | scroll-behavior: smooth; 12 | } 13 | 14 | .page__body { 15 | margin: 0; 16 | min-width: 360px; 17 | min-height: 100%; 18 | font-size: 16px; 19 | } 20 | 21 | img { 22 | height: auto; 23 | max-width: 100%; 24 | object-fit: cover; 25 | } 26 | 27 | a { 28 | text-decoration: none; 29 | } 30 | 31 | .site-container { 32 | overflow: hidden; // если используете на сайте position: sticky - уберите эту настройку 33 | } 34 | 35 | .is-hidden { 36 | display: none !important; /* stylelint-disable-line declaration-no-important */ 37 | } 38 | 39 | .btn-reset { 40 | border: none; 41 | padding: 0; 42 | background-color: transparent; 43 | cursor: pointer; 44 | } 45 | 46 | .list-reset { 47 | list-style: none; 48 | margin: 0; 49 | padding: 0; 50 | } 51 | 52 | .input-reset { 53 | -webkit-appearance: none; 54 | appearance: none; 55 | border: none; 56 | border-radius: 0; 57 | background-color: #fff; 58 | 59 | &::-webkit-search-decoration, 60 | &::-webkit-search-cancel-button, 61 | &::-webkit-search-results-button, 62 | &::-webkit-search-results-decoration { 63 | display: none; 64 | } 65 | } 66 | 67 | .visually-hidden { 68 | position: absolute; 69 | overflow: hidden; 70 | margin: -1px; 71 | border: 0; 72 | padding: 0; 73 | width: 1px; 74 | height: 1px; 75 | clip: rect(0 0 0 0); 76 | } 77 | 78 | .container { 79 | margin: 0 auto; 80 | padding: 0 var(--container-offset); 81 | max-width: var(--container-width); 82 | } 83 | 84 | .centered { 85 | text-align: center; 86 | } 87 | 88 | .dis-scroll { 89 | position: fixed; 90 | left: 0; 91 | top: 0; 92 | overflow: hidden; 93 | width: 100%; 94 | height: 100vh; 95 | overscroll-behavior: none; 96 | } 97 | 98 | .page--ios .dis-scroll { 99 | position: relative; 100 | } 101 | -------------------------------------------------------------------------------- /src/scss/_vars.scss: -------------------------------------------------------------------------------- 1 | // если вы хотите использовать sass-переменные - удалите root 2 | // colors 3 | :root { 4 | // base 5 | --font-family: "Open Sans", sans-serif; 6 | --content-width: 1170px; 7 | --container-offset: 15px; 8 | --container-width: calc(var(--content-width) + (var(--container-offset) * 2)); 9 | 10 | // colors 11 | --light-color: #fff; 12 | } 13 | -------------------------------------------------------------------------------- /src/scss/components/_header.scss: -------------------------------------------------------------------------------- 1 | // ваш код 2 | -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | // базовые подключения 2 | @import "vars"; 3 | @import "mixins"; 4 | @import "fonts"; 5 | @import "settings"; 6 | 7 | // подключения компонентов страницы 8 | @import "./components/header"; 9 | 10 | // тестовые стили, удалите их 11 | .page__body { 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | height: var(--vh); 16 | } 17 | -------------------------------------------------------------------------------- /src/scss/mixins/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | @mixin for-desktop { 4 | @media (min-width: (1025px)) { 5 | @content; 6 | } 7 | } 8 | 9 | @mixin big-desktop { 10 | @media (max-width: (1440px)) { 11 | @content; 12 | } 13 | } 14 | 15 | @mixin tablet { 16 | @media (max-width: (1024px)) { 17 | @content; 18 | } 19 | } 20 | 21 | @mixin small-tablet { 22 | @media (max-width: (768px)) { 23 | @content; 24 | } 25 | } 26 | 27 | @mixin mobile { 28 | @media (max-width: (576px)) { 29 | @content; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/scss/mixins/_burger.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | @mixin burger { 4 | --burger-width: 30px; 5 | --burger-height: 30px; 6 | --burger-line-height: 2px; 7 | 8 | position: relative; 9 | border: none; 10 | padding: 0; 11 | width: var(--burger-width); 12 | height: var(--burger-height); 13 | color: #000; 14 | background-color: transparent; 15 | cursor: pointer; 16 | 17 | &::before, 18 | &::after { 19 | content: ""; 20 | position: absolute; 21 | left: 0; 22 | width: 100%; 23 | height: var(--burger-line-height); 24 | background-color: currentColor; 25 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out; 26 | } 27 | 28 | &::before { 29 | top: 0; 30 | } 31 | 32 | &::after { 33 | top: calc(100% - var(--burger-line-height)); 34 | } 35 | 36 | &__line { 37 | position: absolute; 38 | left: 0; 39 | top: 50%; 40 | width: 100%; 41 | height: var(--burger-line-height); 42 | background-color: currentColor; 43 | transform: translateY(-50%); 44 | transition: transform 0.3s ease-in-out; 45 | } 46 | 47 | &--active { 48 | &::before { 49 | top: 50%; 50 | transform: rotate(45deg); 51 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out; 52 | } 53 | 54 | &::after { 55 | top: 50%; 56 | transform: rotate(-45deg); 57 | transition: transform 0.3s ease-in-out, top 0.3s ease-in-out; 58 | } 59 | 60 | .burger__line { 61 | transform: scale(0); 62 | transition: transform 0.3s ease-in-out; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/scss/mixins/_checkbox.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | @mixin custom-checkbox { 4 | position: relative; 5 | 6 | &__field { 7 | appearance: none; 8 | position: absolute; 9 | } 10 | 11 | &__content { 12 | padding-left: 25px; 13 | cursor: pointer; 14 | 15 | &::before { 16 | content: ""; 17 | position: absolute; 18 | left: 0; 19 | top: 0; 20 | border: 1px solid #000; 21 | width: 15px; 22 | height: 15px; 23 | box-sizing: border-box; 24 | } 25 | 26 | &::after { 27 | content: ""; 28 | position: absolute; 29 | left: 0; 30 | top: 0; 31 | width: 15px; 32 | height: 15px; 33 | box-sizing: border-box; 34 | background-image: url("data:image/svg+xml,%3Csvg version='1.1' id='fi_32282' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='405.272px' height='405.272px' viewBox='0 0 405.272 405.272' style='enable-background:new 0 0 405.272 405.272;' xml:space='preserve'%3E%3Cg%3E%3Cpath d='M393.401,124.425L179.603,338.208c-15.832,15.835-41.514,15.835-57.361,0L11.878,227.836 c-15.838-15.835-15.838-41.52,0-57.358c15.841-15.841,41.521-15.841,57.355-0.006l81.698,81.699L336.037,67.064 c15.841-15.841,41.523-15.829,57.358,0C409.23,82.902,409.23,108.578,393.401,124.425z'%3E%3C/path%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3Cg%3E%3C/g%3E%3C/svg%3E"); 35 | background-position: center; 36 | background-size: 10px 10px; 37 | background-repeat: no-repeat; 38 | opacity: 0; 39 | transition: opacity 0.3s ease-in-out; 40 | } 41 | } 42 | } 43 | 44 | .custom-checkbox__field:checked + .custom-checkbox__content::after { 45 | opacity: 1; 46 | } 47 | 48 | .custom-checkbox__field:focus + .custom-checkbox__content::before { 49 | outline: 2px solid #f00; 50 | outline-offset: 2px; 51 | } 52 | 53 | .custom-checkbox__field:disabled + .custom-checkbox__content { 54 | opacity: 0.4; 55 | pointer-events: none; 56 | } 57 | -------------------------------------------------------------------------------- /src/scss/mixins/_disable-mob-hover.scss: -------------------------------------------------------------------------------- 1 | @mixin hover { 2 | @media (any-hover: hover) { 3 | &:hover { 4 | @content; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/scss/mixins/_flex.scss: -------------------------------------------------------------------------------- 1 | @mixin flex { 2 | display: flex; 3 | } 4 | 5 | @mixin flex-v-center { 6 | display: flex; 7 | align-items: center; 8 | } 9 | 10 | @mixin flex-h-center { 11 | display: flex; 12 | justify-content: center; 13 | } 14 | 15 | @mixin flex-all-center { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | } 20 | 21 | @mixin flex-all-sb { 22 | display: flex; 23 | align-items: center; 24 | justify-content: space-between; 25 | } 26 | -------------------------------------------------------------------------------- /src/scss/mixins/_font-face.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | @mixin font-face($font-family, $url, $weight, $style) { 3 | @font-face { 4 | font-family: "#{$font-family}"; 5 | src: url('../fonts/#{$url}.woff2') format("woff2"); 6 | font-weight: #{$weight}; 7 | font-display: swap; 8 | font-style: $style; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/scss/mixins/_layout.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable value-keyword-case */ 2 | @use "sass:map"; 3 | 4 | @mixin flex-layout($options) { 5 | 6 | .#{map.get($options, parentClass)} { 7 | --gap: #{map.get($options, desktopGap)}; 8 | --elems: #{map.get($options, desktopElems)}; 9 | 10 | display: flex; 11 | flex-wrap: wrap; 12 | gap: var(--gap); 13 | } 14 | 15 | .#{map.get($options, itemsClass)} { 16 | width: calc((100% - ((var(--elems) - 1) * var(--gap))) / (var(--elems))); 17 | 18 | @media (max-width: map.get($options, tablet)) { 19 | --gap: #{map.get($options, tabletGap)}; 20 | --elems: #{map.get($options, tabletElems)}; 21 | } 22 | 23 | @media (max-width: map.get($options, mobile)) { 24 | --gap: #{map.get($options, mobileGap)}; 25 | --elems: #{map.get($options, mobileElems)}; 26 | } 27 | } 28 | } 29 | 30 | // $options: ( 31 | // parentClass: "cards", 32 | // itemsClass: "cards__item", 33 | // desktopGap: 30px, 34 | // desktopElems: 3, 35 | // tablet: "1024px", 36 | // tabletElems: 2, 37 | // tabletGap: 30px, 38 | // mobile: "600px", 39 | // mobileElems: 1, 40 | // mobileGap: 20px 41 | // ); 42 | 43 | // @include flex-layout($options); 44 | -------------------------------------------------------------------------------- /src/scss/mixins/_mini.scss: -------------------------------------------------------------------------------- 1 | /* stylelint-disable */ 2 | 3 | @mixin bg-cover { 4 | background-position: center; 5 | background-size: cover; 6 | background-repeat: no-repeat; 7 | } 8 | 9 | $browser-context: 16; // Default 10 | 11 | @function rem($pixels, $context: $browser-context) { 12 | @return #{$pixels/$context}rem; 13 | } 14 | 15 | @mixin image-set($pathToImage) { 16 | background-image: url("#{$pathToImage}.jpg"); 17 | background-image: image-set( 18 | "#{$pathToImage}.webp"type("image/webp"), 19 | "#{$pathToImage}.jpg"type("image/jpg") 20 | ); 21 | } 22 | 23 | @mixin footerToBottom { 24 | display: grid; 25 | grid-template-columns: 100%; 26 | grid-template-rows: auto 1fr auto; 27 | min-height: 100vh; 28 | } 29 | 30 | @mixin mr($value) { 31 | &:not(:last-child) { 32 | margin-right: $value; 33 | } 34 | } 35 | 36 | @mixin ml($value) { 37 | &:not(:last-child) { 38 | margin-left: $value; 39 | } 40 | } 41 | 42 | @mixin mb($value) { 43 | &:not(:last-child) { 44 | margin-bottom: $value; 45 | } 46 | } 47 | 48 | @mixin mt($value) { 49 | &:not(:last-child) { 50 | margin-top: $value; 51 | } 52 | } 53 | 54 | @mixin pseudo() { 55 | content: ""; 56 | display: block; 57 | } 58 | -------------------------------------------------------------------------------- /src/scss/mixins/_tabs.scss: -------------------------------------------------------------------------------- 1 | @mixin tabs { 2 | .tabs__nav-btn--active { 3 | background-color: #ff0001; 4 | } 5 | 6 | .tabs__panel { 7 | display: none; 8 | } 9 | 10 | .tabs__panel--active { 11 | display: block; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/scss/vendor.scss: -------------------------------------------------------------------------------- 1 | @import "./vendor/normalize"; 2 | 3 | // стили для модального окна 4 | // @import "../../node_modules/graph-modal/dist/graph-modal.min"; 5 | 6 | // стили для табов 7 | // @import "../../node_modules/graph-tabs/dist/graph-tabs.min"; 8 | 9 | // стили для библиотеки aos 10 | // @import "../../node_modules/aos/dist/aos"; 11 | 12 | // стили для свайпера 13 | // @import "../../node_modules/swiper/swiper-bundle.min"; 14 | 15 | // стили для скролла simplebar 16 | // @import "../../node_modules/simplebar/dist/simplebar.min"; 17 | -------------------------------------------------------------------------------- /src/scss/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; 13 | /* 1 */ 14 | -webkit-text-size-adjust: 100%; 15 | /* 2 */ 16 | } 17 | 18 | /* Sections 19 | ========================================================================== */ 20 | 21 | /** 22 | * Remove the margin in all browsers. 23 | */ 24 | 25 | body { 26 | margin: 0; 27 | } 28 | 29 | /** 30 | * Render the `main` element consistently in IE. 31 | */ 32 | 33 | main { 34 | display: block; 35 | } 36 | 37 | /** 38 | * Correct the font size and margin on `h1` elements within `section` and 39 | * `article` contexts in Chrome, Firefox, and Safari. 40 | */ 41 | 42 | h1 { 43 | font-size: 2em; 44 | margin: 0.67em 0; 45 | } 46 | 47 | /* Grouping content 48 | ========================================================================== */ 49 | 50 | /** 51 | * 1. Add the correct box sizing in Firefox. 52 | * 2. Show the overflow in Edge and IE. 53 | */ 54 | 55 | hr { 56 | box-sizing: content-box; 57 | /* 1 */ 58 | height: 0; 59 | /* 1 */ 60 | overflow: visible; 61 | /* 2 */ 62 | } 63 | 64 | /** 65 | * 1. Correct the inheritance and scaling of font size in all browsers. 66 | * 2. Correct the odd `em` font sizing in all browsers. 67 | */ 68 | 69 | pre { 70 | font-family: monospace, monospace; 71 | /* 1 */ 72 | font-size: 1em; 73 | /* 2 */ 74 | } 75 | 76 | /* Text-level semantics 77 | ========================================================================== */ 78 | 79 | /** 80 | * Remove the gray background on active links in IE 10. 81 | */ 82 | 83 | a { 84 | background-color: transparent; 85 | } 86 | 87 | /** 88 | * 1. Remove the bottom border in Chrome 57- 89 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 90 | */ 91 | 92 | abbr[title] { 93 | border-bottom: none; 94 | /* 1 */ 95 | text-decoration: underline; 96 | /* 2 */ 97 | text-decoration: underline dotted; 98 | /* 2 */ 99 | } 100 | 101 | /** 102 | * Add the correct font weight in Chrome, Edge, and Safari. 103 | */ 104 | 105 | b, 106 | strong { 107 | font-weight: bolder; 108 | } 109 | 110 | /** 111 | * 1. Correct the inheritance and scaling of font size in all browsers. 112 | * 2. Correct the odd `em` font sizing in all browsers. 113 | */ 114 | 115 | code, 116 | kbd, 117 | samp { 118 | font-family: monospace, monospace; 119 | /* 1 */ 120 | font-size: 1em; 121 | /* 2 */ 122 | } 123 | 124 | /** 125 | * Add the correct font size in all browsers. 126 | */ 127 | 128 | small { 129 | font-size: 80%; 130 | } 131 | 132 | /** 133 | * Prevent `sub` and `sup` elements from affecting the line height in 134 | * all browsers. 135 | */ 136 | 137 | sub, 138 | sup { 139 | font-size: 75%; 140 | line-height: 0; 141 | position: relative; 142 | vertical-align: baseline; 143 | } 144 | 145 | sub { 146 | bottom: -0.25em; 147 | } 148 | 149 | sup { 150 | top: -0.5em; 151 | } 152 | 153 | /* Embedded content 154 | ========================================================================== */ 155 | 156 | /** 157 | * Remove the border on images inside links in IE 10. 158 | */ 159 | 160 | img { 161 | border-style: none; 162 | } 163 | 164 | /* Forms 165 | ========================================================================== */ 166 | 167 | /** 168 | * 1. Change the font styles in all browsers. 169 | * 2. Remove the margin in Firefox and Safari. 170 | */ 171 | 172 | button, 173 | input, 174 | optgroup, 175 | select, 176 | textarea { 177 | font-family: inherit; 178 | /* 1 */ 179 | font-size: 100%; 180 | /* 1 */ 181 | line-height: 1.15; 182 | /* 1 */ 183 | margin: 0; 184 | /* 2 */ 185 | } 186 | 187 | /** 188 | * Show the overflow in IE. 189 | * 1. Show the overflow in Edge. 190 | */ 191 | 192 | button, 193 | input { 194 | /* 1 */ 195 | overflow: visible; 196 | } 197 | 198 | /** 199 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 200 | * 1. Remove the inheritance of text transform in Firefox. 201 | */ 202 | 203 | button, 204 | select { 205 | /* 1 */ 206 | text-transform: none; 207 | } 208 | 209 | /** 210 | * Correct the inability to style clickable types in iOS and Safari. 211 | */ 212 | 213 | button, 214 | [type="button"], 215 | [type="reset"], 216 | [type="submit"] { 217 | -webkit-appearance: button; 218 | } 219 | 220 | /** 221 | * Remove the inner border and padding in Firefox. 222 | */ 223 | 224 | button::-moz-focus-inner, 225 | [type="button"]::-moz-focus-inner, 226 | [type="reset"]::-moz-focus-inner, 227 | [type="submit"]::-moz-focus-inner { 228 | border-style: none; 229 | padding: 0; 230 | } 231 | 232 | /** 233 | * Restore the focus styles unset by the previous rule. 234 | */ 235 | 236 | button:-moz-focusring, 237 | [type="button"]:-moz-focusring, 238 | [type="reset"]:-moz-focusring, 239 | [type="submit"]:-moz-focusring { 240 | outline: 1px dotted ButtonText; 241 | } 242 | 243 | /** 244 | * Correct the padding in Firefox. 245 | */ 246 | 247 | fieldset { 248 | padding: 0.35em 0.75em 0.625em; 249 | } 250 | 251 | /** 252 | * 1. Correct the text wrapping in Edge and IE. 253 | * 2. Correct the color inheritance from `fieldset` elements in IE. 254 | * 3. Remove the padding so developers are not caught out when they zero out 255 | * `fieldset` elements in all browsers. 256 | */ 257 | 258 | legend { 259 | box-sizing: border-box; 260 | /* 1 */ 261 | color: inherit; 262 | /* 2 */ 263 | display: table; 264 | /* 1 */ 265 | max-width: 100%; 266 | /* 1 */ 267 | padding: 0; 268 | /* 3 */ 269 | white-space: normal; 270 | /* 1 */ 271 | } 272 | 273 | /** 274 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 275 | */ 276 | 277 | progress { 278 | vertical-align: baseline; 279 | } 280 | 281 | /** 282 | * Remove the default vertical scrollbar in IE 10+. 283 | */ 284 | 285 | textarea { 286 | overflow: auto; 287 | } 288 | 289 | /** 290 | * 1. Add the correct box sizing in IE 10. 291 | * 2. Remove the padding in IE 10. 292 | */ 293 | 294 | [type="checkbox"], 295 | [type="radio"] { 296 | box-sizing: border-box; 297 | /* 1 */ 298 | padding: 0; 299 | /* 2 */ 300 | } 301 | 302 | /** 303 | * Correct the cursor style of increment and decrement buttons in Chrome. 304 | */ 305 | 306 | [type="number"]::-webkit-inner-spin-button, 307 | [type="number"]::-webkit-outer-spin-button { 308 | height: auto; 309 | } 310 | 311 | /** 312 | * 1. Correct the odd appearance in Chrome and Safari. 313 | * 2. Correct the outline style in Safari. 314 | */ 315 | 316 | [type="search"] { 317 | -webkit-appearance: textfield; 318 | /* 1 */ 319 | outline-offset: -2px; 320 | /* 2 */ 321 | } 322 | 323 | /** 324 | * Remove the inner padding in Chrome and Safari on macOS. 325 | */ 326 | 327 | [type="search"]::-webkit-search-decoration { 328 | -webkit-appearance: none; 329 | } 330 | 331 | /** 332 | * 1. Correct the inability to style clickable types in iOS and Safari. 333 | * 2. Change font properties to `inherit` in Safari. 334 | */ 335 | 336 | ::-webkit-file-upload-button { 337 | -webkit-appearance: button; 338 | /* 1 */ 339 | font: inherit; 340 | /* 2 */ 341 | } 342 | 343 | /* Interactive 344 | ========================================================================== */ 345 | 346 | /* 347 | * Add the correct display in Edge, IE 10+, and Firefox. 348 | */ 349 | 350 | details { 351 | display: block; 352 | } 353 | 354 | /* 355 | * Add the correct display in all browsers. 356 | */ 357 | 358 | summary { 359 | display: list-item; 360 | } 361 | 362 | /* Misc 363 | ========================================================================== */ 364 | 365 | /** 366 | * Add the correct display in IE 10+. 367 | */ 368 | 369 | template { 370 | display: none; 371 | } 372 | 373 | /** 374 | * Add the correct display in IE 10. 375 | */ 376 | 377 | [hidden] { 378 | display: none; 379 | } --------------------------------------------------------------------------------