Ulazimo u zadnju fazu projekta. Ovdje ćemo pokazati kako napraviti deploy projekta na Netlify. Naravno postoje i drugi servisi za hosting, ali za Netlify je najpopularniji i jednostavan je za korištenje.
Opisat ćemo kako se u gatsby-u može ubaciti third-party sadržaj i kako gatsby može koristiti CMS. Objasnit ćemo i što je to CMS.
Segment stranice koji ćemo napraviti bit će galerija slika. Te slike ćemo dovući u gatsby kroz GraphQL s interneta i one će biti optimizirane i spremne za prikaz. Kako budemo dodavali slike u izvor, tako će se one automatski moći dodati u gatsby bez da moramo mijenjat kod.
Definiramo novu stranicu. Vidimo da ima svoj naslovni bar i da se sastoji isključivo od kolekcije slika. Taj naslovni bar koristimo u Contact
stranici. Za početak ćemo ga izvući i dodijeliti mu props tako da ga možemo koristiti i ovdje.
Dalje, komponenta koju imamo je container za niz slika. Ne bi trebalo biti teško za napraviti. Također, vidimo da svako drugi container ima sivu pozadinu i svaki od njih ima drugi tekst. Ako razmišljate o map()
funkciji nad nizom stringova, čestitam! Počinjete razmišljati kao React programer. Što sa slikama?
U ovim vježbama pokazat ćemo kako se može lako zakačiti na vanjsku stranicu kroz GraphQL i dovući slike. Stranica na koju se kačimo je Instagram, a profil je službeni profil Hrvatske turističke stranice: "Croatia full of life". Vidimo da će trebati dohvatiti dosta velik broj slika. To nije nikakav problem. Svaki od ovih containera ima naslov koji će biti prop i niz slika za prikazati koji je također prop.
Koncept koji želimo pokazati ovdje je CMS. Točnije, ovo što radimo je Headless CMS. CMS stoji za Content Managment System. Riječ je o sustavu za upravljanje sadržajem stranice nakon što je ona napravljena. Zapitajte se kako novinarski portali ažuriraju svoj sadržaj. Mislite da novinari znaju HTML, CSS ili Markdown i pišu članke pa onda rade git push? Teško. Na stranu to što je to van njihove struke, zamislite na što bi ličio taj git history.
Isto je s blogovima. Mislite da se svaki blog dodaje kao novi page u stranicu? Svaki put HTML/CSS pa commit / push?
Taj problem dodavanja sadržaja u stranicu koja je live rješava CMS. CMS radi slično kao i pisanje komentara po društvenim mrežama. Postoji definirano sučelje gdje pišete sadržaj (npr. novinski članak), dodajete slike, boldirate tekst, ubacujete Twitter ili Instagram feed u to i kad završite napravite submit. Stranica detektira da se stvorio novi sadržaj i povuče ga, automatski mu dodijeli link (probajte pročitati link na neki članak na news portalima i vidjet ćete da je jednak naslovu). Taj članak je onda živ i možete ga vidjeti. Pretvoren je u HTML. Napisao ga je novinar koji ne mora znati ni kako se HTML izgovara. To omogućuje CMS.
Zašto je Headless? Jer nemamo integrirano sučelje unutar stranice za dodavanje sadržaja (Wordpress), nego sadržaj dolazi u stranicu kroz neki API (u našem slučaju, GraphQL). Više ovdje. BTW, pogledajte taj URL. Očito je da je auto-generated (CMS) :)
Gatsby ima definirano sučelje (GraphQL) i plugin system koji mu omogućava da se zakači na bilo koji CMS koji preferirate. Često se koriste Contentful i Ghost iako ima i drugih.
U ovim vježbama pokazat ćemo kako se kači na Instagram za vježbu. U sljedećim vježbama spojit ćemo se na Contentful CMS i stvarati dinamičan sadržaj (blog).
Za početak riješimo ContactSeparator
. Očito, moramo promijeniti to ime da se može koristiti općenito. Mijenjanjem imena moramo promijeniti i import. Također, dodajemo mu prop za prikaz proizvoljnog teksta. Promijenit ćemo mu ime u SeparatorBar
i dodat ćemo mu tetx
prop. Zatim prelazimo na galerije slika.
- Commit 1: creating accutal Combobox
- Mijenjamo ime ContactSeparatora u
modules/SeparatorBar
- Dodamo
text
u props - Dodamo poziv u pages
- Mijenjamo ime ContactSeparatora u
- Commit 2: adding PhotoGallery page
- Stvaramo page
- Dodajemo navigaciju u
const
- Commit 3: adding and configuring instagram plugin
- Instaliramo NPM plugin
- Dodajemo konfiguraciju u
gatsby-config.js
- Commit 4: adding and configuring instagram plugin
- Stvaramo
GalleryContainer
- Stvaramo
GalleryContent
- Dodajemo
GalleryContainer
x 3 uGalleryContent
- Dodajemo
GalleryContent
upages
- Stvaramo
- Commit 5: adding ImagePool
- Tražimo GraphQL izraz
- Definiramo ImagePool komponentu
- Pišemo staticQuery
- Commit 6: configuring ImagePool
- Dodajemo props za uzimanje skupa slika
- Radimo slice pa map
- Commit 7: Upgrading GalleryContainer to accept bounds and style
- Dodajemo
isGray
props - Dodajemo
start
iend
props
- Dodajemo
- Commit 8: Correcting styling
- Dodajemo style u
ImagePool
- Dodajemo style u
GalleryContainer
- Dodajemo style u
- **Commit 9: Refactoring to use map **
- Dodajemo map
- Dodajemo isGray logiku
- Dodajemo start / end logiku
Implementiramo redom stablo komponenti:
- Mijenjamo ime ContactSeparatora u
modules/SeparatorBar
- Dodamo
text
u props - Dodamo poziv u pages
Idemo u modules/ContactSeparator
i dodajemo props i mijenjamo ime foldera i komponente u SeparatorBar
. Dodajemo props:
import React from 'react'
import styles from './style.module.css'
const SeparatorBar = ({text}) => (
<section className={styles.separator}>
<div className={styles.horizontalLine} />
<h2>{text}</h2>
<div className={styles.horizontalLine} />
</section>
)
export default SeparatorBar
Ok, sad trebamo primijeniti promjene u pages/contact.js
:
import React from "react"
import HeaderFooterLayout from "../layouts/headerFooter"
import SeparatorBar from '../modules/SeparatorBar'
import ContactForm from '../modules/ContactForm'
const ContactPage = () => (
<HeaderFooterLayout>
<SeparatorBar text="Contact"/>
<ContactForm />
</HeaderFooterLayout>
)
export default ContactPage
Spreman za korištenje u novoj stranici.
Možemo commit!
Sadržaj commitova
- Stvaramo page
- Dodajemo navigaciju u
const
Dodajmo page pages/gallery.js
za početak:
import React from "react"
import HeaderFooterLayout from "../layouts/headerFooter"
import SeparatorBar from '../modules/SeparatorBar'
const PhotoGallery = () => (
<HeaderFooterLayout activeTab="Photo Gallery">
<SeparatorBar text="Photo Gallery"/>
</HeaderFooterLayout>
)
export default PhotoGallery
Dodajmo navigaciju u constants/const
:
export const navs = [
{tab: 'Home', to: '/'},
{tab: 'Accommodation', to: '/'},
{tab: 'Photo Gallery', to: '/gallery'},
{tab: 'Contact', to: '/contact'}
]
Osvježimo stranicu i trebali bismo vidjeti ovo:
Dobar početak! Možemo commit.
Sadržaj commitova
- Instaliramo NPM plugin
- Dodajemo konfiguraciju u
gatsby-config.js
U ovom commit-u nema koda, samo instalacija paketa i dodavanje u config. Krenimo sa instalacijom:
$ npm -i gatsby-source-instagram --save
Kad se paket instalira, dodajemo ga u konfiguraciju. Dokumentacija je ovdje. Mi pratimo dokumentaciju za "scraping for posts" i kasnije posts.
U gastby-config.js
dodamo kod kao u dokumentaciji:
plugins: [
`gatsby-plugin-react-helmet`,
// ...
{
resolve: `gatsby-source-instagram`,
options: {
username: `266897135`,
},
},
// ....
]
username
je broj koji možemo dobiti ovdje. Naš odgovara croatiafulloflife
profilu. Naravno, možete staviti bilo koji profil.
Promjena u gatsby-config.js
datoteci zahtijeva ponovno pokretanje gatsby aplikacije. Ugasimo server sa CTR+C i pokrenimo gastby develop
još jednom.
Možemo commitat.
- Stvaramo
GalleryContainer
- Stvaramo
GalleryContent
- Dodajemo
GalleryContainer
x 3 uGalleryContent
- Dodajemo
GalleryContent
upages
Komponenta GalleryContainer
je komponenta koja će držati slike i naslove. GalleryContent
je modul koji drži 3 GalleryContainer
komponente.
Definirajmo komponentu components/GalleryContainer
:
import React from 'react'
import styles from './style.module'
const GalleryContainer = ({title}) => (
<section className={styles.galleryContainer}>
<h1>{title}</h1>
<div>IMAGES</div>
</section>
)
export default GalleryContainer
Odmah je ubačen title
prop koji označava naslov ("Amazing exterior", "Relaxing Sauna" itd).
Ubacimo i CSS:
.galleryContainer {
width: 80%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
}
Sad stvaramo njegov modul koje ga poziva. Definirat ćemo ga u modules/GalleryContent
i root mu je <main>
.
import React from 'react'
import styles from './style.module.css'
import GalleryContainer from '../../components/GalleryContainer'
const GalleryContinent = () => (
<main className={styles.galleryContent}>
<GalleryContainer title="Amazing exterior" />
<GalleryContainer title="Relaxing sauna" />
<GalleryContainer title="Modern interior" />
</main>
)
export default GalleryContinent
CSS:
.galleryContent {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
Dodajmo ga u pages/gallery
:
import React from "react"
import HeaderFooterLayout from "../layouts/headerFooter"
import SeparatorBar from '../modules/SeparatorBar'
import GalleryContent from "../modules/GalleryContent"
const PhotoGallery = () => (
<HeaderFooterLayout activeTab="Photo Gallery">
<SeparatorBar text="Photo Gallery"/>
<GalleryContent />
</HeaderFooterLayout>
)
export default PhotoGallery
Imamo ovo:
Možemo commit!
Sadržaj commitova
- Tražimo GraphQL izraz
- Definiramo ImagePool komponentu
- Pišemo staticQuery
Definirajmo što sljedeća komponenta mora raditi:
- Dohvatiti sve slike s instagrama
- Prikazati određen broj tih slika u nizu (galerija)
Krenut ćemo od samog GraphQL izraza.
Otvorimo localhost:8000/___graphql
i idemo redom:
- Otvaramo
allInstaNode
: pristupa svim postovima (max 50)edges
: niz postovanode
: odaberemo pojedinačni čvor (post)localFile
: uzimamofile
isto kao kod slika...GatsbyImageSharpFixed
: uzimamo objekt zagatsby-image
Ako se pitate otkud meni ovo, odgovor je kao i uvijek: dokumentacija.
Primijetite da
...GatsbyImageSharpFixed
NE postoji s lijeva. To je zato što je to fragment definiran unutargatsby-transformer-sharp
komponente kojoj ovaj GraphQL editor nema pristup. Što se nalazi unutar tog fragmenta možete vidjeti ovdje. Relevantan GitHub issue
Sad kad imamo osnovnu strukturu ubacit ćemo par filtera:
- Ne želimo dohvaćati video niti kolekcije slika
- Želimo max 24 slike (2 x 4 + 1 x 16)
- Za fixed definiramo veličinu
Ovo radimo dodavanjem filtera. Filteri su ljubičasti, a polja su plava u editoru. Koristit ćemo limit
filter koji ćemo staviti na 24
što znači maksimalno 24 slike. mediaType
ćemo staviti na eq
i GrpahImage
što znači samo slike, bez videa i kolekcija. Imamo ovo:
Fragment
...GatsbyImageSharpFixed
smo zamijenili onim što on predstavlja unutarlocalFile
. Očito je da je on samo kratica i ništa posebno.
Nismo definirali veličinu pa ubacimo i to:
Pokrenimo query i s desne strane vidimo rezultat:
Probajte hoverat mišom preko src
stringa. Ovaj rezultat definira strukturu objekta kojeg ćemo dobiti unutar komponente nakon što se query izvrši. Krenimo pisati tu komponentu.
Komponentu ćemo nazvati <ImagePool />
jer će sadržavati sve naše slike. Komponenta će interno složiti slike u niz i vratiti ga. Prije nego dođemo do toga, napravimo osnovnu komponentu i složimo query:
import React from 'react'
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
const ImagePool = () => {
const data = useStaticQuery(graphql`
query {
myImages: allInstaNode(limit: 24, filter: {mediaType: {eq: "GraphImage"}}) {
edges {
node {
localFile {
childImageSharp {
fixed(width: 320) {
...GatsbyImageSharpFixed
}
}
}
}
}
}
}
`)
return (
<div>
{data.myImages.edges.map(edge =>
<Img fixed={edge.node.localFile.childImageSharp.fixed} />
)}
</div>
)
}
export default ImagePool
Objasnimo kratko kod. Nakon što se query izvrši rezultat je spremljen kao što je prikazano na slici iznad. Pristupamo mu kroz data
. Rekli smo da je edges
niz (array) pa možemo zvati map()
na njemu što i radimo. Nakon toga uzimamo sve .node.localFile.childImageSharp.fixed
i stvaramo gatsby-image
od njih. Tako na kraju dobijemo niz slika. Izi!
Testa radi, dodajmo ovu komponentu u GalleryContainer
umjesto <div>
:
import React from 'react'
import ImagePool from '../ImagePool'
import styles from './style.module.css'
const GalleryContainer = ({title}) => (
<section className={styles.galleryContainer}>
<h1>{title}</h1>
<ImagePool />
</section>
)
export default GalleryContainer
Vidjet ćemo ovaj nered:
Vidimo da visine nisu ujednačene jer smo zakucali width
, a gatsby čuva aspect ratio. Idemo i to pregaziti tako što ćemo definirati i height: 190
:
//...
childImageSharp {
fixed(width: 320 height: 190) {
...GatsbyImageSharpFixed
//...
I sad vidimo puno manji nered:
Možemo commit components/ImagePool
komponentu.
- Dodajemo props za uzimanje skupa slika
- Radimo slice pa map
Sjetimo se što je bio cilj komponente s tim da smo prvi cilj zadovoljili:
Dohvatiti sve slike s instagrama- Prikazati određen broj tih slika u nizu (galerija)
Vidjeli smo na testu ranije da se prikazuje niz slika, ali svih 24 odjednom. Želimo prikazati 4 + 4 + 16 u 3 galerije. Očito da ImagePool
mora primati argumente koji će to omogućiti. Budući da je broj slika zakucan na 24, možemo slati početni i krajnji index slika koje želimo prikazati.
Na primjer, pošaljemo 1 i 4 za prve 4 slike, pa 4 i 8 za druge 4 itd.
Definirajmo to:
import React from 'react'
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
const ImagePool = ({start, end}) => {
const data = useStaticQuery(graphql`
query {
myImages: allInstaNode(limit: 24, filter: {mediaType: {eq: "GraphImage"}}) {
edges {
node {
localFile {
childImageSharp {
fixed(width: 320 height: 190) {
...GatsbyImageSharpFixed
}
}
}
}
}
}
}
`)
return (
<div>
{data.myImages.edges.map(edge =>
<Img fixed={edge.node.localFile.childImageSharp.fixed}/>
)}
</div>
)
}
export default ImagePool
Sad bi trebalo primijeniti te granice. JS ima funkciju koja se zove slice
i koja vraća niz koji je podniz tog niza. Znači, ako pozovemo slice
prije poziva map
funkcije, ona će se izvršiti nad podnizom umjesto nad originalnim nizom. To ćemo i napraviti:
//...
return (
<div>
{data.myImages.edges.slice(start, end).map(edge =>
<Img fixed={edge.node.localFile.childImageSharp.fixed}/>
)}
</div>
Probajmo poslati parametre iz GalleryContainer
komponente:
import React from 'react'
import ImagePool from '../ImagePool'
import styles from './style.module.css'
const GalleryContainer = ({title}) => (
<section className={styles.galleryContainer}>
<h1>{title}</h1>
<ImagePool start={1} end={4} />
</section>
)
export default GalleryContainer
Imamo ovo:
Primijetimo da imamo 3 slike, ne 4. Zašto? (Hint: array indexing)To je to zasad, možemo commit.
- Dodajemo
isGray
props - Dodajemo
start
iend
props
Vidimo da su sve slike iste u sve tri instance. Također, nema sive pozadine. Komponenta GalleryContainer
mora to rješavati, ne ImagePool
. Modul GalleryContent
stvara 3 GalleryContainer
komponente koje se razlikuju po naslovu, sadržaju slika i pozadinskoj boji. Znači da GalleryContainer
mora primati neki props, GalleryContent
ga mora slati.
Taj props je:
start
iend
koji se prosljeđuje uImagePool
isGray
koji govori treba li komponenta imati sivu pozadinu
Znači sve skupa 3. Dodajmo ih!
import React from 'react'
import ImagePool from '../ImagePool'
import styles from './style.module.css'
const GalleryContainer = ({title, start, end, isGray}) => (
<section style={{bbackgroundColor: isGray ? '#f2f2f2' : 'white'}}
className={styles.galleryContainer}>
<h1>{title}</h1>
<ImagePool start={start} end={end} />
</section>
)
export default GalleryContainer
Sad idemo u module i tražimo GalleryContent
. Šaljemo parametre:
import React from 'react'
import styles from './style.module.css'
import GalleryContainer from '../../components/GalleryContainer'
const GalleryContinent = () => (
<main className={styles.galleryContent}>
<GalleryContainer title="Amazing exterior" start={0} end={4} isGray />
<GalleryContainer title="Relaxing sauna" start={4} end={8} />
<GalleryContainer title="Modern interior" start={8} end={24} isGray />
</main>
)
export default GalleryContinent
Vidimo da su logika i sadržaj tu, ali CSS nije baš. Dodajmo ga u sljedeći commit. Zasad. Commitajmo što imamo.
- Dodajemo style u
ImagePool
- Dodajemo style u
GalleryContainer
Vidimo da su slike zbijene i da zadnje slike "bježe" prema lijevo. To je zato što rade overflow, a nemaju flex. Nije problem :)
Krenimo s ImagePool
komponentom. Njoj još nismo niti definirali style datoteku.
import styles from './style.module.css'
//...
<div className={styles.imagePool}>
{data.myImages.edges.slice(start, end).map(edge =>
<Img fixed={edge.node.localFile.childImageSharp.fixed}/>
)}
</div>
Definirajmo flex-wrap
i padding.
Bitno je napomenuti da koristimo
fixed
nefuild
pa će zbog toga prilikom promjene veličine ekrana slike prelaziti u idući red. Razlog je što je safixed
slikama lakše raditi, a sam styling nije poanta ovih vježbi pa smo uzeli fixed. Možete za vježbu probati sa fluidom. Ako se usudite ^ ^
CSS:
.imagePool {
display: flex;
flex-wrap: wrap;
padding: 40px 0;
width: calc(1280px + 8 * 10px);
}
.imagePool > div {
margin: 6px 10px;
}
Dodajmo još nešto zanimljivo na slike: zoom on hover.
Ispod dodajemo hover selektor i transition:
.imagePool > div {
margin: 6px 10px;
transition: all 0.5s ease-in-out;
}
.imagePool > div:hover {
transform: scale(1.1);
}
Strelica selektor > je jako bitna u ovom slučaju. Zašto?
GalleryContainer
CSS:
@import url("https://fonts.googleapis.com/css?family=Lato");
.galleryContainer {
width: 100%;
display: flex;
flex-flow: column;
align-items: center;
justify-content: center;
padding-top: 30px;
}
.galleryContainer h1 {
text-align: center;
margin: 10px 0px 25px;
font-size: 34px;
font-weight: 500;
text-transform: uppercase;
color: #999;
letter-spacing: 1.4px;
font-family: Lato;
}
Možemo commitat
Sadržaj commitova
- Dodajemo map
- Dodajemo isGray logiku
- Dodajemo start / end logiku
Ovaj commit je totalno nepotreban i ne trebamo ga raditi, ali nije loše vježbati map()
.
Ako ste za, idemo u GalleryContent
modul i vidimo niz GalleryContainer
komponenti. Napravit ćemo niz stringova i map()
ćemo ga.
import React from 'react'
import styles from './style.module.css'
import GalleryContainer from '../../components/GalleryContainer'
const titles = ["Amazing exterior", "Relaxing sauna", "Modern interior"]
const GalleryContinent = () => (
<main className={styles.galleryContent}>
{titles.map(title => <GalleryContainer title={title} />)}
</main>
)
export default GalleryContinent
Sad dodajemo logiku za isGray
:
<main className={styles.galleryContent}>
{titles.map((title, index) =>
<GalleryContainer title={title} isGray={index % 2 === 0} />
)}
</main>
Primijetimo index
argument. To je drugi argument svakoj map
funkciji. U većini slučajeva u Reactu vam neće trebati. Ovo je jedan od onih slučajeva kad je potreban.
Uzimamo svaki neparan element da je siv koristeći modulo operator nad indexom.
Sad najteži dio. Definicija granica. Imamo 4 za prva dva i 16 za treći. Možemo reći da je svaki treći poseban. Za početak pokušajmo dobiti raspon od 4:
<main className={styles.galleryContent}>
{titles.map((title, index) =>
<GalleryContainer title={title} isGray={index % 2 === 0}
start={index * 4} end={(index + 1) * 4}/>
)}
</main>
Imamo ga. Sad trebamo special case kad je index 3. Radimo provjeru i ako je index jednak 3 uzimamo 4 * 4, ako ne onda ostaje kako je. Tako vas to vuče na ternarni operator, čestitam! :)
import React from 'react'
import styles from './style.module.css'
import GalleryContainer from '../../components/GalleryContainer'
const titles = ["Amazing exterior", "Relaxing sauna", "Modern interior"]
const GalleryContinent = () => (
<main className={styles.galleryContent}>
{titles.map((title, index) =>
<GalleryContainer title={title} isGray={index % 2 === 0}
start={index * 4}
end={(index + 1) === 3
? (index + 4) * 4
: (index + 1) * 4}
/>
)}
</main>
)
export default GalleryContinent
Možemo dodati i ovaj nepotreban commit :)
Sadržaj commitova
Ono što je bitno upamtiti iz ovih vježbi je način na koji GraphQL radi. Također, što je to CMS jer ćemo se njime detaljnjije bavit u sljedećim vježbama.