Domanart.pl

Tworzenie layoutu 2

Wracamy do cięcia layoutu. Tym razem będzie o wiele prościej.

Kolejne sekcje zaczynają się od tytułu, który jest identyczny dla wszystkich sekcji. Dla każdej sekcji poza klasą dodam id, dzięki czemu w przyszłości będę mógł do niej linkować.


<section class="section section-love-music" id="love-music">
    <div class="container">
        <h2 class="section-title">
            <span>
                stay tuned
            </span>
            Contact With Us
        </h2>

        <!-- tutaj artykuły ze zdjęciami -->
    </div>
</section>

Od razu też stwórzmy stylowanie dla sekcji i nagłówka .section-header. Ja wrzucę to do klas ogólnych, ale może lepiej stworzyć plik _section.scss?


//src/scss/components/_class.scss

.section {
    padding: 6.250em 0;
}

.section-title {
	margin-top:1rem;
	margin-bottom:8rem;
	text-align: center;
	font-size:4rem;
	text-transform: uppercase;
	position: relative;
	padding-bottom:2rem;
}
.section-title span {
	text-align: center;
	font-size:2rem;
	text-transform: lowercase;
	display: block;
	font-weight: normal;
}
.section-title:after {
	content:'';
	position: absolute;
	left:50%;
	bottom:0;
	transform:translateX(-50%);
	width:12rem;
	height:2px;
	background: $color-main;
}

W powyższym kodzie HTML dodaliśmy .container, który centruje nam treść w poziomie. Pozostaje wrzucić do niego trzy artykuły.


<section class="section section-love-music" id="love-music">
    <div class="container">
        <header class="section-header">
            <h2 class="section-header-title">
                <span>
                    learn how to
                </span>
                Love the music
            </h2>
        </header>

        <div class="row">
            <div class="col">
                <article class="box">...</article>
            </div>
            <div class="col">
                <article class="box">...</article>
            </div>
            <div class="col">
                <article class="box">...</article>
            </div>
        </div>
    </div>
</section>

Artykuły obok siebie możemy ustawić za pomocą grida, albo flexboxa. Nie ma tutaj jednej zasady.

Ja wybiorę flexboxa. Dzięki temu będę mógł zastosować automatyczne szerokości (flex: 1) w zależności od ilości kolumn.

Jeżeli chodzi o odstępy między kolumnami, jakiś czas temu w najnowszych przeglądarkach pojawiała się właściwość gap: 10px. Niestety nie zadziała ona w każdej przeglądarce. Zamiast tego możemy zastosować klasyczne podejście z padding.


//src/scss/components/_class.scss

.row {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
	grid-template-rows: 1fr;
	gap: 1rem;
}

@media only screen and (max-width:860px) {
    .row {
		grid-template-columns: 1fr;
		gap: 5rem;
    }
}

pojedynczy artykuł będzie miał postać:


<article class="box">
    <a href="" class="box-photo">
        <span class="box-photo-overlay"></span>
        <img src="images/img1.jpg" class="box-photo-img" alt="...">
    </a>
    <h3 class="box-title">
        Lorem ipsum sit
    </h3>
    <div class="box-content">
        Lorem ipsum dolor sit amet consectetur, adipisicing elit. Doloribus recusandae quas similique deserunt
    </div>
    <a href="" class="btn box-btn">Read more</a>
</article>

oraz stylowanie dla niego w pliku components/_box.scss, który importujemy w pliku style.scss:


//src/scss/components/_box.scss

.box {
	max-width: 50rem;
	margin-left: auto;
	margin-right: auto;
}

.box-photo {
	position: relative;
	display: inline-block;
	margin-bottom: 2rem;
}

.box-photo-img {
	overflow: hidden;
	display: block;
	max-width: 100%;
	height: auto;
}

.box-photo:hover .box-photo-overlay {
	opacity: 1;
}

