Создаем сайт-портфолио с responsive-дизайном

Всем привет! Сегодня мы разберем, как написать полностью отзывчивый сайт-портфолио. При этом мы будем использовать только HTML, CSS и JS. Вы научитесь создавать responsive-дизайн и познакомитесь поближе с псевдоэлементами CSS.

Также мы расскажем вам о модуле nodemailer: с его помощью мы создадим рабочую контактную форму.

Если хотите посмотреть демо или даже весь туториал, вот видео:

Итак, приступим к написанию кода.

Код

Прежде чем начать писать код, давайте рассмотрим структуру папок.

И да, это NodeJS-приложение, потому что мы хотим получить рабочую почтовую систему, а отсылать почту могут только серверы, не клиентский браузер.

Как видите, у нас есть файл project.js. В этом файле содержатся данные нашего проекта. Наличие такого файла облегчает добавление, удаление и редактирование проектов. Давайте посмотрим структуру данных в этом файле.

let projects = [
    {
        name: "project one",
        tags: "#javascript, #fullstack, #ui/ux, #backend",
        image: "project (1).png",
    },
    {
        name: "project two",
        tags: "#javascript, #fullstack",
        image: "project (2).png",
    },
    // +8 more
]

Здесь у нас есть имя проекта, его теги и путь к изображению. Благодаря этому мы можем легко управлять проектом, не редактируя код.

Начнем работу с инициализации NPM.

NPM init

Откройте командную строку или терминал и перейдите внутрь вашей корневой директории (за пределами директории public). Запустите команду npm init. Это инициализирует NPM для ваших проектов.

Затем запустите следующую команду, чтобы установить библиотеки:

npm i express.js nodemon nodemailer dotenv
  • express.js — для создания сервера
  • nodemon — для непрерывного запуска сервера
  • nodemailer — отвечает за отправку почты
  • dotenv — для создания переменных окружения. Нам это пригодится для хранения email id и пароля вне сервера.

После установки библиотек давайте внесем некоторые изменения в package.json. Откройте этот файл и измените данные в scripts.

"scripts": {
    "start": "nodemon server.js"
  },

После этого создайте файл server.js в вашей корневой директории (не в папке public). Откройте его.

Server.js

Начнем с импорта библиотек и пакетов.

const express = require('express');
const path = require('path');
const nodemailer = require('nodemailer');
const dotenv = require('dotenv');

Настроим dotenv, чтобы иметь доступ к переменным окружения.

dotenv.config();

Затем сохраните в переменной путь к вашей папке public и создайте сервер.

let initialPath = path.join(__dirname, "public");
let app = express();

Теперь используйте метод app.use для настройки связующих программ (middleware).

app.use(express.static(initialPath));
app.use(express.json());

express.json позволит делиться данными формы, а express.static установит папку public в качестве статичного пути.

После этого создайте путь к home. И отправьте файл index.html.

app.get('/', (req, res) => {
    res.sendFile(path.join(initialPath, "index.html"));
})

Наконец, установите прослушивание сервером порта 3000.

app.listen(3000, () => {
    console.log('listening.....');
})

Наш сервер готов. Давайте запустим его при помощи команды npm start (в терминале).

Переходим к работе над портфолио.

Портфолио

Откройте index.html и напишите базовую структуру HTML. Затем подключите к этому файлу style.css и app.js. После этого создайте панель навигации navbar.

<!-- navbar  -->
<nav class="navbar">
    <h1 class="brand">logo</h1>
    <div class="toggle-btn">
        <span></span>
        <span></span>
    </div>
    <ul class="links-container">
        <li class="links-item"><a href="#" class="link active">home</a></li>
        <li class="links-item"><a href="#project-section" class="link">project</a></li>
        <li class="links-item"><a href="#about-section" class="link">about</a></li>
        <li class="links-item"><a href="#contact-section" class="link">contact</a></li>
    </ul>
</nav>

