11 dk, tahmini okuma süresi
Minimum reactjs ve typescript bilginizle bu projeyi takip edebileceksiniz. Mapbox ile fonksiyonel bir harita ekleyeceğiz ve intersection observer ile de farklı bazı animasyonları tetikleyceğiz. Bu web sitesini gelecekteki projeleriniz için şablon (template) olarak kullanabilir ve kişisel portföyünüze ekleyebilirsiniz. Hadi başlayalım!
Bitmiş web sitesini burada bulabilirsiniz.
Bu projeye ait kodu burada bulabilirsiniz. Kaybolduğunuzu düşünüyorsanız veya düzeltemediğiniz garip bir hata varsa, buraya bir göz atmaktan çekinmeyin.
Bu web sitesi, bir şirket için bir açılış sayfası (landing page) tarzı bir projedir. Bu projede bir pazarlama şirketi örneği üzerinden gitmek istedim. Ancak, bu web sitesini bazı küçük değişikliklerle diğer birçok şirkete uydurabileceksiniz. Müşterilerin (veya potansiyel müşterilerin) bizimle iletişime geçmesi için bazı formlar eklediğimizi görebilirsiniz, ancak bu proje için arka ucumuz (backend) olmadığı için yalnızca bu bilgileri kosnola aktaracağız (log).
Ziyaretçilere yaptığımız projeleri, ortaklarımızı, son çalışmalarımızı, bloglarımızı ve son olarak ortaklarımızı göstereceğiz. Kullanıcıların bizi sosyal medyada takip etmelerine ve haftalık bültenimize abone olmalarına kolaylık sağlayacağız.
React projelerimizi typescript eklenmiş haliyle kolayca başlatmaz için şu komutu kullanabiliriz:
npx create-react-app corporate-react-ts --template typescript
Kurulum bittiğinde genellikle App.tsx dosyasında gereksiz olan her şeyi silerim. App.tsx'imizin içindeki tüm başlangıç kodunu silin ve projemiz için gerekli olan kodu yazmaya başlayın. Styled components kullanacağız, bu şekilde bileşen (component) için gerekli css'i bileşen içinde kolayca tutabiliriz ve css dosyaları oluşturmamıza gerek kalmaz. App.tsx'imiz şimdi şöyle görünecek:
import React from 'react';
import styled from 'styled-components';
function App() {
return (
<AppWrapper>
App file
</AppWrapper>
);
}
export default App;
const AppWrapper = styled.div`
min-height: 100vh;
display: flex
width: 100vw;
`;
Styled components kullanarak global bir css dosyası oluşturabiliriz ama bu projede bunun gerekli olacağını düşünmedim, bu yüzden hala index.css dosyasını ve bazı temel değişikliklerle tutuyoruz. Ayrıca web sitemize uyacağını düşündüğüm bazı renkleri de ekleyeceğiz. Projelerimde tüm renkleri kullanmamama rağmen, projemde birlikte iyi giden renkleri gelecekteki iyileştirme ve eklemeler için bulundurmayı seviyorum. index.css için bu noktaya kadar gerekli olan kodun tamamını burada bulabilirsiniz:
html {
scroll-behavior: smooth;
}
* {
padding:0;
margin:0;
box-sizing: border-box;
}
:root {
--white: rgb(255, 255, 255);
--black: rgb(0,0,0);
--gray: rgb(69, 69, 69);
--gray-2: rgb(149, 149, 149);
--yellow: #ffbe0b;
--pink-lavender: #cdb4dbff;
--orchid-pink: #ffc8ddff;
--nadeshiko-pink: #ffafccff;
--uranian-blue: #bde0feff;
--darker-blue: rgb(63, 108, 151);
--baby-blue-eyes: #a2d2ffff;
--top-margin: 100px;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
Projemizin olabildiğince gerçekçi görünmesi ve hissettirmesi için bazı görseller kullanacağız. Elbette kendi görsellerinizi kullanabilir veya ücretsiz görseller sunan birçok web sitesinden birini ziyaret edebilir ve onların görsellerinden bazılarını kullanabilirsiniz. Bu projede kullanılan görsellerin çoğunu unsplash, pexels vb. web sitelerinden topladım. Daha hafif olmaları için görsellerimin biçimini (style) yeniden boyutlandırdım ve değiştirdim. Bu da önerilir ve çevrimiçi olarak ücretsiz olarak halledilebilir. Görsellerinizi src klasörüne kaydetmek için assets adlı bir klasör oluşturun. Görsellerinizi buraya kaydedin.
Tek bir sayfa oluşturduğumuz için bu proje için yönlendirme (routing) ile uğraşmamıza gerek yok. Projelerimi yukarıdan aşağıya olacak şekilde oluşturmayı seviyorum. Bu yüzden öncelikle web sitemiz için gezinme çubuğunu (navbar), ardından bölümleri (sections) ve son olarak da altbilgiyi (footer) oluşturmamız gerekiyor.
CSS libraries such as tailwindcss and bootstrap offer solutions for spacing and padding in different screen sizes. Not having to do it manually is a time saver but we are using styled components in this particular project. So we need to create a component to wrap other components that we want to have the same padding. As we will be using this component in other components it's a better idea to create this component inside the components folder.
Tailwindcss ve bootstrap gibi CSS kitaplıkları (libraries), farklı ekran boyutlarında boşluk (margin) ve dolgu (padding) için çözümler sunar. Bunu manuel olarak yapmak zorunda olmamak zaman kazandırıyor ancak bu projede styled components kullanıyoruz. Bu nedenle, aynı dolguya sahip olmasını istediğimiz diğer bileşenleri sarmak için bir bileşen oluşturmamız gerekiyor. Bu bileşeni diğer bileşenlerde kullanacağımız için bu bileşeni components klasörü içinde oluşturmak daha iyi bir fikir.
components klasörü içinde ContainerComponent.tsx adlı bir dosya oluşturun. Bu bileşen, flex'in görüntülenme değerini belirleyecek ve yönüne, üzerinde ayarladığımız pervane ile karar verilecektir. Bu bileşen, display değeri olarak flex alacak ve yönüne, üzerinde ayarladığımız prop ile karar verilecek. Padding değeri gayet basit olacak, ekran küçüldükçe padding de küçülecek. Bir max-width değeri belirleyerek çok büyük ekranlarda da görüntünün istediğim gibi kalmasını sağlayacağım. Bu benim tamamen kişisel tercihim tabi.
import React, { ReactNode } from 'react'
import styled from 'styled-components'
interface PropType {
children: ReactNode
column: boolean
}
const ContainerComponent = (props:PropType ) => {
return (
<Container column={props.column}>
{props.children}
</Container>
)
}
export default ContainerComponent;
const Container = styled.div<PropType>`
display:flex;
flex-direction: ${props => props.column ? "column": "row"};
width: 100%;
padding-left: 10px;
padding-right: 10px;
max-width: 1920px;
@media (max-width: 1920px) {
padding-left: 80px;
padding-right: 80px;
}
@media (max-width: 1440px) {
padding-left: 60px;
padding-right: 60px;
}
@media (max-width: 1200px) {
padding-left: 50px;
padding-right: 50px;
}
@media (max-width: 960px) {
padding-left: 40px;
padding-right: 40px;
}
@media (max-width: 600px) {
padding-left: 30px;
padding-right: 30px;
}
@media (max-width: 480px) {
padding-left: 20px;
padding-right: 20px;
}
`
Artık içeriğimiz ekran boyutu ne olursa olsun hizalı görünecek.
Şimdi src klasörünün içinde components adlı bir klasör oluşturun. Bileşenlerimizi burada tutacağız. Bu klasörün içinde bir navbar klasörü oluşturun. Gezinme çubuğunda şirketimizin logosuna ihtiyacımız olacak. Bu logoyu ayrıca bizi sayfanın en üstüne taşıyacak olan bir ana sayfa düğmesi olarak kullanacağız. Ücretsiz olarak logo tasarlayabileceğiniz bir sürü web sitesi var veya isterseniz sadece bir resim de kullanabilirsiniz. logomu bir bileşene dönüştürdüm ve bu, navbar klasörü içinde oluşturduğum basit Logo.tsx bileşeni:
import React from 'react'
import styled from 'styled-components';
import logo from '../../assets/marketing-co.png';
const Logo = () => {
return (
<a href='/#'>
<Img src={logo} alt="brand logo" />
</a>
)
}
export default Logo
const Img = styled.img`
border-radius: 50%;
margin: 5px;
width: 94px;
`
Ayrıca web sayfamızın farklı bölümleri için bağlantılara ihtiyacımız olacak. Kodumu olabildiğince temiz tutmak için bu bağlantıları bileşenlere de dönüştüreceğiz. Bu bileşene NavbarLink.tsx adını vereceğiz. Bu bileşenler prop olarak bir string değeri alacak, böylece ziyaretçilerimizi web sayfamızda gitmek istedikleri yere gönderebileceğiz. Ayrıca boşlukları elbette "-" ile değiştirmemiz gerekiyor. Bunun dışında burada görebileceğiniz gibi sadece basit bir bileşen:
import React from 'react'
import styled from 'styled-components'
interface PropType {
to: string;
}
const NavbarLink = ({ to }: PropType) => {
const link = to.replace(' ', '-')
return (
<A href={`#${link}`}>{to}</A>
)
}
export default NavbarLink
const A = styled.a`
text-decoration: none;
color: var(--baby-blue-eyes);
font-size: 1.6rem;
margin: 10px;
text-transform: uppercase;
transition: 0.4s;
:hover {
color: var(--nadeshiko-pink);
}
`
Artık daha büyük ekranlar için temel gezinme çubuğu yapıldığına göre, daha küçük ekran boyutları düşünebiliriz. Projelerimin çoğunda önerilenin aksine önce masaüstünü düşünüyorum, masaüstü görüntüsünü taslarmakla başlıyorum. Bir alışkanlık diyelim. Her neyse, ekranın üst kısmındaki büyük bir gezinme çubuğu yerine bir kenar çubuğuna ihtiyacımız var ve gezinme için ve bu kenar çubuğunu açıp kapatmak için bir hamburger butonuna ihtiyacımız var.
Bu bileşenlerin birlikte çalıştığını gördükten sonra bir açıklamanın daha iyi sonuç vereceğini düşünüyorum. Şimdi navbar klasörü içinde HamburgerComponent.tsx adında bir bileşen oluşturalım. Bu bileşen oluşturacağımız yan çubuğu açıp kapayacak. Bu bileşende kullanılan kod burada:
import React, { useState } from 'react'
import styled from 'styled-components'
import RightNav from './RightNav'
const Hamburger = () => {
const [open, setOpen] = useState<boolean>(false)
return (<>
<StyledHamburger open={open} onClick={() => {setOpen(!open)}}>
<div></div>
<div></div>
<div></div>
</StyledHamburger>
<RightNav open={open} />
</>)
}
export default Hamburger;
const StyledHamburger = styled.div<{ open: boolean }>`
position: ${({ open }) => open ? "fixed" : 'absolute'};
top: 20px;
right: 50px;
width: 40px;
z-index: 20;
height: 40px;
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
&:hover {
cursor: pointer;
}
@media screen and (min-width: 1201px) {
display: none;
}
div {
width: 40px;
height: 0.25rem;
background-color: ${({ open }) => open ? "var(--nadeshiko-pink)" : 'var(--baby-blue-eyes)'};
border-radius: 10px;
transform-origin: 1px;
transition: all 0.3s ease-in-out;
&:nth-child(1) {
transform: ${({ open }) => open ? 'rotate(45deg)' : 'rotate(0)'};
}
&:nth-child(2) {
transform: ${({ open }) => open ? 'translateX(100%)' : 'translateX(0)'};
opacity: ${({ open }) => open ? 0 : 1};
}
&:nth-child(3) {
transform: ${({ open }) => open ? 'rotate(-45deg)' : 'rotate(0)'};
}
}
`
Gördüğünüz gibi, bu bileşen yalnızca ekran boyutu 1200 pikselden küçük olduğunda görünür olacaktır. Ayrıca ayarladığımız boolean değerini RightNav bileşenimize de gönderiyoruz. Artık navbar klasörü içinde RightNav.tsx dosyamızı oluşturabiliriz. RightBarWrapper için type (tip) değerini aşağıdaki gibi belirledim. Tipleri bu şekilde de ayrı ayrı tanımlayabileceğimizi göstermek için bu şekilde tutmak istedim. Kod burada gördüğünüz gibi yine çok basit:
import React from 'react'
import styled from 'styled-components'
import RightNavbarLink from './RightNavbarLink';
interface propType {
open:boolean
}
const RightNav = ( props: propType) => {
return (
<RightBarWrapper open={props.open}>
<LinkContainer>
<RightNavbarLink to="about" />
<RightNavbarLink to="we offer" />
<RightNavbarLink to="portfolio" />
<RightNavbarLink to="recent work" />
<RightNavbarLink to="contact" />
</LinkContainer>
</RightBarWrapper>
)
}
export default RightNav
const RightBarWrapper = styled.div<{open:boolean}>`
position: fixed;
display: flex;
border-left: 4px solid var(--baby-blue-eyes);
justify-items: center;
transition: all 0.3s ease-in-out;
background: var(--white);
transform: ${({ open }) => open ? 'translateX(0)' : 'translateX(100%)'};
top: 0;
right: 0;
height: 100vh;
width: 250px;
z-index:10;
@media screen and (min-width: 1201px) {
display: none;
}
`
const LinkContainer = styled.div`
margin-top: 100px;
display: flex;
flex-direction: column;
width: 100%;
margin-left: 30px;
`
Burada henüz sahip olmadığımız RightNavbarLink adlı bir bileşenimiz var. Bu, bağlantılarımız için oluşturduğumuz bileşene çok benzeyecektir. navbar klasöründe RightNavbarLink.tsx dosyası oluşturun. Burada bu bileşenin tam kodunu görebilirsiniz:
import React from 'react'
import styled from 'styled-components';
interface propType {
to: string;
}
const RightNavbarLink = ({ to }: propType) => {
const link = to.replace(' ', '-')
return (
<A href={`#${link}`}>{to}</A>
)
}
export default RightNavbarLink
const A = styled.a`
transition: 0.2s;
color: var(--baby-blue-eyes);
text-decoration: none;
text-transform: uppercase;
transform: 1s;
margin-bottom: 20px;
font-size: 1.4rem;
:hover {
color: var(--nadeshiko-pink);
}
`
Navigasyon çubuğumuz için gerekli tüm parçalara sahip olduğumuza göre artık bu çubuğu oluşturabiliriz. navbar klasörünün içinde her şey toparlayabimemiz için Navbar.tsx dosyasını oluşturmamız gerekiyor. Banner bileşeni görünür olmadığında gezinme çubuğumuz arka plan rengini değiştirecektir. intersection observer yardımıyla bunu kolayca halledebileceğiz.
Bu bileşenin yükseklik değerini seçtiğimiz bir değere ayarlayacağız. Bu değeri bir değişken olarak tutmak partik bir uygulama. Bu sayede yükseklik (height) değerini değiştirmemiz gerekirse sayfa düzenimiz bozulmaz ve sayfamız garip görünmez. Gezinme çubuğumuz "position:fixed" olacak, bu şekilde kolay gezinme için sayfada nerede olursak olalım onu görebileceğiz. Ayrıca "z-index:100"'ün ayarlanması, bileşenimizin diğerlerinin üstünde görünmesini sağlayacaktır.
Bu dosyanın kodu şöyle görünmelidir:
import React from 'react'
import styled from 'styled-components'
import ContainerComponent from '../ContainerComponent'
import Hamburger from './HamburgerComponent'
import Logo from './Logo'
import NavbarLink from './NavbarLink'
const Navbar = (props: { bgColor: string }) => {
return (
<Nav bgColor={props.bgColor} id="navbar">
<ContainerComponent column={false}>
<Logo />
<RightAlign>
<NavbarLink to="about" />
<NavbarLink to="we offer" />
<NavbarLink to="portfolio" />
<NavbarLink to="recent work" />
<NavbarLink to="contact" />
</RightAlign>
<Hamburger />
</ContainerComponent>
</Nav>
)
}
export default Navbar
const Nav = styled.nav<{ bgColor: string }>`
background: ${props => props.bgColor};
height: var(--top-margin);
position: fixed;
transition: .4s;
transition-delay: 1s;
width: 100%;
max-width: 100vw;
display: flex;
flex-direction: row;
z-index: 100;
`
const RightAlign = styled.div`
margin-left: auto;
display: flex;
flex-direction: row;
align-items: center;
@media (max-width: 1200px) {
display: none;
}
`
Banner, açılış sayfaları için çok önemlidir. Sloganınız ile temiz, derli toplu bir afişe ihtiyacınız var. Bu proje bir şablon olduğu için basit bir slogan ve bazı lorem ipsum metni ile bunu taklit edebiliriz. Arka planda bir görsel kullanmak performans açısından size pahalıya mal olabilir ancak pazarlama açısından faydalı da olabilir. Bu proje için, tabii ki yeniden boyutlandırıp formatı webp olarak değiştirdikten sonra resimlerimi kullandım.
Bu bileşendeki görece zor kısım intersection observer diyebiliriz. Neyse ki bu amaç için kullanabileceğimiz bir npm paketi var. react-intersection-observer paketini aşağıdaki komutla kurun:
npm i react-intersection-observer
Ayrıca ziyaretçilerin bir sonraki bölüme kolayca geçebilmeleri için bir düğmeye ihtiyacımız olacak. Bu düğmeyi web sitemizin diğer bölümlerinde kullanabiliriz, bu nedenle diğer bölümlerde de kullanabileceğimiz bir bileşen oluşturmak daha iyi olacaktır. Bu şekilde, web sitelerimizde tutarlı bir stil tutabileceğiz. components klasörümüzde Button.tsx bileşenini oluşturalım. Düğme elbette bir text (metin) prop almalıdır, ancak bu noktada henüz "onClick" işlevi eklemek isteyip istemediğimizi bilmiyoruz. Yine de bu fonksiyonu açık uçlu bırakıyoruz. Bu bileşenin kodu:
import React from 'react'
import styled from 'styled-components';
interface propType {
text: string,
onClick?: Function,
}
const Button = (prop: propType) => {
return (
<StyledButton>
{prop.text}
</StyledButton>
)
}
export default Button
const StyledButton = styled.button`
padding: 14px 20px;
font-size: 1.4rem;
border-radius: 30px;
border: 2px solid white;
color: var(--white);
background: linear-gradient(25deg, var(--uranian-blue), var(--nadeshiko-pink));
text-transform: capitalize;
transition: .4s;
:hover {
border: 2px solid black;
background: inherit;
cursor: pointer;
color: var(--black);
}
`
Banner bileşenindeki her şeyi merkezde bir sütun (column) olarak tutmak istiyoruz. Container bileşenimizi zaten oluşturduğumuz için, sütun değerini "true" olarak iletebiliriz. App.tsx buradaki state (durumu) oluşturacağımız ana bileşen olacaktır. Banner görüntülendiğinde değeri true olacaktır. Bu boolean değeri ile bgColor değerleri arasında geçiş yapacağız ve onu gezinme çubuğuna ileteceğiz. Bu şekilde, banner görüntülenmediğinde gezinme çubuğumuz, seçtiğimiz bir arka plan rengini alacak. App.tsx dosyası bu noktaya kadar şöyle görünecek:
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import Banner from './components/Banner';
import Navbar from './components/navbar/Navbar';
function App() {
const [ onScreen, setOnScreen ] = useState();
const [ bgColor, setBGColor ] = useState("inherit");
useEffect(() => {
if(!onScreen) setBGColor("var(--white)");
else if(onScreen) setBGColor("inherit");
}, [onScreen])
return (
<AppWrapper>
<Navbar bgColor={bgColor}/>
<Banner setOnScreen={setOnScreen}/>
</AppWrapper>
);
}
export default App;
const AppWrapper = styled.div`
min-height: 100vh;
display: flex
width: 100vw;
`;
Şimdi Banner.tsx'i components'in içinde aşağıdaki kod ile oluşturalım:
import React, { useEffect } from 'react'
import styled from 'styled-components'
import bgImg from '../assets/office-designers-smallerx3.webp';
import Button from './Button';
import ContainerComponent from './ContainerComponent';
import { useInView } from 'react-intersection-observer';
const Banner = (props: any) => {
const { ref, inView } = useInView({
rootMargin: "0px",
threshold: 1,
});
useEffect(() => {
if(inView) props.setOnScreen(true);
else if(!inView) props.setOnScreen(false);
}, [inView, props])
return (
<BannerWrapper ref={ref}>
<ContainerComponent column={true}>
<BannerContent>
<H1>YOU BRAND WILL GROW HERE</H1>
<H2>Lorem ipsum, dolor sit</H2>
<P>Lorem ipsum dolor sit amet consectetur adipisicing elit. Rem mollitia voluptatum minus deserunt delectus ipsum cupiditate. Unde, repudiandae molestiae? Mollitia temporibus unde beatae. Odio quod, accusamus dicta fugiat molestiae recusandae?</P>
<a href="#about"><Button text={"discover"}/></a>
</BannerContent>
</ContainerComponent>
</BannerWrapper>
)
}
export default Banner
const BannerWrapper = styled.section`
background: linear-gradient( rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.7) ), url(${bgImg});
background-position: bottom;
background-repeat: no-repeat;
background-size: cover;
min-height: 100vh;
width: 100%;
padding-top: var(--top-margin);
margin-top: 0;
`
const BannerContent = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: white;
margin-top: var(--top-margin);
`
const H1 = styled.h1`
font-size: 1.4rem;
text-transform: uppercase;
@media (max-width: 1440px) {
font-size: 1.2rem;
}
@media (max-width: 1200px) {
font-size: 1.1rem;
}
`
const H2 = styled.h2`
font-size: 4rem;
margin-bottom: 16px;
text-transform: uppercase;
color: var(--baby-blue-eyes);
@media (max-width: 1440px) {
font-size: 3.6rem;
}
@media (max-width: 1200px) {
font-size: 3rem;
}
@media (max-width: 720px) {
text-align: center;
font-size: 2.6rem;
}
@media (max-width: 480px) {
font-size: 2rem;
}
`
const P = styled.p`
text-align: center;
max-width: 500px;
font-size: 1.2rem;
margin-bottom: 100px;
margin-top: 20px;
line-height: 1.7;
@media (max-width: 960px) {
font-size: 1.1rem;
line-height: 1.6;
}
@media (max-width: 480px) {
font-size: 1rem;
line-height: 1.3;
}
`
Elbette farklı ekran boyutları için ufak değişiklikler var ama bunun dışında gördüğünüz gibi özel bir şey yok.
Bu bölüm de gayet basit olacak. Bannerdan daha temiz olmalı, ziyaretçilerin gözlerini yormak istemeyiz. Bu bölüm için, yanında bir resim ve diğer tarafta içerik bulunan bir grid ekleyeceğiz. İçerik, şirketimizin başlangıç hikayesi, amacı ve belki de bu noktaya kadar olan yolculuğunuzun bir özeti hakkında olmalıdır. About.tsx dosyasının tam kodu burada:
import React from 'react'
import styled from 'styled-components'
import cactusImg from '../assets/yellow-cactus-white-bg-smaller.webp';
const About = () => {
return (
<AboutWrapper id="about">
<Img />
<TextBox>
<TextBoxTitle>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Modi molestias
</TextBoxTitle>
<TextBoxTitleText>
Odio labore sequi consequatur ipsa cupiditate pariatur ducimus itaque consequuntur nulla nostrum!cupiditate pariatur ducimus itaque consequuntur nulla nostrum!
</TextBoxTitleText>
<div>
<InfoBoxTitle>
Some Title
</InfoBoxTitle>
<InfoText>Lorem ipsum dolor sit amet consectetur adipisicing elit. Praesentium hic ipsa numquam quos ullam quo eum? Sit a dicta assumenda est doloremque unde eius consequuntur sint vero, dolorem non illo?</InfoText>
</div>
<div>
<InfoBoxTitle>
Another Title
</InfoBoxTitle>
<InfoText>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Quae neque cupiditate consequuntur perferendis laboriosam mollitia vitae ipsum expedita. </InfoText>
</div>
</TextBox>
</AboutWrapper>
)
}
export default About
const AboutWrapper = styled.section`
display:grid;
grid-template-columns: 1fr 1fr;
@media (max-width: 1200px) {
grid-template-columns: 1fr;
}
`
const Img = styled.div`
background-image: url(${cactusImg});
background-position: bottom;
background-repeat: no-repeat;
background-size: cover;
width: 100%;
padding-top: var(--top-margin);
padding-bottom: var(--top-margin);
@media (max-width: 1200px) {
height: 500px;
}
@media (max-width: 960px) {
height: 400px;
}
@media (max-width: 720px) {
height: 300px;
}
`
const TextBox = styled.div`
padding: 100px 140px 140px 20px;
background: #ebebf5;
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 30px;
@media (max-width: 1200px) {
padding: 120px;
}
@media (max-width: 960px) {
padding: 60px;
}
@media (max-width: 480px) {
display: flex;
flex-direction: column;
}
`
const TextBoxTitle = styled.h2`
grid-column: span 2;
margin-bottom: 20px;
color: var(--darker-blue);
font-size: 1.8rem;
`
const InfoBoxTitle = styled.h3`
font-size: 1.6rem;
margin-bottom: 14px;
`
const InfoText = styled.p`
font-size: 1.1rem;
line-height: 1.2;
`
const TextBoxTitleText = styled.p`
grid-column: span 2;
font-size: 1.1rem;
line-height: 1.2;
`
Responsive (ekran boyutuna duyarlı) tasarım, front-end web geliştirmenin önemli bir parçası. En popüler ekran boyutlarını seçmek yerine, tasarımımı farklı ekran boyutlarında kontrol etmeyi seviyorum. Bir şekilde bozulduğunu veya kötü görünmeye başladığını görürsem, o ekran boyutundaki tasarımla oynarım. Sadece küçük bir ipucu.
Şimdi bu dosyayı banner bileşeninin altıa, App.tsx bileşeninin içine alın.
Bu bölümde potansiyel müşterilerimize neler sunabileceğimizi göstereceğiz. Ne kadar hızlı olduğumuzu, geliştiricilerimizin/mühendislerimizin ne kadar iyi olduğunu vb. göstereceğiz. Oluşturduğum bu bileşen iki ana bölümden oluşmaktadır. Bu kısmın ana bölümünde, kullanıcılar üzerlerine geldiğinde daha fazla bilgi veren kartları görecekler.
Bileşenimizi temiz ve okunması kolay tutmak için gerekli verileri başka bir klasörde oluşturdum. src klasöründe data adlı bir klasör oluşturun. Benzer davranışı içeren başka bölümler de var, bu yüzden burada bu klasörde farklı daha fazla veri (data) dosyası oluşturacağız. data klasöründe offerData.ts adlı bir dosya oluşturun. Bu dosyanın içinde bazı ikonlarımız olacak. Fontawesome'ı yüklemek ve gerekli simgeleri edinmek için devam edin ve bu komutu girin:
npm i @fortawesome/fontawesome-svg-core @fortawesome/free-brands-svg-icons @fortawesome/free-solid-svg-icons @fortawesome/react-fontawesome
Artık simgelerimizi kullanmaya başlayabiliriz. Simgelerin eklenmesiyle offerData.ts dosyamız şu şekilde görünecek:
import { faRocket, faCode, faRobot, faGift, IconDefinition } from '@fortawesome/free-solid-svg-icons';
interface OfferData {
title: string
text: string
detail: string
icon: IconDefinition
};
const data:OfferData[] = [
{
title: "We Are Fast",
icon: faRocket,
detail: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusantium eligendi assumenda deserunt ullam perspiciatis modi libero inventore earum voluptas iure?",
text: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere ab deserunt iste veritatis officia"
},
{
title: "Expert Developers",
icon: faCode,
detail: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusantium eligendi assumenda deserunt ullam perspiciatis modi libero inventore earum voluptas iure?",
text: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere ab deserunt iste veritatis officia"
},
{
title: "Lots of Promotions",
icon: faGift,
detail: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusantium eligendi assumenda deserunt ullam perspiciatis modi libero inventore earum voluptas iure?",
text: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere ab deserunt iste veritatis officia"
},
{
title: "Automize Your Growth",
icon: faRobot,
detail: "Lorem, ipsum dolor sit amet consectetur adipisicing elit. Accusantium eligendi assumenda deserunt ullam perspiciatis modi libero inventore earum voluptas iure?",
text: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Facere ab deserunt iste veritatis officia"
},
]
export default data
Bu verileri Offer.tsx dosyamıza aktaracağız ve "map()" yöntemi yardımıyla her nesne için bir grid öğesi oluşturacağız. Offer.tsx dosyasının bitmiş kodu, henüz oluşturmadığımız OfferBottom.tsx bileşenine sahip olacak. Bu nedenle, bir hata görürseniz, oluşturulana kadar görmezden gelin.
Şimdi components içinde offer adında bir klasör oluşturun. Burada 2 bileşenimizi, Offer.tsx ve OfferBottom.tsx dosyalarını oluşturacağız. Offer.tsx dosyası için kod burada:
import React from 'react'
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import OfferBottom from './OfferBottom';
import data from '../../data/offerData';
const Offer = () => {
return (
<>
<OfferWrapper id='we-offer'>
<Content>
<OfferHeader>
<OfferTitle>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</OfferTitle>
<OfferSubTitle>Corporis modi optio, dolorum fugiat magni</OfferSubTitle>
</OfferHeader>
{data.map(d => <OfferBox key={d.title}>
<DetailInfo>
<DetailText>{d.detail}</DetailText>
</DetailInfo>
<OfferInfo>
<IconSpan>
<FontAwesomeIcon icon={d.icon}></FontAwesomeIcon>
</IconSpan>
<BoxTitle>{d.title}</BoxTitle>
<BoxText>{d.text}</BoxText>
</OfferInfo>
</OfferBox>)}
</Content>
</OfferWrapper>
<OfferBottom />
</>
)
}
export default Offer
const OfferWrapper = styled.section`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100px;
@media (max-width: 1200px) {
padding:60px;
}
@media (max-width: 960px) {
padding: 40px;
}
@media (max-width: 720px) {
padding:20px;
}
`
const Content = styled.div`
display: grid;
grid-template-columns: repeat(4 ,1fr );
grid-gap: 30px;
@media (max-width: 1200px) {
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
}
@media (max-width: 720px) {
grid-template-columns: 1fr;
grid-gap: 6px;
}
`
const OfferHeader = styled.div`
grid-column: 1/-1;
text-align: center;
margin-bottom: 60px;
`
const OfferTitle = styled.h2`
font-size: 1.8rem;
margin-top: 50px;
`
const OfferSubTitle = styled.p`
margin-top:20px;
`
const DetailInfo = styled.div`
width: 100%;
height: 0%;
transition: 1s;
opacity: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
`
const DetailText = styled.p`
margin-bottom: 20px;
line-height: 1.7;
text-align: center;
`
const OfferInfo = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
`
const OfferBox = styled.div`
transition : 0.4s;
cursor: pointer;
border-radius: 10px;
:hover{
box-shadow:0 0 30px var(--nadeshiko-pink);
${DetailInfo} {
height: 100%;
opacity: 1;
}
${OfferInfo} {
transform: scale(0);
}
}
padding: 40px 20px;
@media (max-width: 1200px) {
padding: 30px 16px;
}
@media (max-width: 720px) {
padding: 12px 10;
}
`
const IconSpan = styled.span`
font-size: 2.4rem;
color: var(--baby-blue-eyes);
`
const BoxTitle = styled.h3`
margin-bottom: 26px;
width: 100%;
`
const BoxText = styled.p`
line-height: 1.6;
font-size: 1.07rem;
`
Ziyaretçi kartın üzerine geldiğinde arkasındaki metin içeriğini görecek ve harika bir solma (fade out) / slayt (slide) animasyonu olacak. Bu animasyondan pek oşlanmadıysanız, değiştirmekten çekinmeyin, bu sizin projeniz.
Bu bölümün alt kısımda başka bir bileşen olarak oluşturulacak ve bazı bilgiler içeren 2 resim olacak. Yine grid kullanacağız çünkü sayfa düzenlerini grid kullanarak oluşturmayı çok daha kolay buluyorum. OfferBottom.tsx dosyasının kodu aşağıdaki gibi:
import React from 'react'
import styled from 'styled-components'
import call from '../../assets/callcenter-employees-smaller.webp';
import meeting from '../../assets/office-people-smaller.webp';
const OfferBottom = () => {
return (
<Wrapper>
<Img src={call} alt="Call center team" />
<InfoBox>
<AboveTitle>
CONTACT US
</AboveTitle>
<InfoTitle>
WE ARE READY TO HELP 24/7
</InfoTitle>
<InfoText>
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Nesciunt quis excepturi quia magnam dolores laudantium reiciendis eveniet, ab accusamus sequi?
</InfoText>
</InfoBox>
<Img src={meeting} alt="a typical team meeting" />
<InfoBox>
<AboveTitle>
SIMPLE SOLUTIONS FOR COMPLEX PROBLEMS
</AboveTitle>
<InfoTitle>
NO MATTER HOW DIFFICULT THE TASK
</InfoTitle>
<InfoText>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Possimus voluptates sed culpa ratione sapiente mollitia.
</InfoText>
</InfoBox>
</Wrapper>
)
}
export default OfferBottom
const Wrapper = styled.div`
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 10px;
background: var(--black);
@media (max-width: 1200px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 720px) {
grid-template-columns: 1fr;
}
`
const Img = styled.img`
height:100%;
width:100%;
object-fit: cover;
`
const InfoBox = styled.div`
padding: 20px 40px;
`
const InfoTitle = styled.h3`
color: var(--baby-blue-eyes);
margin-bottom: 20px;
font-weight: bold;
font-size: 2rem;
`
const InfoText = styled.p`
font-size: 1.2rem;
line-height: 1.6;
color: var(--gray-2);
`
const AboveTitle = styled.p`
color: var(--gray-2);
margin-top: 10px;
`
Ufak bir hatırlatma, şimdiye kadar herhangi bir sorun yaşadıysanız burepository'i kontrol edebilirsiniz.
Şimdi App.tsx içindeki Offer.tsx dosyasını içe aktarın ve onu ilgili bileşeninin altına yerleştirin.
Portföy bölümümüzü teklif (offer) bölümünün altına yerleştireceğiz. İlerledikçe web sitemizin daha da karmaşıklığını göreceksiniz. Şimdi devam edin ve components klasörünün içinde bir portfolio klasörü oluşturun. Portföy bölümümüzde iki ana bölüm olacak. İlkinde ziyaretçilerimize, çözüm ürettiğimiz firmaları göstereceğiz.
Çalıştığımız her şirket bir resimle temsil edilecek, bunlar şirketlerin resimleri veya ilgili bir görsel olmalı. Aslında daha önce çalıştığımız herhangi bir şirket olmadığı için herhangi bir görseli burada kullanabiliriz. Bu görselleri bir grid düzenine yerleştireceğiz. Ziyaretçiler bir resmin üzerine geldiğinde, resmi kaplayan görünmez element (opacity:0) yavaş yavaş görünür hale gelecek ve üzerine tıklayarak şirketin web sitesi ziyaret edebileceğiz. Tabii ki bizim durumumuzda tüm linkler sadece bannera yönlendirecek.
Bu bileşeni oluşturmak için elbette bazı verilere ihtiyacımız olacak. Şimdi data klasörünün içinde portfolioData.ts adlı bir dosya oluşturun. Oluşturduğumuz dosya şunun gibi bir şey olacak:
import mechanic from '../assets/black-white-mechanic-smaller.webp';
import view from '../assets/blue-red-view-smaller.webp';
import flower from '../assets/flower-blue-smaller.webp';
import lines from '../assets/lines-orange-white-smaller.webp';
import neuron from '../assets/neuron-smaller.webp';
import tree from '../assets/tree-green.webp';
import map from '../assets/map-pins-smaller.webp';
interface PortfolioData {
img: string
alt: string
cols: string
link: string
};
const data:PortfolioData[] = [
{img: mechanic, alt: "some cool company", cols: "4", link: "#"},
{img: view, alt: "some cool company", cols: "4", link: "#"},
{img: map, alt: "some cool company", cols: "4", link: "#"},
{img: lines, alt: "some cool company", cols: "5", link: "#"},
{img: flower, alt: "some cool company", cols: "7", link: "#"},
{img: neuron, alt: "some cool company", cols: "6", link: "#"},
{img: tree, alt: "some cool company", cols: "6", link: "#"}
]
export default data
"cols" değerleri, grid içinde elementlerin ne kadar yer kaplayacaklarını belirler. Bu nesnelerin oluşturulma sırası hakkında endişelenmemize gerek yok çünkü "grid-auto-flow: row dense;" bunları nesnelerei sıkıştıracak. Bu şekilde grid, bizim için boş alanları dolduracak. İşimizi çok kolaylaştırıyor.
Bu verileri portfolio klasöründe bulunan Portfolio.tsx dosyamıza aktaracağız. Bileşeni temiz tutmak için verileri buraya kabul aktaracak, kartlar olarak işleyecek ve başka bir bileşen oluşturacağız.
PortfolioItem.tsx dosyasını oluşturacağız, bu bileşen verileri prop olarak alacak. Her nesne (object) map() metodu ile bir kart oluşturulması için kullanılacak.
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faPaperPlane } from '@fortawesome/free-solid-svg-icons';
import styled from 'styled-components';
interface propTypes {
imgSrc: string
alt: string
colSpan: string
link: string
}
interface styleProp {
colSpan: string
}
const PortfolioItem = (props:propTypes) => {
return (
<Wrapper colSpan={props.colSpan}>
<A href={props.link}>
<FontAwesomeIcon icon={faPaperPlane}></FontAwesomeIcon>
</A>
<Img loading='lazy' src={props.imgSrc} alt={props.alt} />
</Wrapper>
)
}
export default PortfolioItem
const A = styled.a`
width: 100%;
height: 100%;
opacity: 0;
position: absolute;
background: rgba(255,255,255);
transition: 0.4s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 1;
font-size: 2rem;
`
const Img = styled.img`
width: 100%;
height: 340px;
object-fit: cover;
`
const Wrapper = styled.div<styleProp>`
position: relative;
grid-column: span ${props => props.colSpan};
@media (max-width: 600px){
grid-column: 1/-1;
}
:hover {
${A} {
opacity: 0.7;
}
}
`
Artık Portfolio.tsx dosyasını oluşturabiliriz, bu bileşenin kodu da çok basit:
import React from 'react'
import styled from 'styled-components'
import ContainerComponent from '../ContainerComponent'
import data from '../../data/portfolioData';
import PortfolioItem from './PortfolioItem';
import Numbers from './Numbers';
const Portfolio = () => {
return (
<Wrapper id='portfolio'>
<ContainerComponent column={true}>
<H2>Some of The Companies That Love Our Solutions</H2>
<SubTitle>
Lorem ipsum dolor sit amet consectetur adipisicing elit.
</SubTitle>
<Grid>
{data.map(d => {
return <PortfolioItem key={d.img} imgSrc={d.img} alt={d.alt} colSpan={d.cols} link={d.link}/>
})}
</Grid>
</ContainerComponent>
<Numbers />
</Wrapper>
)
}
export default Portfolio
const Wrapper = styled.section`
`
const H2 = styled.h2`
margin-top: 160px;
text-align: center;
width: 100%;
font-size: 2.4rem;
@media (max-width: 1200px){
margin-top: 100px;
}
@media (max-width: 960px){
margin-top: 80px;
}
`
const SubTitle = styled.p`
text-align: center;
margin-top: 20px;
margin-bottom: 80px;
`
const Grid = styled.div`
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-gap: 20px;
grid-auto-flow: dense;
align-items: center;
@media (max-width: 600px){
grid-template-columns: 1fr;
}
`
Var olmayan Numbers bileşeninin neden olduğu hatayı görmezden gelebelirsiniz. Veya şimdiye kadar nasıl göründüğünü görebilirmek için geçici olarak bu bileşeni kaldırabilirsiniz.
Artık sayılarımız üzerinde çalışmaya başlayacağız. Bu bileşen, bir sayım ve bir intersection observer içerecektir. Bu bileşen görüntülendiğinde sayılarımız belirleyeceğimiz sayılara kadar saymaya başlayacaktır. Şimdi data klasörümüzün içinde numbersData.ts dosyasını oluşturalım. Bu dosyada bazı basit nesne (object) dizilerimiz (array) olacak, bu dosyanın kodu burada:
interface NumData {
num: string
text: string
};
const data:NumData[] = [
{num: "2365", text: "Projects Completed"},
{num: "7012", text: "Happy Clients"},
{num: "10524", text: "Campaigns Automized"},
{num: "364", text: "Professionals"},
{num: "1842", text: "Cups of Coffee Taken"},
]
export default data
Bileşen görüntülendiğinde saymaya başlayacak bir fonksiyon oluşturmak yerine işlerimizi kolaylaştırmak için bir paket kuracağız. bash npm i react-countup komutu ile gerekli paketi kurabiliriz.
Bu kısım ayrıca bir grid olacak ve bir bileşeni grid öğesi olarak işleyecektir (render). Bileşenimizde, bir sayı, sayının ne için olduğunu açıklayan bir metin ve son olarak bileşenin ekranda olup olmadığını doğrulamak için bir boolean değeri kullanacağız. NumbersItem.tsx bileşeninin tam kodu aşağıdaki gibi:
import React, { useEffect, useState } from 'react'
import styled from 'styled-components'
import CountUp from 'react-countup';
interface PropTypes {
num: string,
text: string,
inView: boolean
}
const NumbersItem = (props: PropTypes) => {
const [ activated, setActivated ] = useState<boolean>(false);
useEffect(() => { if(props.inView) setActivated(true) }, [props.inView])
return (
<Wrapper>
<Num>{activated && <CountUp start={0} end={parseInt(props.num)} duration={2.5} />}</Num>
<p>{props.text}</p>
</Wrapper>
)
}
export default NumbersItem
const Wrapper = styled.div`
padding: 20px;
display: flex;
flex-direction: column;
justify-content: center;
@media (max-width: 480px){
align-items: center;
}
`
const Num = styled.p`
font-size: 2.8rem;
font-weight: bold;
`
Artık Numbers.tsx bileşenini oluşturabiliriz:
import React from 'react'
import styled from 'styled-components'
import ContainerComponent from '../ContainerComponent'
import data from '../../data/numbersData'
import NumbersItem from './NumbersItem'
import { useInView } from 'react-intersection-observer'
const Numbers = () => {
const { ref, inView } = useInView({
rootMargin: "0px",
threshold: 1,
});
return (
<Wrapper>
<ContainerComponent column={false}>
<Grid ref={ref}>
{data.map(d => <NumbersItem key={d.num} inView={inView} num={d.num} text={d.text}/>)}
</Grid>
</ContainerComponent>
</Wrapper>
)
}
export default Numbers
const Wrapper = styled.div`
margin-top: 160px;
margin-bottom: 40px;
width: 100%;
background: linear-gradient(0.25turn, var(--orchid-pink), var(--baby-blue-eyes));
min-height: 300px;
display: flex;
aşign-items: center;
`
const Grid = styled.div`
flex-grow: 1;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 40px;
@media (max-width: 1200px) {
grid-gap: 20px
}
@media (max-width: 960px) {
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px
}
@media (max-width: 600px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
`
Artık portföy bölümümüzü ele bitirdik. Buraya kadar her şey yolundaysa, bu bileşen görüntülendiğinde sayıların artmaya başladığını görmelisiniz. App.tsx içindeki Portfolio.tsx dosyasını aktarmayı ve offer bölümünün altına yerleştirmeyi unutmayın.
Bu bölümde blogumuzda yazılan son metinleri ve çalıştığımız markaları ziyaretçilerimize göstereceğiz. Şimdi en basit veri dosyamızı oluşturalım. data klasörünün içinde brandData.ts dosyası oluşturun. Dosyanın içinde sadece bazı marka görsellerini kullanacağız. Burada kullanmak için bazı markalar oluşturdum:
import awesome from '../assets/awesome-logo-smaller.webp';
import cool from '../assets/cool-brand-smaller.webp';
import international from '../assets/international-brand-smaller.webp';
import quality from '../assets/quality-brand-smaller.webp';
import known from '../assets/well-known-brand-smaller.webp';
interface BrandData {
img: string
};
const data:BrandData[] = [
{img: awesome},
{img: cool},
{img: known},
{img: international},
{img: quality }
]
export default data
Ayrıca markanın adı gibi daha fazla bilgiyi veya markayla ilgili bazı küçük bilgileri buraya ekleyebilirsiniz. Burada benim uydurduğum markaları herkesin tanıdığını varsayıyorum. Bu yüzden ben daha fazla bilgi eklemiyorum. Bence böyle daha iyi görünüyor ama yine de geliştirilebileceğini düşündüğünüz bölümleri değiştirin.
Son blog verileri için başka bir dosyaya ihtiyacımız olacak. data klasörünün içinde şimdi recentWorkCardData.ts oluşturun. Bu dosya bir dizi nesnesini dışa aktaracaktır. Her nesnenin bir başlığı, gönderiyi açıklayan bir metni, yazarın adı ve yazarın bir avatarı olacak. Tam kod burada:
import woman from '../assets/woman-avatar-smaller.webp'
interface recentWorkCardData {
title: string
text: string
name: string
avatar: string
}
const data:recentWorkCardData[] = [
{
title: "5 Methods to Increase Visibility",
text: "Praesent porta enim augue, et iaculis sem scelerisque id. Nulla sagittis fringilla imperdiet. Donec vestibulum leo a diam pharetra, vel ullamcorper mauris convallis.",
name: "Jane Doe",
avatar: woman
},
{
title: "How To Gain More Customers",
text: "Praesent porta enim augue, et iaculis sem scelerisque id. Nulla sagittis fringilla imperdiet. Donec vestibulum leo a diam pharetra, vel ullamcorper mauris convallis.",
name: "Jane Doe",
avatar: woman
},
{
title: "Free AI Tools You Can Use Today",
text: "Praesent porta enim augue, et iaculis sem scelerisque id. Nulla sagittis fringilla imperdiet. Donec vestibulum leo a diam pharetra, vel ullamcorper mauris convallis.",
name: "Jane Doe",
avatar: woman
},
];
export default data;
Bu veriler eşleştirilecek ve RecentWorkCard.tsx adlı bir bileşene dönüştürülecek. recentWork adlı bir klasör oluşturun ve burada RecentWorkCard.tsx dosyasını oluşturun. Bu bileşenin kodu da gayet basit:
import React from 'react'
import styled from 'styled-components'
import lines from '../../assets/lines-orange-white-smaller.webp'
interface PropTypes {
title: string
text: string
name: string
avatar: string
}
const RecentWorkCard = (props:PropTypes) => {
return (
<Wrapper>
<InfoBox>
<H3>{props.title}</H3>
<WriterP>{props.text}</WriterP>
</InfoBox>
<WriterBox>
<Img src={props.avatar} alt="" />
<WriterInfo>
<p>{props.name}</p>
<p>Sr. Web Developer</p>
</WriterInfo>
</WriterBox>
</Wrapper>
)
}
export default RecentWorkCard
const InfoBox = styled.div`
margin-bottom: 20px;
transition: 0.4s;
`
const Wrapper = styled.div`
width: 100%;
border-radius: 5px;
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
min-height: 400px;
background: linear-gradient( rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7) ), url(${lines});
background-position: bottom;
background-repeat: no-repeat;
background-size: cover;
color: white;
:hover {
cursor: pointer;
${InfoBox} {
color: var(--baby-blue-eyes);
}
}
`
const WriterBox = styled.div`
width: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-gap: 20px;
`
const WriterInfo = styled.div`
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
`
const H3 = styled.h3`
font-size: 1.4rem;
@media (max-width: 960px){
font-size: 2rem;
}
margin-bottom: 20px;
`
const WriterP = styled.p`
margin-top: 6px;
@media (max-width: 960px){
font-size: 1.4rem;
}
`
const Img = styled.img`
width: 100%;
height: 60px;
border-radius: 5px;
object-fit: cover;
`
Gördüğünüz gibi burada karmaşık bir şey yok.
Markalar bileşenine henüz sahip olmasak da devam edip RecentWork.tsx dosyasını da oluşturabiliriz. Bu bişelenin yokluğundan kaynaklanan hatayı görmezden gelin. Bu dosyanın kodu burada:
import React from 'react'
import styled from 'styled-components'
import ContainerComponent from '../ContainerComponent'
import data from '../../data/recentWorkCardData'
import RecentWorkCard from './RecentWorkCard'
import Brands from './Brands'
const RecentWork = () => {
return (
<Wrapper id='recent-work'>
<ContainerComponent column={true}>
<H2>Our most recent publications</H2>
<SubTitle>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Id quas maxime a doloremque esse.</SubTitle>
<RecentWorkGrid>
{data.map(d => <RecentWorkCard key={d.title} title={d.title} text={d.text} name={d.name} avatar={d.avatar} />)}
</RecentWorkGrid>
<Brands />
</ContainerComponent>
</Wrapper>
)
}
export default RecentWork
const Wrapper = styled.section`
width: 100%;
margin-bottom: 60px;
margin-top: 120px;
`
const H2 = styled.h2`
width: 100%;
text-align: center;
font-size: 2.8rem;
font-weight: thin;
`
const SubTitle = styled.p`
margin-top: 6px;
text-align: center;
font-size: 1.2rem;
color: var(--gray-2);
`
const RecentWorkGrid = styled.div`
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 60px;
width: 100%;
margin: auto;
margin-top: 60px;
@media (max-width: 960px){
grid-template-columns: 1fr;
}
`
Yine, karmaşık bir şey yok.
Markayı içeren parçayı oluşturma zamanı geldi. Brands.tsx dosyasının kodu burada:
import React from 'react'
import styled from 'styled-components'
import data from '../../data/brandData'
import BrandItem from './BrandItem'
const Brands = () => {
return (
<Wrapper>
<H3>You may know our partners</H3>
{data.map(d => <BrandItem img={d.img} />)}
</Wrapper>
)
}
export default Brands
const Wrapper = styled.div`
margin-top: 120px;
width: 100%;
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-gap: 100px;
@media (max-width: 1200px) {
grid-gap: 20px;
}
@media (max-width: 720px) {
grid-template-columns: repeat(3, 1fr);
grid-gap: 30px;
}
@media (max-width: 480px) {
grid-template-columns: 1fr 1fr;
grid-gap: 20px;
}
`
const H3 = styled.h3`
grid-column: 1/ -1;
margin-bottom: 0;
font-size: 1.8rem;
margin-left: 10px;
`
Şimdi tüm markalarımızı BrandItem.tsx dosyası içinde oluşturmak için kullanacağımız bileşen:
import React from 'react'
import styled from 'styled-components'
interface PropTypes {
img: string
}
const BrandItem = (props:PropTypes) => {
return (
<Wrapper>
<Img src={props.img} alt="" />
</Wrapper>
)
}
export default BrandItem
const Wrapper = styled.div`
padding: 10px;
border-radius: 5px;
display: flex;
flex-direction: column;
align-items: center;
`
const Img = styled.img`
width: 100%;
transition: 0.4s;
max-width: 160px;
border-radius: 5px;
:hover {
box-shadow: 0px 0px 32px var(--nadeshiko-pink);
}
`
Eklenen bu bileşen ile "Recent Work" (son çalışmalar) bölümünü sonlandırmış oluyoruz. Bu bileşeni App.tsx içine aktarın ve portföy bölümünün altında kullanın.
Elbette ziyaretçilere istedikleri zaman bizimle kolayca iletişim kurma ve yerimizi gösterme seçeneği sunmalıyız. İletişim bölümü bu iki ana bölümden oluşacaktır. components içinde contact adında bir klasör oluşturalım ve başlayalım.
contact klasörünün içinde haritamızı oluşturmaya başlayabiliriz. Google maps yerine bu harita için mapbox kullanmaya karar verdim. Bu haritayı kullanabilmek için api anahtarınızı almayı unutmayınız. API anahtarınızı projenizin kökündeki .env.local dosyasına kaydedin. API anahtarımı REACT_APP_MAPBOX_ACCESS_TOKEN olarak adlandırdım. Değişkeni "REACTAPP" ile başlatmak, bizim uygulamamızda onu görünür kılacaktır.
Bazı performans sorunları vardı ama şimdi performans açısından kabul edilebilir olduğunu düşünüyorum. Ana iş parçacığını (Main thread) engellememek için bir web worker kullanacağız. Haritayı tam ekran yapmak için bir buton, ofisimizin yerini gösteren bir pin ve ziyaretçinin yerini göstermek için başka bir buton haritamızın üzerinde olacak. Şimdi bu kısım için gerekli paketleri bu komutla kuralım:
npm i mapbox-gl react-map-gl worker-loader
Inside the contact folder create the MapComponent.tsx file and the add this code:
import React from 'react';
import styled from 'styled-components';
import ReactMapGL, {
Marker,
FullscreenControl,
GeolocateControl,
NavigationControl
} from 'react-map-gl';
import pin from '../../assets/pin.png'
import mapboxgl from 'mapbox-gl';
// @ts-ignore
// eslint-disable-next-line import/no-webpack-loader-syntax
mapboxgl.workerClass = require('worker-loader!mapbox-gl/dist/mapbox-gl-csp-worker').default;
const MapComponent = () => {
return (
<Wrapper>
<ReactMapGL
initialViewState={{ latitude: 48.87, longitude: 2.3, zoom: 5.5 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
style={{ height:"100%", width: "100%"}}
attributionControl={false}
mapboxAccessToken={process.env.REACT_APP_MAPBOX_ACCESS_TOKEN}
>
<GeolocateControl
positionOptions={{enableHighAccuracy: true}}
trackUserLocation={false}
/>
<FullscreenControl />
<Marker longitude={2.3} latitude={48.87} anchor="bottom" >
<img style={{width:"21px", height:"14px",top:"-11px", bottom: "-7px"}} src={pin} alt="pin" />
</Marker>
<NavigationControl visualizePitch={true}/>
</ReactMapGL>
</Wrapper>
);
};
export default MapComponent
const Wrapper = styled.div`
width: 100%;
height: 100%;
@media (max-width: 960px) {
grid-column: 1/-1;
}
min-height: 400px;
`
Sadece google'da biraz arama yaparak düzeltebileceğiniz bazı performans sorunları olduğunu göreceksiniz. Haritada daha fazla özelleştirme ve yapılandırma da yapılabilir. Dökümanları okumanızı okumanızı ve seçeneklerinizi araştırmanızı öneririm. İlerleyen zamanlarda, sonraki projelerinizde işinize yarayacağına eminim.
Şimdi iletişim formumuzu ele alalım. Henüz bir arka ucumuz veya postalarımızı işlemek için herhangi bir aracımız olmadığı için, ziyaretçiler tarafından bize verilen verileri console.log()'da kullanacağız.
import React, { useState } from 'react'
import styled from 'styled-components'
import Button from '../Button'
const ContactForm = () => {
const [name, setName] = useState<string>("");
const [email, setEmail] = useState<string>("");
const [subject, setSubject] = useState<string>("");
const [text, setText] = useState<string>("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("the info to send", name, email, subject, text)
}
return (
<Wrapper>
<H3>Feel free to contact us!</H3>
<P>We will respond to you as soon as we can</P>
<Form onSubmit={(e) => handleSubmit(e)}>
<Label htmlFor="f-name">
Enter your name
<Input
required
type="text"
name="f-name"
placeholder='John Doe'
onChange={(e) => setName(e.target.value)}
/>
</Label>
<Label htmlFor="f-email">
Enter your email address
<Input
type="email"
name="f-email"
required
placeholder='example@gmail.com'
onChange={(e) => setEmail(e.target.value)}
/>
</Label>
<Label htmlFor="f-subject">
Enter a subject
<Input
type="text"
name="f-subject"
required
placeholder='Marketing campaign'
onChange={(e) => setSubject(e.target.value)}
/>
</Label>
<Label htmlFor="f-textarea">
What would you like to share with us
</Label>
<Textarea
required
rows={12}
onChange={(e) => setText(e.target.value)}
/>
<Button text='submit'/>
</Form>
</Wrapper>
)
}
export default ContactForm
const Wrapper = styled.div`
width: 100%;
display: flex;
flex-direction: column;
padding: 20px 0;
`
const H3 = styled.h3`
font-size: 1.8rem;
`
const P = styled.p`
color: var(--gray-2);
`
const Form = styled.form`
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
`
const Label = styled.label`
font-size: .8rem;
margin-top: 14px;
`
const Input = styled.input`
width: 100%;
padding: 8px;
font-size: 1.4rem;
transition: 0.4s;
:focus {
box-shadow: 0px 0px 32px var(--baby-blue-eyes);
}
`
const Textarea = styled.textarea`
width: 100%;
padding: 10px;
font-size: 1.1rem;
transition: 0.4s;
:focus {
box-shadow: 0px 0px 32px var(--baby-blue-eyes);
}
margin-bottom: 10px;
`
Bu bölümde özel bir şey yok. Gördüğünüz gibi oluşturduğumuz buton bileşeni için başka bir kullanım daha bulduk, bu yüzden kodu tekrarlamak yerine bileşen oluşturmak çok daha basit.
Şimdi Contact.tsx dosyasını oluşturacağız ve az önce oluşturduğumuz bu iki bileşeni buraya aktaracağız. Bu bileşenin kodu burada:
import React from 'react'
import styled from 'styled-components'
import ContactForm from './ContactForm'
import ContainerComponent from '../ContainerComponent'
import Map from './MapComponent'
const Contact = () => {
return (
<ContainerComponent column={false}>
<Wrapper id='contact'>
<Map />
<ContactForm />
</Wrapper>
</ContainerComponent>
)
}
export default Contact
const Wrapper = styled.section`
display: grid;
grid-template-columns: 1fr 1fr;
width: 100%;
min-height: 600px;
grid-gap: 40px;
@media (max-width: 960px) {
grid-template-columns: 1fr;
}
`
İşte bu, şimdi harika bir iletişim bölümümüz var. Bu kısım da şimdiye kadar oluşturduğumuz diğer tüm parçalar gibi responsive. Geliştirebileceğinizi düşündüğünüz bir şey varsa, devam edin ve değiştirin!
Bu bileşeni RecentWork'un altında App.tsx dosyamıza aktarın. Haritayı doğru bir şekilde oluşturmak için index.css içine ilk satıra aşağıdaki kodu ekleyin:
@import 'mapbox-gl/dist/mapbox-gl.css';
Footer, ziyaretçinizin en son göreceği kısım. Footer, 3 ana bölümden oluşacak. Ziyaretçilerin telefon yoluyla bizimle iletişime geçmeleri için bir bölüm. Ziyaretçilerimizden haftalık bültenimize (newsletter) abone olmalarını istemek için başka bir bölüm. Telif hakkı metni ve ziyaretçilerin şirketimizi sosyal medyadan takip etmesini sağlayan sosyal medya bağlantılarını içerecek olan footer'ın alt kısmı için son bölüm.
components klasörünün içinde footer klasörümüzü oluşturarak başlayalım. Bu kısım için oluşturacağımız ilk dosya Contact.tsx. Kod çok basit olduğu için hemen sizinle paylaşacağım:
import React from 'react'
import styled from 'styled-components'
const ContactComponent = () => {
return (
<Contact>
<H3>Let's talk!</H3>
<Text>Feel free to contact us 24/7. We can discuss an ongoing project or start talking about a new one. You will be a ble to reach an employee whenever you call.</Text>
<Number>0555-5555-555555-555</Number>
<Number>0555-5555-555555-554</Number>
</Contact>
)
}
export default ContactComponent
const Contact = styled.div`
padding: 100px 0px 120px;
@media (max-width: 960px){
padding: 40px 0 0 0 ;
}
`
const H3 = styled.h3`
color: var(--white);
font-size: 2rem;
margin-bottom: 40px;
`
const Text = styled.p`
margin-bottom: 20px;
`
const Number = styled.p`
color: var(--baby-blue-eyes);
font-size: 1.4rem;
`
Şimdi haber bültenimizi oluşturalım. Bu kısım için de bir arka ucumuz yok, bu yüzden ziyaretçi tarafından bize verilen e-posta adresini console.log()'layacağız. Burada Newsletter.tsx dosyası oluşturun ve aşağıdaki kodu ekleyin:
import React, { useState } from 'react'
import styled from 'styled-components';
import Button from '../Button';
const NewsletterComponent = () => {
const [email, setEmail] = useState<string>("");
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log("sent email is ==> ",email)
}
return (
<Newsletter>
<H3>Sign up to our newsletter.</H3>
<Text>If you wanna find out the latest tips and tricks we think that can help your business grow, sign up to our newsletter. You can subscribe easily if you don't like our weekly emails, no worries.</Text>
<Form onSubmit={e => handleSubmit(e)}>
<label htmlFor="">
<Input
type="email"
required
onChange={e => setEmail(e.target.value)}
/>
</label>
<Button text='Sign Up'/>
</Form>
</Newsletter>
)
}
export default NewsletterComponent;
const H3 = styled.h3`
color: var(--white);
font-size: 2rem;
margin-bottom: 40px;
`
const Text = styled.p`
margin-bottom: 20px;
margin-left: auto;
`
const Newsletter = styled.div`
padding: 100px 0px 120px;
width: 100%;
@media (max-width: 960px){
padding: 0 ;
}
`
const Form = styled.form`
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
`
const Input = styled.input`
margin-top: 20px;
margin-bottom: 20px;
width: 100%;
padding: 8px;
font-size: 1.4rem;
transition: 0.4s;
:focus {
box-shadow: 0px 0px 32px var(--baby-blue-eyes);
}
`
Gördüğünüz gibi sadece basit bir form.
Footer'ın alt kısmı için fontawesome'dan bazı simgeler kullanacağız. Bu kısım ziyaretçileri, eğer href değerlerini değiştirmezseniz, blogumuza ve github profilimize yönlendirecek devam edin ve değiştirin. FooterBottom.tsx dosyasının kodu burada:
import React from 'react'
import styled from 'styled-components'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faFacebook, faTwitter, faGithub } from '@fortawesome/free-brands-svg-icons';
import { faBlog } from '@fortawesome/free-solid-svg-icons';
const FooterBottomComponent = () => {
return (
<FooterBottom>
<Icons>
<IconA href="/#">
<FontAwesomeIcon icon={faFacebook} />
</IconA>
<IconA href="https://wwizard-blog.netlify.app/">
<FontAwesomeIcon icon={faBlog} />
</IconA>
<IconA href="https://github.com/ilci66">
<FontAwesomeIcon icon={faGithub} />
</IconA>
<IconA href="/#">
<FontAwesomeIcon icon={faTwitter} />
</IconA>
</Icons>
<Text>You can find the full code <A href='#'>here</A> and the blog post <A href='/#'>here</A>. Ilker AKBIYIK</Text>
</FooterBottom>
)
}
export default FooterBottomComponent
const FooterBottom = styled.div`
width: 100%;
display: flex;
flex-direction: row;
grid-column: 1/-1;
align-items:center;
@media (max-width: 960px){
padding-top: 20px;
}
@media (max-width: 720px){
flex-direction: column;
}
`
const A = styled.a`
color: var(--baby-blue-eyes);
text-decoration: none;
`
const Icons = styled.div`
display: flex;
flex-direction: row;
@media (max-width: 720px){
width: 100%;
justify-content: space-between;
}
`
const IconA = styled.a`
font-size: 2.4rem;
color: var(--baby-blue-eyes);
background: var(--gray-2);
margin: 10px;
padding: 10px 20px;
transition: 0.4s;
:hover {
background: var(--white);
color: var(--nadeshiko-pink);
}
@media (max-width: 600px){
padding: 6px 8px;
margin: 4px;
}
`
const Text = styled.p`
margin-bottom: 20px;
margin-left: auto;
@media (max-width: 720px){
margin: auto;
}
`
Şimdi tüm bunları Footer.tsx dosyasıyla tamamlayabiliriz. Bu dosya çoğunlukla bileşenlerin yerleştirilmesini işleyecek. Kod burada:
import React from 'react'
import styled from 'styled-components'
import ContainerComponent from '../ContainerComponent'
import Contact from './Contact';
import Newsletter from './Newsletter';
import FooterBottomComponent from './FooterBottom';
const Footer = () => {
return (
<Wrapper>
<ContainerComponent column={true}>
<ContactNewsletter>
<Contact />
<Newsletter />
</ContactNewsletter>
<FooterBottomComponent />
</ContainerComponent>
</Wrapper>
)
}
export default Footer
const Wrapper = styled.section`
width: 100%;
background: var(--gray);
margin-top: 40px;
color: var(--gray-2);
@media (max-width: 960px){
padding-bottom: 40px;
}
`
const ContactNewsletter = styled.div`
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10vw;
width: 100%;
@media (max-width: 960px){
grid-template-columns: 1fr;
grid-gap: 20px;
}
`
Bu bileşeni App.tsx içine ekleyin ve son olarak App.tsx dosyamız şöyle görünecek:
import React, { useEffect, useState } from 'react';
import styled from 'styled-components';
import Banner from './components/Banner';
import Navbar from './components/navbar/Navbar';
import About from './components/About';
import Offer from './components/offer/Offer';
import Portfolio from './components/portfolio/Portfolio';
import RecentWork from './components/recentWork/RecentWork';
import Contact from './components/contact/Contact';
import Footer from './components/footer/Footer';
function App() {
const [ onScreen, setOnScreen ] = useState();
const [ bgColor, setBGColor ] = useState("inherit");
useEffect(() => {
if(!onScreen) setBGColor("var(--white)");
else if(onScreen) setBGColor("inherit");
}, [onScreen])
return (
<AppWrapper>
<Navbar bgColor={bgColor}/>
<Banner setOnScreen={setOnScreen}/>
<About />
<Offer />
<Portfolio />
<RecentWork />
<Contact />
<Footer />
</AppWrapper>
);
}
export default App;
const AppWrapper = styled.div`
min-height: 100vh;
display: flex
width: 100vw;
`;
Bence bu proje başlangıç seviyesi için oldukça uygun ve reactjs becerilerinizi geliştirmeniz için yeterince karmaşıklık da içeriyor. Düz HTML ve CSS yerine reactjs kullanarak bir açılış sayfası türü proje oluşturmak biraz alışılmadık, ancak tepki ile böyle bir web sitesini kolayca oluşturabileceğinizi anlamak eğlenceliydi. HTML ve CSS yerine reactjs kullanarak bu şekilde bir sayfa oluşturmak biraz alışılmadık gelebilir, ancak react ile de bu tip bir web sitenin kolayca oluşturmak eğlenceliydi ve react beceriniz arttırması yönünden de önemli.
Styled components, performans açısından size pahalıya mal olabilir. Bu projeyi düz CSS kullanarak geliştirmiş olsaydınız, web siteniz biraz daha hızlı olabilirdi. Bir geliştirici olarak, her proje için hangi araçların kullanılacağına ilişkin kararları sizin vermeniz gerekir. Her proje için ne kullanmanız gerektiğine karar vermek için birden fazla araçla deneyim sahibi olmanız gerekir. Her proje, farklı gereklilikler ve potansiyel sorunlarla gelecek.
Umarım bişeyler kapmışsınızdır ve bu projeyi geliştirirken biraz eğlenmişsinizdir. Burada durmayın, daha da geliştirin, başka sayfalar ekleyin, bir arka uç oluşturun, postalama mantığı oluşturun vb. Bunu gelecekteki projeleriniz için bir şablon olarak kullanmakta özgürsünüz.
Ilker Akbiyikİçindekiler