.box-photo-overlay {
	opacity: 0;
	background: rgba(#111, 0.8);
	transition: 0.5s all;
	position: absolute;
	left: 0;
	top: 0;
	width: 100%;
	height: 100%;
}

.box-photo-overlay:before,
.box-photo-overlay:after {
	content: '';
	width: 6rem;
	height: 1px;
	background: rgba(#fff, 0.8);
	position: absolute;
	left: 50%;
	top: 50%;
	transform: translate(-50%, -50%);
}

.box-photo-overlay:after {
	transform: translate(-50%, -50%) rotate(-90deg);
}

.box-title {
	text-transform: uppercase;
	font-size: 1.8rem;
}

.box-content {
	margin-bottom: 4rem;
	color: #AAA;
}

.box-btn {
	text-transform: uppercase;
	font-weight: bold;
	color:#fff;
	background: #333;
	display: inline-block;
	border:0;
	padding:2rem 3.5rem;
	border-radius:4rem;
	min-width:16rem;
	transition:0.5s all;
	cursor: pointer;
	text-decoration: none;
}
.box-btn:hover {
	background: $color-main;
	color:#333;
}

Sekcja Our team

Sekcja jest bardzo podobna do tej powyższej, dlatego wygląd html będzie tutaj praktycznie identyczny. Dochodzi jedynie dodatkowa klasa, dzięki której zmienimy nieco wygląd:


<section class="section section-our-team" id="our-team">
    <div class="container">
        <h2 class="section-title">
            <span>
                behind the scenes
            </span>
            Meet our team
        </h2>

        <div class="row">
            <div class="col">
                <article class="box">
                    <a href="" class="box-photo">
                        <span class="box-photo-overlay"></span>
                        <img class="box-photo-img" src="images/photo1.png" alt="">
                    </a>
                    <h2 class="box-title">
                        Lorem ipsum sit
                    </h2>
                    <div class="box-content">
                        Lorem ipsum dolor sit amet consectetur, adipisicing elit. Doloribus recusandae quas similique deserunt
                    </div>
                    <a href="" class="btn box-btn">Read more</a>
                </article>
            </div>
            <div class="col">
                <article class="box">
                    ...
                </article>
            </div>
            <div class="col">
                <article class="box">
                    ...
                </article>
            </div>
            <div class="col">
                <article class="box">
                    ...
                </article>
            </div>
        </div>
    </div>
</section><!-- e: section-our-team -->

Dodajmy stylowanie w pliku components/_our-team.scss:


//src/scss/components/_main-our-team.scss

.section-our-team {

}

.section-our-team .box {
	padding: 0 2rem;
	text-align: center;
}

.section-our-team .box-photo img {
	border-radius: 50%;
}

.section-our-team .box-photo-overlay {
	border-radius: 50%;
}

Sekcja main parallax

Między sekcjami znajduje się dodatkowa sekcja z tłem. Jest nawet prostsza od powyższych.


<section class="section section-love-music" id="love-music">
    ...
<section>

<section class="main-parallax">
    <h2 class="main-parallax-title">
        <span>Love the</span> music 1
    </h2>
    <div class="main-parallax-text">
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Non, aut.
        Lorem ipsum dolor sit amet, consectetur
    </div>
</section><!-- e: main parallax -->

<section class="section section-our-team" id="about-us">
    ...
<section>

//src/scss/components/_main-parallax.scss

.main-parallax {
	$c1: rgba(#222, 0.4);

	height: 51rem;
	background-image: linear-gradient($c1, $c1),
		linear-gradient(90deg, rgba(#000, 0.6), rgba(#000, 0.1), rgba(#000, 0.6)),
		url(../images/banner-paralax.jpg);
	background-position: center center;
	background-attachment: fixed;
	display: flex;
	justify-content: center;
	align-items: center;
	flex-direction: column;
	position: relative;
}

.main-parallax-title {
	font-size: 6rem;
	text-transform: uppercase;
	color: #FFF;
	font-weight:200;
	margin: 0;
	text-align: center;
}

.main-parallax-title span {
	color: $color-main;
}

.main-parallax-text {
	font-size:1.8rem;
	color: rgba(white, 0.35);
	text-align: center;
	margin-top: 1.5rem;
	max-width: 84rem;
}

Sekcja kontakt


<section class="main-contact" id="contact">
    <div class="container">
        <h2 class="section-title">
            <span>
                stay tuned
            </span>
            Contact With Us
        </h2>

        <div class="main-contact-text">
            Lorem ipsum dolor sit, amet consectetur adipisicing elit. Illum, reiciendis ipsa illo dolorum quae accusantium architecto necessitatibus ad dolorem nam!
        </div>

        <form action="..." method="POST" id="contactForm" class="form">
            <div class="form-row">
                <label for="formName">Name and surname:</label>
                <input required type="text" name="name" id="formName" data-error-text="Wpisz poprawnie imię i nazwisko" placeholder="Name and surname">
            </div>
            <div class="form-row">
                <label for="formEmail">Email:</label>
                <input required type="email" name="email" id="formEmail" data-error-text="Wpisz poprawny email" placeholder="Email">
            </div>
            <div class="form-row">
                <label for="formMessage">Message:</label>
                <textarea required pattern=".+" name="message" data-error-text="Wpisz wiadomość" id="formMessage" placeholder="Message"></textarea>
            </div>
            <div class="form-row form-row-last">
                <button type="submit" class="btn btn-form">Wyślij</button>
            </div>
        </form>
    </div>
</section><!-- e: main contact -->

//src/scss/_mixins.scss

@mixin visuallyhidden() {
	border: 0;
	clip: rect(0 0 0 0);
	height: 1px;
	margin: -1px;
	overflow: hidden;
	padding: 0;
	position: absolute;
	width: 1px;
    white-space: nowrap;

	.focusable:active,
	.focusable:focus {
		clip: auto;
		height: auto;
		margin: 0;
		overflow: visible;
		position: static;
		width: auto;
	}
}

//components/_main-contact.scss

.main-contact {
    padding: 10rem 0;
}
.main-contact-text {
    margin-bottom: 7rem;
    color:#888;
}
.main-contact label {
    @include visuallyhidden();
}

//src/scss/components/_form.scss

.form {
}

.form-row {
	margin-bottom: 2rem;
}

.form input[type="text"],
.form input[type="email"],
.form textarea {
	padding: 2rem;
	border: 1px solid #C4C4C4;
	font-family: $font-main;
	border-radius: 0.7rem;
	width: 100%;
	max-width: 56rem;
	transition: 0.3s all;

	&:focus {
		border-color: #333;
		box-shadow: inset 0 0 0 1px #333;
		outline: none;
	}

	&.field-error {
		border-color: red;
	}
	&.field-error:focus {
		border-color: red;
		box-shadow: inset 0 0 0 1px red;
	}
}

.form textarea {
	max-width: 100%;
	min-height: 15rem;
	resize: vertical;
}

.form .form-error-text {
	margin-top: 0.5rem;
	color: red;
}

.form-row-last {
	display: flex;
	align-items: center;
}

.form-message {
	margin-left: 2rem;
	font-weight: bold;
}

Jeżeli chodzi o oskryptowanie tego formularza, to całość opisałem w tym artykule. Poniżej bezczelnie sobie zapożyczę kod z końcowego dema. Do ostatniej funkcji dodałem tylko export.


//src/js/_form.js

function removeFieldError(field) {
    const errorText = field.nextElementSibling;
    if (errorText !== null) {
        if (errorText.classList.contains("form-error-text")) {
            errorText.remove();
        }
    }
};

function createFieldError(field, text) {
    removeFieldError(field); //przed stworzeniem usuwam by zawsze był najnowszy komunikat

    const div = document.createElement("div");
    div.classList.add("form-error-text");
    div.innerText = text;
    if (field.nextElementSibling === null) {
        field.parentElement.appendChild(div);
    } else {
        if (!field.nextElementSibling.classList.contains("form-error-text")) {
            field.parentElement.insertBefore(div, field.nextElementSibling);
        }
    }
};

function toggleErrorField(field, show) {
    const errorText = field.nextElementSibling;
    if (errorText !== null) {
        if (errorText.classList.contains("form-error-text")) {
            errorText.style.display = show ? "block" : "none";
            errorText.setAttribute('aria-hidden', show);
        }
    }
};

function markFieldAsError(field, show) {
    if (show) {
        field.classList.add("field-error");
    } else {
        field.classList.remove("field-error");
        toggleErrorField(field, false);
    }
};

export default function() {
    const form = document.querySelector("#contactForm");
    const inputs = form.querySelectorAll("[required]");

    //wyłączamy domyślną walidację
    form.setAttribute("novalidate", true);

    for (const el of inputs) {
        el.addEventListener("input", e => markFieldAsError(e.target, !e.target.checkValidity()));
    }

    form.addEventListener("submit", e => {
        e.preventDefault();

        let formErrors = false;

        //2 etap - sprawdzamy poszczególne pola gdy ktoś chce wysłać formularz
        for (const el of inputs) {
            removeFieldError(el);
            el.classList.remove("field-error");

            if (!el.checkValidity()) {
                console.log(el.dataset.errorText);
                createFieldError(el, el.dataset.errorText);
                el.classList.add("field-error");
                formErrors = true;
            }
        }

        if (!formErrors) {
            const submit = form.querySelector("[type=submit]");
            submit.disabled = true;
            submit.classList.add("element-is-busy");

            const formData = new FormData();
            for (const el of inputs) {
                formData.append(el.name, el.value)
            }

            const url = form.getAttribute("action");
            const method = form.getAttribute("method");

            fetch(url, {
                method: method.toUpperCase(),
                body: formData
            })
            .then(res => res.json())
            .then(res => {
                if (res.errors) {
                    const selectors = res.errors.map(el => `[name="${el}"]`);
                    const fieldsWithErrors = form.querySelectorAll(selectors.join(","));
                    for (const el of fieldsWithErrors) {
                        markFieldAsError(el, true);
                        toggleErrorField(el, true);
                    }
                } else {
                    if (res.status === "ok") {
                        const div = document.createElement("div");
                        div.classList.add("form-send-success");
                        div.innerText = "Wysłanie wiadomości się nie powiodło";

                        form.parentElement.insertBefore(div, form);
                        div.innerHTML = `
                            <strong>Wiadomość została wysłana</strong>
                            <span>Dziękujemy za kontakt. Postaramy się odpowiedzieć jak najszybciej</span>
                        `;
                        form.remove();
                    }
                    if (res.status === "error") {
                        //jeżeli istnieje komunikat o błędzie wysyłki
                        //np. generowany przy poprzednim wysyłaniu formularza
                        //usuwamy go, by nie duplikować tych komunikatów
                        const statusError = document.querySelector(".form-send-error");
                        if (statusError) {
                            statusError.remove();
                        }

                        const div = document.createElement("div");
                        div.classList.add("form-send-error");
                        div.innerText = "Wysłanie wiadomości się nie powiodło";
                        submit.parentElement.appendChild(div);
                    }
                }
            }).finally(() => {
                submit.disabled = false;
                submit.classList.remove("element-is-busy");
            });
        }
    });
}

//src/js/_app.js

import { pageHeaderSticky } from "./page-header";
import makeForm from "./_form";

document.addEventListener('DOMContentLoaded', function() {
    pageHeaderSticky();
    makeForm();
});

W naszym przypadku brakuje w zasadzie tylko odrobinę stylowanie. Po pierwsze klasa .loading, którą zaaplikujemy dla przycisku wysyłającego:


//src/scss/components/_class.scss

.loading {
	position: relative;
}

.loading:before {
	content:'';
	display: block;
	width:3rem;
	height:3rem;
	border:2px solid rgba(#fff, 0.3);
	border-right-color:#fff;
	border-radius: 50%;
	position: absolute;
	left:50%;
	top:50%;
	transform:translate(-50%, -50%) rotate(0deg);
	animation: loadingAnim 0.6s 0s infinite linear;
}

.btn.loading {
	cursor: default;
	padding-left:5rem;
	background: #333;
	color:rgba(#fff, 0.2);
}

.btn.loading:before {
	left:3rem;
}

Po wysyłce tworzony będzie element `.form-send-success`, lub `.form-send-error`. Je także ostylujmy:


.form-send-success,
.form-send-error {
	border: 1px solid #ddd;
	padding: 3rem;

	strong {
		font-size: 2rem;
		display: block;
		margin-bottom: 1rem;
		color: $color-main;
	}
	span {
		display: block;
	}
}

Mapa

Kolejnym elementem będzie mapa. Dodajmy go na do html, a także dodajmy mu małe tylowanie:


<div class="main-map" id="mainMap">
</div>

//src/scss/component/_map.scss
.main-map {
    height: 31.250rem;
    background: #ddd;
}

Google mapa

Pierwszy sposób na dodanie mapy to użycie GoogleMap. Aby to zrobić po pierwsze musimy wygenerować nasz prywatny klucz. Możemy to zrobić na stronie https://developers.google.com/maps/documentation przechodząc do zakładki Maps Javascript Api.

Aby zdobyć klucz musimy podać dane odnośnie naszej karty płatniczej. Jakiś czas temu Google mocno zmieniło podejście do developerów korzystających z ich usług. Ceny poszły w górę, ale przede wszystkim straciliśmy możliwość anonimowego używania ich produktów - konieczne staje się podpięcie karty. Jeżeli wcześniej robiłeś zakupy na AppStore, to będziesz używał tego samego konta.

Po wygenerowaniu klucza, do HTML dodajemy linijkę ze skryptem:


<script async defer src="https://maps.googleapis.com/maps/api/js?key=TWOJ_KLUCZ&callback=initMap"></script>

następnie tworzymy nowy plik src/js/_map.js, w którym umieszczamy kod:


function mainMap() {
    const centerPnt = {lat: 52.229675, lng: 21.012230};
    const map = new google.maps.Map(document.getElementById("mainMap"), {
        zoom: 16,
        center: centerPnt,
        streetViewControl: false,
        mapTypeControl: false,
        styles : [...]
    });
    const marker = new google.maps.Marker({
        position: centerPnt,
        map: map,
        icon : 'images/marker.png'
    });
}

export { mainMap }

W powyższym linku do google map, który wrzuciliśmy do HTML wywoływana jest funkcja initMap. Jest to sposób na ominięcie zabezpieczeń CORS. Żeby mapa zadziałała, musimy naszą powyższą funkcję mainMap wystawić poza nasz budowany webpackiem skrypt. Możemy to zrobić ustawiając ją jako właściwość obiektu window:


//src/js/_app.js

import { pageHeaderSticky } from "./page-header";
import makeForm from "./_form";
import mainMap from "./_map";

window.initMap = mainMap;

document.addEventListener('DOMContentLoaded', function() {
    pageHeaderSticky();
    makeForm();
});

Kolorystykę (wartość właściwości style) dla mapy pobrałem ze strony https://snazzymaps.com/, natomiast pozycję na którą wskazuje mapa za pomocą strony https://www.latlong.net/.

Mapa MapBox

Inną możliwością jest użycie map MapBox. Tutaj karty nie musimy podawać, jedyny wymóg to zarejestrowanie się na ich stronie, a i liczba darmowych odwołań do mapy jest o wiele większa.

Po zalogowaniu musimy stworzyć swój klucz poprzez kliknięcie w przycisk ”+ create a token”. Po otrzymaniu klucza, możemy stworzyć naszą mapę poprzez kliknięcie “Or install the Maps SDK: Web”. W kolejnych krokach wybieram opcje jakie nas interesują.

Ja wybrałem opcję CDN. Wrzuciłem więc do html odpowiednie linijki:


<script src='https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js'></script>
<link href='https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css' rel='stylesheet' />

Przechodząc przez kolejne kroki dostaniemy kod, który musimy dodać do naszej strony. Kod ten będzie załączał mapę z domyślnym wyglądem. Żeby go zmienić musimy wejść w opcję Studio, która znajduje się tuż pod avatarem w prawym górnym rogu strony MapBox.

Możemy generować własny styl, ale też możemy wybrać jeden z dostępnych na liście. Ja wybrałem ten ciemny. Kliknąłem na ikonke z 2 kropkami przy tym stylu, a następnie skopiowałem url do tych styli.

Po tych czynnościach możemy utworzyć plik src/js/_map.js w którym dodamy kod:


const position = [20.99995, 52.23277]; //pozycja na mapie
const token = "TWOJ_TOKEN"; //twój własny token
const styleUrl = "mapbox://..."; //adres do twoich styli

function map() {
	mapboxgl.accessToken = token;
    const map = new mapboxgl.Map({
        container: 'mainMap',
        style: styleUrl,
        zoom:13.7,
        center: position
    });
}

export { map }

który następnie zaimportowałem do pliku src/js/app.js:


//src/js/_app.js

import { pageHeaderSticky } from "./page-header";
import makeForm from "./_form";
import {map} from './js/map';

document.addEventListener('DOMContentLoaded', function() {
    pageHeaderSticky();
    makeForm();
    map();
});

Jeżeli chcemy dodać nasz własny marker, do funkcji map dodamy:


const position = [20.99995, 52.23277];
const token = "TWOJ_TOKEN";
const styleUrl = "mapbox://styles/mapbox/light-v9";

function map() {
	mapboxgl.accessToken = token;
    const map = new mapboxgl.Map({
        container: 'mainMap',
        style: styleUrl,
        zoom:13.7,
        center: position
    });

    map.on("load", function () {
      map.loadImage("images/marker.png", function(error, image) {
          if (error) throw error;
          map.addImage("custom-marker", image);
          map.addLayer({
            id: "markers",
            type: "symbol",
            source: {
              type: "geojson",
              data: {
                type: "FeatureCollection",
                features:[{"type":"Feature","geometry":{"type":"Point","coordinates":position}}]}
            },
            layout: {
              "icon-image": "custom-marker",
            }
          });
        });
    });
}

export { map }

Pozostaje stopka.


<footer class="page-footer">
    <div class="container">
        <ul class="page-footer-list">
            <li><a href="">Home Page</a></li>
            <li><a href="">About Us</a></li>
            <li><a href="">Gallery</a></li>
            <li><a href="">Contact</a></li>
        </ul>

        <span class="copyright">
            © Copyright 2020
        </span>
    </div>
</footer>

//src/scss/_page-footer.scss

.page-footer {
    background: #222;
    padding: 2.5rem 0;
}
.page-footer .container {
    display: flex;
}
.page-footer-list {
    list-style:none;
    padding:0;
    margin:0;
}
.page-footer-list li {
    padding-bottom: 0.8rem;
}
.page-footer-list a {
    color: rgba(#fff, 0.8);
    text-transform: uppercase;
    font-size: 0.813rem;
    text-decoration: none;
    font-weight: bold;
    display: inline-block;
    transition: 0.4s color;
}
.page-footer-list a:hover {
    color: $color-main;
}
.page-footer .copyright {
    margin-left:auto;
    color:rgba(#fff, 0.7);
}

@media (max-width:550px) {
    .page-footer .container {
        display: flex;
        flex-direction: column;
        align-items: center;
    }
    .page-footer-list {
        text-align: center;
    }
    .page-footer .copyright {
        margin-top: 1.875rem;
        margin-left:0;
    }
}

Podsumowanie

Tutaj możesz zobaczyć wersję końcową.

DEMO