Напишите стили для нее.

*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html{
    scroll-behavior: smooth;
}

body{
    width: 100%;
    position: relative;
    background: #1d1d1d;
    color: #fff;
    font-family: 'roboto', sans-serif;
}

/* navbar */

.navbar{
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 60px;
    background: #1d1d1d;
    padding: 0 10vw;
    display: flex;
    justify-content: space-between;
    align-items: center;
    z-index: 9;
}

.brand{
    text-transform: capitalize;
    font-weight: 500;
}

.links-container{
    display: flex;
    list-style: none;
}

.link{
    text-transform: capitalize;
    color: #fff;
    text-decoration: none;
    margin: 0 10px;
    padding: 10px;
    position: relative;
}

.link:hover:not(.active){
    opacity: 0.7;
}

.link.active::before,
.seperator::before{
    content: '';
    position: absolute;
    bottom: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 5px;
    height: 5px;
    border-radius: 50%;
    background: #fff;
}

.link.active::after,
.seperator::after{
    content: '';
    position: absolute;
    bottom: 2px;
    left: 0;
    width: 100%;
    height: 1px;
    background: #fff;
}

Примечания:

  1. В html у нас указано свойство scroll-behavior. Без этого не будет эффекта плавной прокрутки.
  2. Вы также могли заметить элемент seperator в стилях: не волнуйтесь, мы создадим его позже.

От редакции Techrocks. Работу свойства scroll-behavior можно посмотреть в статье «10 полезных приемов использования CSS».

Результат:

Теперь создадим шапку сайта.

Хедер

<!-- home section -->
<section class="home">
    <div class="hero-content">
        <h1 class="hero-heading"><span class="highlight">hi, </span>i am john</h1>
        <p class="profession">web developer</p>
        <p class="info">Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur odit in laudantium suscipit blanditiis asperiores.</p>
        <a href="#contact-section" class="btn">contact</a>
    </div>
    <img src="img/img1.png" class="image" alt="">
</section>
/* home section */

.home{
    width: 100%;
    min-height: calc(100vh - 60px);
    height: auto;
    margin-top: 60px;
    padding: 0 10vw;
    display: flex;
    align-items: center;
    justify-content: space-between;
    position: relative;
}

.hero-content{
    width: 50%;
}

.hero-heading{
    font-size: 5rem;
    text-transform: capitalize;
    font-weight: 500;
}

.highlight{
    color: #ff3559;
}

.profession{
    width: fit-content;
    display: block;
    margin: 10px 0 20px;
    margin-left: auto;
    text-transform: capitalize;
    position: relative;
    padding: 10px 20px;
    color: #1d1d1d;
    z-index: 2;
}

.profession::before{
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: #e3e3e3;
    z-index: -1;
    transform: skewX(10deg);
}

.profession::after{
    content: '';
    position: absolute;
    top: 0;
    left: -100px;
    width: 100px;
    height: 2px;
    background: #e3e3e3;
}

.info{
    line-height: 30px;
    margin-bottom: 50px;
}

.btn{
    padding: 10px 20px;
    text-decoration: none;
    border-radius: 50px;
    background: #ff3559;
    color: #fff;
    text-transform: capitalize;
    border: none;
}

Результат:

Отлично! Теперь перейдем к разделу about.

About

<!-- about section -->
<section class="about" id="about-section">
    <h2 class="heading">about <span class="highlight">me</span></h2>
    <p class="sub-heading">Lorem ipsum dolor sit amet consectetur. </p>
    <div class="seperator"></div>

    <div class="about-me-container">
        <div class="left-col">
            <img src="img/img2.png" class="about-image" alt="">
        </div>
        <div class="right-col">
            <p class="about-para">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Accusamus totam quia numquam tempora nostrum earum similique enim laudantium iusto. Quaerat illo numquam minus pariatur, cum qui ipsum sapiente, atque optio voluptatibus necessitatibus, quis dolores veniam delectus inventore beatae? Accusamus, illum! Non nam dolores assumenda quibusdam repellat beatae quae eum atque sed, velit culpa, at animi cumque suscipit. Ratione delectus dolores odit dicta ipsum libero molestiae et reprehenderit sapiente earum. Alias aut architecto quis, earum iusto beatae quibusdam maiores, rerum, consequatur aliquid doloribus? Quas accusantium quidem eos ex, aperiam recusandae. Veritatis?</p>
            <a href="#" class="btn">download cv</a>
        </div>
    </div>
