如何使用纯 HTML、CSS 和 JS 制作完全响应的现代作品集。
视频教程
代码
您可能觉得有用的文章
大家好,今天我们将学习如何使用纯 HTML、CSS 和 JS 制作一个完全响应式的现代作品集,无需其他库。您将学习如何创建响应式设计,学习 CSS 伪元素。您将学习使用 Nodemailer 制作一个可用的联系表单,以及更多内容。
如果您想观看演示或需要带说明的编码教程,您可以观看下面的教程。
视频教程
如果您能订阅我的 YouTube 频道来支持我,我将不胜感激。
因此,不要浪费更多时间,让我们看看如何编写代码。
代码
在我们开始编写代码之前,让我们看看它的文件夹结构。
是的,它是一个 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 初始化
在根目录下的公共文件夹之外,打开命令提示符或终端并运行npm init
cmd。这会将 NPM 初始化到你的项目中。
现在运行此命令来安装这些库。
npm i express.js nodemon nodemailer dotenv
express.js
- 用于创建服务器nodemon
- 用于持续运行服务器nodemailer
- 用于发送邮件dotenv
- 用于设置环境变量。我们将使用它来在服务器外部存储我们的电子邮件 ID 和密码。
安装完库之后,让我们在中进行一些更改package.json
。打开它,并更改其scripts
数据。
"scripts": {
"start": "nodemon server.js"
},
然后server.js
在根目录(而不是公共文件夹)中创建文件。打开它。
服务器.js
首先导入库/包。
const express = require('express');
const path = require('path');
const nodemailer = require('nodemailer');
const dotenv = require('dotenv');
设置dotenv
以便我们可以访问环境变量。
dotenv.config();
然后将公共文件夹路径存储到变量并创建服务器。
let initialPath = path.join(__dirname, "public");
let app = express();
现在使用app.use
方法来设置中间件。
app.use(express.static(initialPath));
app.use(express.json());
它们都很重要,express.json
将启用表单数据共享并将express.static
公共文件夹设置为静态路径。
之后创建主路线。并发送index.html
文件。
app.get('/', (req, res) => {
res.sendFile(path.join(initialPath, "index.html"));
})
最后,让服务器监听3000
端口。
app.listen(3000, () => {
console.log('listening.....');
})
好了,我们的服务器现在完成了。让我们npm start
在终端上运行 cmd 来运行服务器。
现在让我们开始着手投资组合。
文件夹
打开index.html
并编写基本的 HTML 结构。然后链接style.css
至app.js
文件。之后创建导航栏。
<!-- 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;
}
笔记
- 您可以看到
scroll-behavior
已赋予html
。如果不赋予此项,则无法获得平滑滚动的效果。 seperator
您还可以在样式中看到元素,但不用担心,我们稍后会创建它。
输出
现在创建标题部分
<!-- 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 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;
}
输出
现在创建技能部分。在about
部分中添加此结构。
<section class="about" id="about-section">
//previous elements
<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
为不同的元素设置不同的 CSS 变量。这样我们就可以用不同的颜色添加相同的效果。
.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 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');
})
})
在上面的代码中,我们只是向过滤按钮添加了点击事件并切换了一些元素类。
好了,我们的项目部分已经全部完成了。现在,开始制作联系表格。
<!-- 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 发送邮件的方法。有一些事项需要注意。
process.env.EMAIL
&process.env.PASSWORD
这个关键字允许你访问环境变量,但我们还没有为此创建任何变量。在根目录中创建一个文件.env
。名称应该相同。打开它并输入以下内容。
EMAIL=your email
PASSWORD=your email's password
所以现在如果你明白了,process.env
就会访问这些变量。
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);
})
}
})
在上面的代码中,我只是选择所有表单输入并发出路由POST
请求/mail
。
我们的联系表格也已填写完毕。
现在让这个网站具有响应能力。
响应式 - 平板电脑
/* 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');
})
响应式 - 移动
/* 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;
}
}
您可以观看视频教程中的响应部分以了解输出
就这样吧。希望你理解了所有内容。如果你有疑问或者我遗漏了什么,请在评论区告诉我。
您可能觉得有用的文章
如果您能订阅我的YouTube频道,我将不胜感激。我创作了非常棒的网络内容。
感谢您的阅读。
文章来源:https://dev.to/themodernweb/how-to-make-complete-responsive-modern-portfolio-using-pure-html-css-and-js-1p65