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 }
Footer
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ą.