</section>
/* about section */

.about{
    width: 100%;
    height: auto;
    padding: 50px 10vw;
}

.heading{
    text-align: center;
    font-weight: 500;
    font-size: 3.5rem;
    text-transform: capitalize;
}

.sub-heading{
    text-align: center;
    font-size: 1rem;
    margin: 10px;
    opacity: 0.7;
}

.seperator{
    width: 25%;
    margin: 20px auto;
    position: relative;
}

.about-me-container{
    margin: 150px 0 100px;
    width: 100%;
    display: grid;
    grid-template-columns: 40% 60%;
    grid-gap: 50px;
}

.left-col, .right-col{
    position: relative;
}

.left-col::before{
    content: 'yes, its me';
    text-transform: capitalize;
    position: absolute;
    right: 0;
    top: -20px;
}

.left-col::after{
    content: '';
    position: absolute;
    top: -10px;
    right: 80px;
    width: 50px;
    height: 2px;
    background: #fff;
    transform-origin: right;
    transform: rotate(-30deg);
}

.about-image{
    border-radius: 10px;
    box-shadow: 0 10px 10px rgba(0, 0, 0, 0.25);
}

.about-para{
    font-size: 1.2rem;
    font-weight: 300;
    line-height: 35px;
    margin-bottom: 40px;
}

Результат:

Следующий на очереди — раздел навыков (skills).

Skills

Эта структура будет находиться внутри раздела about.

<section class="about" id="about-section">
      //предыдущие элементы
<h2 class="heading">languages and framework i know</h2>
    <div class="seperator"></div>
    <div class="skill-container">
        <div class="skill-card" style="--bg: #f06529">
            <p class="skill">HTML</p>
        </div>
        <div class="skill-card" style="--bg: #379ad6">
            <p class="skill">CSS</p>
        </div>
        <div class="skill-card" style="--bg: #cc6699">
            <p class="skill">SCSS</p>
        </div>
        <div class="skill-card" style="--bg: #f7df1e">
            <p class="skill">JavaScript</p>
        </div>
        <div class="skill-card large" style="--bg: #5ed9fb">
            <p class="skill">ReactJS</p>
        </div>
        <div class="skill-card large" style="--bg: #83cd29">
            <p class="skill">NodeJS</p>
        </div>
        <div class="skill-card" style="--bg: #326690">
            <p class="skill">Postgres SQL</p>
        </div>
        <div class="skill-card" style="--bg: #ffa000">
            <p class="skill">Firebase</p>
        </div>
        <div class="skill-card large" style="--bg: #5ed9fb">
            <p class="skill">Much More</p>
        </div>
    </div>
</section>

Как вы, наверное, заметили, у нас есть style="--bg: value" для элемента skill-card. Он устанавливает разные --bg для разных элементов. Таким образом мы получаем одинаковый эффект с разными цветами.

.skill-container{
    position: relative;
    margin-top: 100px;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-gap: 20px;
}

.skill-card{
    height: 200px;
    border-radius: 10px;
    border: 1px solid #464646;
    text-align: center;
    position: relative;
    cursor: pointer;
    transition: .5s;
}

.skill{
    font-size: 2rem;
    color: #464646;
    line-height: 200px;
}

.skill-card:hover{
    background: var(--bg);
}

.skill-card:hover .skill{
    color: #fff;
}

.skill-card.large{
    grid-column: 2 span;
}

Результат:

Цвет фона показывается при наведении

Теперь давайте создадим раздел project.

Project

Начнем с кнопок для фильтров.

<!-- project section -->
<section class="project" id="project-section">
    <h2 class="heading">Project<span class="highlight">s</span></h2>
    <p class="sub-heading">Lorem ipsum dolor sit amet consectetur. </p>
    <div class="seperator"></div>

    <div class="filters">
        <button class="filter-btn active" id="all">all</button>
        <button class="filter-btn" id="javascript">javaScript</button>
        <button class="filter-btn" id="ui">ui/ux</button>
        <button class="filter-btn" id="backend">backend</button>
        <button class="filter-btn" id="fullstack">fullStack</button>
    </div>
</section>

Атрибут id в фильтрах поможет нам в фильтрации проектов.

/* project section */

.project, .contact{
    position: relative;
    padding: 50px 10vw;
}

.filters{
    width: fit-content;
    display: block;
    margin: 100px auto;
}

.filter-btn{
    padding: 10px 20px;
    border-radius: 5px;
    border: none;
    text-transform: capitalize;
    margin: 0 5px 10px;
    cursor: pointer;
}

.filter-btn.active{
    background: #ff3559;
    color: #fff;
}

Результат:

Давайте создадим одну карточку проекта — в чисто стилистических целях.

<div class="project-container">
    <div class="project-card">
        <img src="img/project (1).png" alt="">
        <div class="content">
            <h1 class="project-name">project one</h1>
            <span class="tags">#javascript</span>
        </div>
    </div>
</div>
.project-container{
    width: 100%;
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-gap: 20px;
}

.project-card{
    position: relative;
    cursor: pointer;
    display: block;
}

.project-card img{
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.project-card .content{
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    display: flex;
    justify-content: center;
    align-items: center;
    transition: .5s;
    text-transform: capitalize;
    opacity: 0;
}

.project-name{
    font-weight: 300;
    font-size: 2.5rem;
    text-align: center;
}

.tags{
    position: absolute;
    bottom: 20px;
    opacity: 0.6;
    width: 90%;
}

.project-card:hover .content{
    opacity: 1;
}

.project-card.hide{
    display: none;
}

Результат:

Контент показывается при наведении

Теперь карточку проекта можно закомментировать.

<div class="project-container">
    <!-- <div class="project-card">
        <img src="img/project (1).png" alt="">
        <div class="content">
            <h1 class="project-name">project one</h1>
            <span class="tags">#javascript</span>
        </div>
    </div> -->
</div>

И давайте сделаем эту карточку динамической. Но прежде добавьте ваш файл project.js перед файлом app.js. Без этого вы не сможете получить доступ к данным проекта.

<script src="project.js"></script>
<script src="app.js"></script>

Открываем app.js. Перед созданием карточки проекта сделаем ссылки переключателями активного класса. Напишите такой код:

// links

const links = document.querySelectorAll('.link');

links.forEach(link => {
    link.addEventListener('click', () => {
        links.forEach(ele => ele.classList.remove('active'));
        link.classList.add('active');
    })
})

Вот теперь можно перейти к карточкам проекта. Пишем код:

// creating dynamic project card

const projectContainer = document.querySelector('.project-container');

projects.forEach(project => {
    projectContainer.innerHTML += `
    <div class="project-card" data-tags="#all, ${project.tags}">
        <img src="img/${project.image}" alt="">
        <div class="content">
            <h1 class="project-name">${project.name}</h1>
            <span class="tags">${project.tags}</span>
        </div>
    </div>
    `;
})

Как видите, мы просто выбираем контейнер проекта, а затем перебираем данные для создания карточек.

Результат:

Отлично! Теперь сделаем наши кнопки фильтрации рабочими.

// filters

const filters = document.querySelectorAll('.filter-btn');

filters.forEach(filterBtn => {
    filterBtn.addEventListener('click', () => {
        let id = filterBtn.getAttribute('id');
        let projectCards = document.querySelectorAll('.project-card');
        projectCards.forEach(card => {
            if(card.getAttribute('data-tags').includes(id)){
                card.classList.remove('hide');
            } else{
                card.classList.add('hide');
            }
        })

        filters.forEach(btn => btn.classList.remove('active'));
        filterBtn.classList.add('active');
    })
})

В этом коде мы просто добавляем событие click для кнопки filter и переключаем некоторые классы элементов.

Итак, наш раздел project полностью готов. Приступаем к работе над контактной формой.

Contact me

<!-- contact form -->
<section class="contact" id="contact-section">
    <h2 class="heading">Contact<span class="highlight"> me</span></h2>
    <p class="sub-heading">Lorem ipsum dolor sit amet consectetur. </p>
    <div class="seperator"></div>

    <div class="contact-form">
        <div class="name">
            <input type="text" class="first-name" required placeholder="first name">
            <input type="text" class="last-name" required placeholder="last name">
        </div>
        <input type="email" required class="email" placeholder="email">
        <textarea class="message" placeholder="message" required></textarea>
        <button class="btn contact-btn">contact</button>
    </div>
</section>

<footer class="footer">made with love by modern web</footer>
/* contact form */

.contact-form{
    width: 100%;
    margin-top: 100px;
    position: relative;
}

.contact-form input, .message{
    width: 100%;
    display: block;
    height: 50px;
    padding: 20px;
    border-radius: 5px;
    background: #000;
    color: #fff;
    border: none;
    outline: none;
    margin: 30px 0;
    text-transform: capitalize;
    resize: none;
}

.message{
    height: 200px;
}

.contact-form .name{
    display: flex;
    justify-content: space-between;
}

.name input{
    width: 49%;
    margin: 0;
}

.contact-form .btn{
    display: block;
    margin: auto;
    cursor: pointer;
}

/* footer */

.footer{
    width: 100%;
    height: 30px;
    text-align: center;
    background-color: #ff3559;
    text-transform: capitalize;
    line-height: 30px;
}

Результат:

Теперь давайте пропишем почтовый маршрут в server.js.

app.post('/mail', (req, res) => {
    const { firstname, lastname, email, msg } = req.body;

    const transporter = nodemailer.createTransport({
        service: 'gmail',
        auth: {
            user: process.env.EMAIL,
            pass: process.env.PASSWORD
        }
    })

    const mailOptions = {
        from: 'sender email',
        to: 'receiver email',
        subject: 'Postfolio',
        text: `First name: ${firstname}, \nLast name: ${lastname}, \nEmail: ${email}, \nMessage: ${msg}`
    }

    transporter.sendMail(mailOptions, (err, result) => {
        if (err){
            console.log(err);
            res.json('opps! it seems like some error occured plz. try again.')
        } else{
            res.json('thanks for e-mailing me. I will reply to you within 2 working days');
        }
    })
})

Таким образом отсылается почта при помощи nodemailer. Тут несть несколько вещей, на которые стоит обратить внимание.

1. process.env.EMAIL и process.env.PASSWORD — ключевые слова, дающие доступ к переменным окружения. Но мы пока ни одной переменной не создали. Поэтому создайте в своей корневой директории файл .env. Имя должно быть точно таким. Откройте его и наберите следующее:

EMAIL=your email
PASSWORD=your email's password

Теперь, как вы понимаете, process.env будет обращаться к этим переменным.

2. Параметры from и to. В коде, приведенном выше, я не вводил свои email-адреса. Но чтобы почта заработала, нужно указать email id в качестве параметров from и to. Идентификатор может быть одинаковым для обоих.

Итак, наш сервер полностью готов. Теперь сделаем контактную форму рабочей.

//contact form
const contactBtn = document.querySelector('.contact-btn');
const firstName = document.querySelector('.first-name');
const lastName = document.querySelector('.last-name');
const email = document.querySelector('.email');
const msg = document.querySelector('.message');

contactBtn.addEventListener('click', () => {
    if(firstName.value.length && lastName.value.length && email.value.length && msg.value.length){
        fetch('/mail', {
            method: 'post',
            headers: new Headers({'Content-Type': 'application/json'}),
            body: JSON.stringify({
                firstname: firstName.value,
                lastname: lastName.value,
                email: email.value,
                msg: msg.value,
            })
        })
        .then(res => res.json())
        .then(data => {
            alert(data);
        })
    }
})

В этом коде я просто выбираю все input и делаю запрос POST к роуту /mail.

Контактная форма тоже готова.

Пора сделать наш сайт отзывчивым.

Responsive — планшет

/* tablet view */

@media (max-width: 996px){
    html{
        font-size: 14px;
    }
    /* toggle btn */
    .toggle-btn{
        position: absolute;
        width: 40px;
        height: 40px;
        right: 10vw;
        cursor: pointer;
    }
    .toggle-btn span{
        position: absolute;
        width: 100%;
        height: 2px;
        background: #fff;
        top: 30%;
        transition: .5s;
    }
    .toggle-btn span:nth-child(2){
        top: 70%;
    }
    .toggle-btn.active span:nth-child(1){
        top: 50%;
        transform: rotate(45deg);
    }
    .toggle-btn.active span:nth-child(2){
        top: 50%;
        transform: rotate(-45deg);
    }
    /* links */
    .links-container{
        position: absolute;
        top: 60px;
        background: #1d1d1d;
        width: 100%;
        left: 0;
        padding: 0 10vw;
        flex-direction: column;
        transition: .5s;
        opacity: 0;
        pointer-events: none;
    }
    .links-container.show{
        opacity: 1;
        pointer-events: all;
    }
    .link{
        margin-left: auto;
        text-align: center;
        display: block;
        height: 50px;
    }
    /* home section */
    .home{
        flex-direction: column-reverse;
        height: fit-content;
        padding-bottom: 50px;
    }
    .home .image{
        width: 250px;
        margin: 40px;
    }
    .hero-content{
        width: 70%;
        min-width: 350px;
        text-align: center;
    }
    .hero-heading{
        font-size: 4.5rem;
    }

    /* about-section */
    .about-me-container{
        grid-template-columns: 1fr;
    }
    .left-col{
        margin: auto;
        width: 50%;
        min-width: 320px;
    }
    .skill-container, .project-container{
        grid-template-columns: repeat(2, 1fr);
    }
    .skill-card{
        grid-column: 1 span !important;
    }
}

Как видите, мы здесь стилизуем toggle-btn. Но функциональность надо прописать в app.js.

//toggle button
const toggleBtn = document.querySelector('.toggle-btn');
const linkContainer = document.querySelector('.links-container');

toggleBtn.addEventListener('click', () => {
    toggleBtn.classList.toggle('active');
    linkContainer.classList.toggle('show');
})

Responsive — мобильные устройства

/* mobile view */
@media (max-width: 500px){
    html{
        font-size: 12px;
    }
    p, .sub-heading, .about-para, .left-col::before, .tags{
        font-size: 1.4rem;
    }
    .about-image{
        width: 90%;
        margin: auto;
        display: block;
    }
    .skill-container, .project-container{
        grid-template-columns: 1fr;
    }
    .skill{
        font-size: 2.5rem;
    }
    .project-name{
        font-size: 3rem;
    }
    .name{
        flex-direction: column;
    }
    .name input{
        width: 100%;
    }
    .first-name{
        margin-bottom: 20px !important;
    }
}

Результат можно посмотреть в видеоруководстве.


Перевод статьи «How to make fully responsive modern portfolio using pure HTML, CSS and JS».

[customscript]techrocks_custom_after_post_html[/customscript]

[customscript]techrocks_custom_script[/customscript]

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх