Cara Membuat Aplikasi Clone Pastebin dengan Node.js

Tags


Salah satu hal yang paling sering dilakukan oleh pengguna komputer adalah copy dan paste.

Bagaimana tidak, hampir setiap hari ada saja sesuatu yang di-copy-paste, entah itu teks, file, gambar, video, dan lain-lain.

Tapi copy dan paste tidak lengkap tanpa adanya kemampuan berbagi atau sharing.

Untuk memberi fitur tersebut, Pastebin ( https://pastebin.com/ ) menyediakan layanan untuk menyimpan dan berbagi paste.

User yang terdaftar maupun tidak bisa mengakses fitur paste yang disediakan oleh website tersebut.

Setelah user mem-paste apa yang telah dicopy-nya, user diberi URL paste miliknya yang kemudian dapat disebar luaskan.

Walaupun ide tersebut sederhana, kita bisa melihat betapa besar fungsi dari Pastebin.

Oh iya, sebelum kita lanjut, Anda boleh download source code praktikumnya di sini:


Sayangnya, website tersebut tidak mengizinkan semua konten paste.

Ada yang tidak boleh dipaste di website tersebut, seperti:
  • Daftar email
  • Detail login
  • Source code curian
  • Data hasil hack
  • Data/informasi yang dilindungi hak cipta
  • Daftar password
  • Data perbankan
  • Data personal
  • Data pornografi
  • Link spam

Seperti yang dijelaskan pada https://pastebin.com/faq#5 :


Hehehe... kalau begitu yang boleh apa dong???

Wajarlah kalau pengguna Pastebin justru cenderung melanggar kebijakan tersebut seperti yang dilansir dari bbc (http://www.bbc.com/news/technology-17524822):

Ngomong-ngomong, walaupun ide Pastebin sederhana, unique visitor Pastebin  pada tahun 2012 mencapai 17 juta lho...

Traffic yang sangat menggiurkan bukan?

Coba, bayangkan jika kita bisa membuat website seperti itu, lalu ambil potongan kuenya 1 persen saja...

Berarti sudah dapat 170 ribu unique visitor.

Apalagi kalau kita sebagai Admin mengizinkan semua konten dan tidak memerlukan login.

Oleh karena itulah, saya kali ini akan membahas cara membuat clone dari situs Pastebin dengan Node.js agar pembaca dapat membuat Pastebin-nya sendiri dengan kebijakan konten sesuka pembaca.

Aplikasi yang akan kita buat ini bernama "Node.js Pasta".

Memang tidak 100 persen sama dengan Pastebin, tapi setidaknya fungsi utamanya sama.

Harapan saya, setidaknya pembaca dapat 1 persen kue unique visitornya.

Yang Anda Butuhkan sebelum Kita Belajar

Sebelum kita mulai, pastikan Anda telah memiliki akun di Heroku.

Heroku digunakan untuk meng-host aplikasi Node.js kita secara gratis, jadi daftarkan diri Anda sebagai free account.

Aplikasi ini memang bisa berjalan di localhost, tapi untuk mengetest tampilan sharing di social media, perlu hosting Node.js di Heroku.

Apabila Anda sudah memiliki hostingan lain atau mungkin VPS, silakan digunakan, tapi caranya tidak akan dibahas di sini.

Beberapa software yang harus Anda install terlebih dahulu adalah:

  • Node.js ( https://nodejs.org/en/download/ )
  • Visual Studio Code ( https://code.visualstudio.com/Download )
  • Git For Windows( https://git-for-windows.github.io/ )
  • Heroku Client ( https://devcenter.heroku.com/articles/heroku-cli )


Setelah semua software tersebut terinstall, lakukan pengecekan terhadap software-software tersebut.

Pertama, jalankan perintah ini di command line:
node -v

Hasil yang diharapkan adalah:
v6.11.0

Kemudian jalankan perintah ini di command line:
git --version

Hasil yang diharapkan adalah:
git version 2.10.2.windows.1

Kemudian jalankan perintah ini di command line:
heroku -v

Hasil yang diharapkan adalah:
heroku-cli/6.14.13-6ab5022 (windows-x64) node-v8.4.0

Hasil-hasil tersebut tidak harus sama persis karena tergantung versi berapa yang terinstall.

Membedah Aplikasi Pastebin

Halaman Utama (New Paste)

Pastebin memiliki dua fungsi utama: menyimpan teks yang dipaste dan memberi link untuk membagikannya di tempat lain.


Yang dilakukan oleh pengguna pastebin untuk menyimpan paste adalah dengan membuka halaman berandanya, kemudian mengisi textarea "New Paste".

Selanjutnya pada bagian "Optional Paste Settings", pengguna dapat memilih syntax highlighting dari berbagai macam alternatif yang nanti akan ditampilkan pada halaman paste.

Pengguna juga dapat memilih "Paste Expiration" untuk menentukan usia paste yang dibuat.

Pilihan "Paste Expiration" yang disediakan adalah:

  • Never
  • 10 Minutes
  • 1 Hour
  • 1 Day
  • 1 Week
  • 2 Weeks
  • 1 Month
  • 6 Month
  • 1 Year


Selain itu, pada bagian "Paste Exposure", pengguna dapat memilih:

  • Public
  • Unlisted


Public berarti paste akan tampil dalam listing "Public Pastes" di bagian kanan dan di halaman "Trends".

Unlisted berarti paste tidak akan tampil di bagian manapun dan hanya bisa dilihat oleh pengguna yang mengetahui link dari paste tersebut.

Selain itu, jika pengguna adalah pro member, mereka dapat menempatkan paste pada suatu folder dengan mengisi input "Folder".

Dan option selanjutnya adalah "Title". Di sini pengguna dapat memberi nama paste yang dia buat.

Untuk membedakan member dan mengidentifikasi pro member, Pastebin juga menyediakan form login di sebelah kanan dari "Optional Paste Settings".

Halaman Trends

Pada halaman "Trends", Pastebin menampilkan trending pastes yang ditampilkan dalam bentuk tabel.


Kolom pertama adalah "Name/Title" atau judul dari paste.

Kolom kedua adalah "Added" atau kapan paste tersebut dibuat.

Kolom ketiga adalah "Hits" yang sepertinya adalah jumlah klik dari paste tersebut.

Kolom keempat adalah "User" yakni siapa yang membuat paste tersebut.

Apabila pembuat paste adalah guest atau tidak login terlebih dahulu, maka nama pembuat paste tersebut tidak ditampilkan pada kolom ini.

Halaman Search

Halaman "Search" menampilkan paste yang dicari dengan keyword tertentu.

Tidak ada yang istimewa dari halaman ini, karena halaman ini melakukan search dengan menggunakan google custom search.

Memang cara ini sangat mudah dilakukan dan hemat resource, tapi jika halaman paste tertentu belum diindex google atau jika website ini mengalami deindex, maka hasil pencarian untuk paste tersebut tidak akan muncul.


API, Tools, FAQ dan Sisanya

Pastebin juga menyediakan API untuk programmer.

Jadi programmer bisa membuat paste atau mendapat list dari paste atau untuk keperluan lain.

Sedangkan, Tools merupakan daftar aplikasi yang menggunakan API dari Pastebin. Jika kita membuatnya, kita bisa kontak Pastebin agar aplikasi kita dilisting di halaman tersebut.

FAQ cukup jelas.

Sisanya, merupakan fitur untuk pengguna yang sudah login, jadi tidak perlu dibahas.

Merancang Node.js Pasta

Setelah kita membedah Pastebin yang asli, kita bisa mengetahui beberapa fitur utama pada Pastebin.

Beberapa fitur seperti login tidak diperlukan karena kita akan membuat versi lain yang tidak membutuhkan login.

API tidak akan disertakan di sini karena tidak terlalu berguna. Mungkin kita akan membuatnya jika pengguna clone ini sudah semakin banyak.

Jadi, fitur-fitur yang akan disediakan oleh Node.js Pasta adalah:

  • Copy-paste teks.
  • Syntax highlighting. Dalam kasus ini hanya 3 jenis: Text, HTML, CSS, dan Javascript.
  • Paste Expiration. Dalam kasus ini, kita mengubah durasinya menjadi: 1 Day, 12 Hours, 3 Hours, 30 Minutes, dan 2 Minutes.
  • Paste Exposure. Dalam kasus ini pilihannya: Public dan Unlisted.
  • Paste Title.
  • Recent Pastes. Yakni daftar paste yang baru dibuat.
  • Paste Manager tanpa login.
  • Edit dan Delete Paste dari Paste Manager.
  • Paste Manager link dan Paste link generator.
  • Trending Pastes.
  • Halaman Paste.
  • Halaman 404 Not Found. Berguna jika paste yang dibuat sudah expired atau jika pengguna salah memasukkan URL.
  • Paste sharing ke social media: Facebook, Twitter, dan Google Plus.


Rancangan User Interface

Node.js Pasta memiliki desain user interface yang sederhana.

Terdiri atas empat halaman, yakni "Home/New Paste", "Paste Manager", "Paste", dan "Trends".

Tidak ada CSS yang rumit karena kita akan menggunakan bootstrap.

Home/New Paste:


Paste Manager:


Paste:


Trends:

Package NPM yang Dibutuhkan

Untuk mengerjakan aplikasi Node.js Pasta, diperlukan beberapa modul NPM ini.

Body Parser

install: npm install body-parser --save

Module ini berfungsi untuk memparsing request body.

Expressjs

install: npm install express --save

Module ini berfungsi sebagai framework untuk routing dan sebagai wrapper untuk menjalankan fungsi Node.js sebagai web server.

EJS

install: npm install ejs --save

Module ini berfungsi sebagai template engine untuk Expressjs. Dengan module ini kita bisa menyajikan html sebagai template dan memanipulasi nilai tertentu di dalamnya secara dinamis.

Randomstring

install: npm install randomstring --save

Module ini berfungsi untuk men-generate string acak dengan parameter-parameter tertentu.

SQLite3

install: npm install sqlite3 --save

Module ini digunakan sebagai DBMS. Keuntungan menggunakan module ini adalah, kita tidak memerlukan server khusus untuk database karena hanya berupa file. Apabila Anda ingin menggantinya menjadi MySQL silakan dicoba sendiri karena tidak dibahas di sini.

Package Non-NPM yang Dibutuhkan

Selain package NPM, dibutuhkan juga package non NPM.

Sebenarnya package-package ini juga tersedia di npmjs.com, tetapi menurut saya lebih mudah menggunakannya tanpa NPM.

JQuery

Package ini adalah library javascript front-end yang berguna untuk memanipulasi HTML dan event-event-nya. Selain itu, Bootstrap juga memerlukan library ini.

Untuk mendapatkannya, download versi terbarunya di:

https://jquery.com/download/

Bootstrap

Package ini adalah library javascript dan CSS yang menyediakan berbagai elemen user interface yang cantik. Semua elemen tersebut didesain agar bersifat responsive.

Untuk mendapatkannya, download versi 3-nya di:

https://getbootstrap.com/docs/3.3/getting-started/#download

Ace

Package ini adalah library javascript untuk membuat web based code editor.

Sebenarnya kita tidak membutuhkan fungsinya sebagai code editor, tetapi hanya mengambil fungsi syntax highlightingnya saja.

Bahasa pemrograman yang didukung syntax highlightingnya sangat banyak, namun kita hanya akan mengambil empat saja: Text, HTML, CSS, dan Javascript.

Untuk mendapatkannya, download versi terbarunya di:

https://github.com/ajaxorg/ace-builds

Font Awesome

Font Awesome adalah library yang berisi icon-icon yang sangat banyak. Icon-icon ini adalah font yang berupa vector dan scalable, sehingga tidak akan pecah gambarnya walaupun dalam ukuran berapapun.

Untuk mendapatkannya, download versi terbarunya di:

http://fontawesome.io/

Memprogram Node.js Pasta

Sekarang kita telah mengetahui rancangan aplikasi Node.js Pasta dan telah mendapatkan package-package yang dibutuhkannya.

Saatnya mulai memprogram.

Tapi sebelumnya, kita buat dahulu struktur direktori project ini agar lebih rapi.

Dan ada satu hal lagi yang perlu saya sampaikan.

Jika Anda ragu apakah kode yang Anda tulis dan penempatannya sudah benar, silakan cek dan samakan dengan kode yang ada pada source code buku ini.

Struktur Direktori Project

Pertama-tama, buatlah folder bernama "ls-node-js-pasta".

Selanjutnya buatlah folder-folder lain di dalamnya sedemikian rupa sehingga strukturnya seperti gambar di bawah ini.


Anda boleh melewatkan folder "node_modules" karena ini akan dibuat secara otomatis ketika Anda menginstall module melalui NPM.

Jika Anda tidak yakin dengan struktur direktori yang Anda buat, silakan cek source code buku ini (di dalam folder "ls-node-js-pasta").

Menempatkan Package-Package Non NPM di Direktori Project

Selanjutnya tempatkanlah package-package non NPM yang telah Anda download ke dalam direktori project sesuai dengan jenis filenya.

File Javascript (extension: .js) dimasukkan ke folder "js".

File Gambar dimasukkan ke folder "img".

File Font (extension: .otf, .eot, .svg, .ttf, .woff, .woff2) dimasukkan ke folder "fonts"

File CSS (extension: .css) dimasukkan ke folder "css".

Khusus untuk gambar, silakan copy dari source code buku ini, karena file gambar tersebut tidak ada di internet.

Anda boleh menggunakan gambar Anda sendiri, asalkan resolusinya sama dengan milik saya.

Selanjutnya, kembali ke folder "ls-node-js-pasta/views".

Di sini, buatlah file kosong bernama:

  • functions.ejs
  • 404.ejs
  • home.ejs
  • manage.ejs
  • paste.ejs
  • trends.ejs


Selanjutnya buka folder "ls-node-js-pasta" dan buat file kosong bernama "index.js".

Jika Anda masih tidak yakin apakah penempatannya benar, silakan dicek di source code buku ini.

Menginstall Package NPM di Direktori Project

Sekarang kita sudah menempatkan package non NPM di folder yang benar.

Saatnya menginstall package NPM-nya.

Masih di folder "ls-node-js-pasta", jalankan perintah ini:
npm init
Selanjutnya, isi saja sesuai yang ditawarkan hingga jawaban terakhir (yes/no).

Kemudian, ketikkan perintah ini:
npm install body-parser --save
npm install express --save
npm install ejs --save
npm install randomstring --save
npm install sqlite3 --save

Dari langkah tersebut, akan dibuat file baru secara otomatis bernama package.json.

Pastikan isi filenya sama dengan ini:

{
  "name": "ls-node-js-pasta",
  "version": "1.0.0",
  "description": "Node.js Pasta adalah Node.js Pastebin Clone",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Lusfikar Sheba",
  "license": "UNLICENSED",
  "dependencies": {
    "body-parser": "^1.17.2",
    "ejs": "^2.5.7",
    "express": "^4.15.4",
    "randomstring": "^1.1.5",
    "sqlite3": "^3.1.9"
  }
}

Perhatikan pada bagian yang dicetak tebal.

Pastikan ada bagian tersebut.

Nama author bisa beda, tergantung apa yang kita isi saat melakukan npm init. Begitu pula license.

Mengimpor Package NPM

Setelah package NPM terinstall, kita bisa mengimpornya di dalam script.

Sekarang buka file "index.js' pada folder "ls-node-js-pasta".

Saat ini file tersebut masih kosong.

Isi file tersebut dengan kode ini:

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var randomstring = require("randomstring");
var sqlite3 = require('sqlite3').verbose();
var fs = require('fs');

Di sinilah kita mengimpor package-package NPM agar nanti bisa digunakan.

Perhatikan pada bagian:

var app = express();

Mengapa di bagian tersebut ada fungsi express()?

Padahal yang lain tidak ada hal semacam itu.

Hal itu disebabkan pada saat mengimport express di bagian:

var express = require('express');

Yang diimport merupakan fungsi, karena itu harus dipanggil dengan express().

Mempersiapkan Framework Expressjs

Setelah mengimpor package-package NPM ke script "index.js", kita akan mempersiapkan framework Expressjs.

Disini kita akan membuat kerangka routes dan mengonfigurasi Expressjs agar bisa berjalan semestinya.

Buka file "index.js" kemudian di bawah kode impor NPM ini:

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var randomstring = require("randomstring");
var sqlite3 = require('sqlite3').verbose();
var fs = require('fs');

, tambahkan kode ini:

app.set('view engine', 'ejs'); //YANG INI - 1
app.set('view options', { //YANG INI - 2
    rmWhitespace: true
});

app.use('/views', express.static(__dirname + '/views/')); //YANG INI - 3
app.use(bodyParser.urlencoded({ extended: true })); //YANG INI - 4

app.get('/', function (req, res) { //YANG INI - 5

});

app.get('/trends', function (req, res) {

});

app.post('/new-paste', function(req, res) {

});

app.post('/manage/save-paste/:manageID', function(req, res) { //YANG INI - 6

});

app.post('/manage/delete-paste/:manageID', function(req, res) {

});

app.get('/manage/:manageID', function (req, res) {

})

app.get('/paste/:pasteID', function (req, res) {

})

app.get('*', function(req, res){ //YANG INI - 7

});

app.set('port', (process.env.PORT || 5000)); //YANG INI - 8

app.listen(app.get('port'), function () { //YANG INI - 9
    console.log('Node app is running on port', app.get('port'));
});

Perhatikan kode bagian "//YANG INI - 1".

Di sana, kita menge-set EJS sebagai view engine.

Artinya setelah kita mengimpor EJS, kita harus memberitahu Expressjs bahwa kita mengguakan template engine tersebut.

Pada kode bagian "//YANG INI - 2", kita mengonfigurasi EJS agar menghapus whitespace.

Bisa dilihat di dokumentasinya pada ( https://github.com/mde/ejs#options )

Jika hal itu tidak dilakukan, saat kita membuat paste akan ada tambahan whitespace yang tidak diinginkan.

Pada kode bagian "//YANG INI - 3", kita memberi akses pada Expressjs agar dapat merespon file statis.

Yang dimaksud file statis di sini adalah file CSS, HTML, Gambar, Javascript, dan file apapun yang terletak pada direktori yang didaftarkan pada fungsi:

app.use('/views', express.static(__dirname + '/views/'));

Jadi, apabila kita tidak melakukan ini, request:

http://nama_host/views/js/jquery.min.js

Tidak sah dan file "jquery.min.js" tersebut tidak bisa diakses.

Agar bisa diakses, kita harus melakukan "//YANG INI - 3".

Pada kode bagian "//YANG INI - 4":

app.use(bodyParser.urlencoded({ extended: true }));

Kita memberitahu Expressjs untuk menggunakan module Body Parser untuk mem-parse application/x-www-form-urlencoded.

Jadi apabila kita memiliki form pada sebuah file HTML, dan pada form tersebut ada input dengan atribut name="inputpertama", seperti ini:

<input type="text" name="inputpertama">

Maka di script servernya nilai input tadi bisa diakses dengan:

app.post('/getinput', function(req, res) {
 console.log(req.body.inputpertama );//bisa begini...
});

Tidak hanya untuk merequest "/getinput" saja, bisa juga "/" atau "/apapun" tergantung apa yang ditulis pada argument app.post.

Anda bisa lihat pada source code buku ini bahwa terdapat beberapa penggunaan Body Parser semacam ini.


Sekarang perhatikan pada bagian "//YANG INI - 5":

app.get('/', function (req, res) {

});

Dengan kode ini, kita akan merespon request "/" atau yang biasa disebut Home.

Request semacam ini bisa juga disertai dengan variabel seperti pada bagian "//YANG INI - 6".

app.post('/manage/save-paste/:manageID', function(req, res) {

});

Perhatikan bahwa variabel diletakkan setelah "/manage/save-paste", yakni ":manageID".

Tanda titik dua pada routing di Expressjs menandakan bahwa itu adalah variabel.

Dengan cara tersebut, kita bisa mengakses:

http://nama_host/manage/save-paste/123

http://nama_host/manage/save-paste/345

http://nama_host/manage/save-paste/atauapapunjuga

Sekarang perhatikan pada kode bagian "//YANG INI - 7":

app.get('*', function(req, res){

});

Di situ ada karakter asterisk ('*').

Itu menandakan bahwa request apapun yang tidak terdaftar akan diarahkan ke situ.

Routes semacam ini berguna untuk mengarahkan halaman yang tidak ditemukan (404) ke halaman 404 yang telah disiapkan.

Pada kode bagian "//YANG INI - 8":

app.set('port', (process.env.PORT || 5000));

Kita memberitahu Expressjs bahwa server akan berjalan pada port 5000 atau pada port yang didefinisikan oleh environment variable (ditandai dengan process.env.PORT).

Hal ini berguna saat kita men-deploy aplikasi ini ke Heroku karena Heroku sendiri yang menentukan port berapa untuk aplikasi kita walaupun setelah di-deploy semua aplikasi Heroku berjalan pada port 443 secara default.

Hal tersebut disebabkan (sepertinya karena) Heroku menggunakan semacam proxy untuk mengarahkan subdomain kita di port 443 ke port yang asli di network internal mereka.

URL dengan port yang asli tadi tidak bisa diakses secara langsung dari internet.

Dan akhirnya, pada kode bagian "//YANG INI - 9":

app.listen(app.get('port'), function () {
    console.log('Node app is running on port', app.get('port'));
});

Kita memerintahkan Express.js untuk menjalankan server pada port tadi.

Membuat Database dengan SQLite3

Karena aplikasi ini bisa menyimpan paste, maka diperlukan sebuah DBMS.

Mengingat akun Heroku yang free tidak menyediakan fitur MySQL, maka saya memilih SQLite3 sebagai DBMS.

SQLite3 dapat digunakan di Heroku karena hanya berupa file dan tidak butuh server yang terpisah.

Memang ada kekurangannya, tapi untuk skala kecil SQLite3 masih dapat digunakan.

Sekarang buka kembali file "index.js".

Di bawah script untuk import module:

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var randomstring = require("randomstring");
var sqlite3 = require('sqlite3').verbose();
var fs = require('fs');

Tulis kode ini:

var db;

db = new sqlite3.Database('./db_pasta.db');//YANG INI - 1
db.run(//YANG INI - 2
`CREATE TABLE IF NOT EXISTS pastes (
    id INTEGER PRIMARY KEY,
    newpaste TEXT,
    syntaxhi TEXT,
    pasteex TEXT,
    pasteexpos TEXT,
    title TEXT,
    pasteperm TEXT,
    pastemanagerperm TEXT,
    hits INTEGER
);`, function(){
    db.each("SELECT * from pastes", function(err, row) {//YANG INI - 3
        console.log("scheduling the timeout for: " + row.title);
        setTimeout(function(){//YANG INI - 4
            console.log("deleting the row automatically...");
            db.run("DELETE FROM pastes WHERE id = '" + row.id + "'");
            console.log("deleted the row automatically...");
        }, textToMS(row.pasteex));
    });
}); 

Bila Anda tidak yakin dengan penempatannya, silakan cek file "index.js" pada source code buku ini.

Bila kita perhatikan pada script di atas, database aplikasi ini sangat sederhana.

Hanya memiliki satu tabel.

Memang demikian. Karena saya berusaha menyederhanakan clone Pastebin agar mudah dipelajari.

Jika Anda memiliki versi sendiri yang lebih kompleks tidak masalah. Anda tinggal kembangkan sendiri aplikasi ini.

Sekarang perhatikan pada kode bagian "//YANG INI - 1".

Di sana kita membuat database baru atau meng-overwrite database lama dengan nama " db_pastebin_clone.db".

File database itu berada di folder "ls-node-js-pasta".

Selanjutnya, pada kode bagian "//YANG INI - 2", kita menjalankan query:

CREATE TABLE IF NOT EXISTS pastes (
    id INTEGER PRIMARY KEY,
    newpaste TEXT,
    syntaxhi TEXT,
    pasteex TEXT,
    pasteexpos TEXT,
    title TEXT,
    pasteperm TEXT,
    pastemanagerperm TEXT,
    hits INTEGER
);

Kolom "id" adalah id dari row. Di sana tertulis "INTEGER PRIMARY KEY".

Hal itu dilakukan agar "id" bersifat auto increment.

Kolom "newpaste" menyimpan teks yang kita paste pada aplikasi ini.

Kolom "syntaxhi" menyimpan setting syntax highlighting apa yang kita gunakan untuk paste tersebut.

Kolom "pasteex" menyimpan setting mengenai berapa lama paste disimpan dalam database hingga expired dan dihapus secara otomatis.

Kolom "pasteexpos" menyimpan setting mengenai apakah paste ini bersifat public atau unlisted.

Kolom "title" adalah judul dari paste.

Kolom "pasteperm" adalah permalink dari paste yang bisa dishare dan dilihat publik.

Kolom "pastemanagerperm" adalah permalink dari paste manager yang hanya ditujukan untuk pembuat paste. Ini sebaiknya tidak dibagikan pada orang lain. Selain itu, tidak ada cara untuk menemukan permalink ini jika pembuat paste lupa.

Kolom "hits" mencatat total kunjungan pada paste. Bukan paste manager.

Nilai "hits" akan bertambah seiring bertambahnya kunjungan pada paste.

Nilai itu juga menjadi faktor penentu urutan listing dari paste-paste pada halaman "Trends".

Pada fungsi db.run tadi, kita juga memberikan callback:

db.each("SELECT * from pastes", function(err, row) {//YANG INI - 3
    console.log("scheduling the timeout for: " + row.title);
    setTimeout(function(){//YANG INI - 4
        console.log("deleting the row automatically...");
        db.run("DELETE FROM pastes WHERE id = '" + row.id + "'");
        console.log("deleted the row automatically...");
    }, textToMS(row.pasteex));
});

Pada kode bagian "//YANG INI - 3", kita menjelajahi tiap row dari tabel paste dan setiap satu row ditemukan, maka callback ini dijalankan:

console.log("scheduling the timeout for: " + row.title);
setTimeout(function(){ //YANG INI - 4
    console.log("deleting the row automatically...");
    db.run("DELETE FROM pastes WHERE id = '" + row.id + "'");
    console.log("deleted the row automatically...");
}, textToMS(row.pasteex));

Di sini kita akan menjadwalkan penghapusan setiap row dari tabel paste selama:

textToMS(row.pasteex)

Jadi setTimeout tadi akan selama itu.

Kita menggunakan textToMS karena kita akan mengonversi 1 Day, 12 Hours ... 2 Minutes menjadi jumlah milisecond dalam integer.

Kita melakukan ini pada saat aplikasi ini baru dijalankan karena boleh jadi ketika aplikasi ini berhenti entah karena crash atau faktor lain, masih ada paste yang belum dihapus karena belum memenuhi durasi lifetime-nya.

Sebenarnya cara ini kurang akurat, tetapi setidaknya memberi ide mengenai bagaimana cara membuat expirable content pada sebuah aplikasi web.

Membuat Fungsi-Fungsi Utility

Setelah kita membuat database untuk Node.js Pasta, kita akan membuat fungsi-fungsi utility-nya.

Fungsi-fungsi yang akan kita buat ini berguna untuk menyederhanakan penulisan kode sehingga tidak banyak copy paste dari satu baris ke baris yang lain.

Adapun fungsi-fungsi utility yang akan kita buat adalah:

  • textToMS
  • getBaseURL
  • getTheExcerpt


Fungsi textToMS dan getBaseURL ditulis di file "index.js" sedangkan getTheExcerpt akan ditulis pada template "functions.ejs".

Fungsi getTheExcerpt ditulis di file template karena ini merupakan utility untuk keperluan template rendering.

Fungsi textToMS

Pertama-tama, buka file "index.js", kemudian scroll hingga bagian terbawah.

Selanjutnya tulis kode ini:

function textToMS(textTime){
    if(textTime == "1 Day"){
        return 24 * 60 * 60 * 1000;
    } else if(textTime == "12 Hours") {
        return 12 * 60 * 60 * 1000;
    } else if(textTime == "3 Hours") {
        return 3 * 60 * 60 * 1000;
    } else if(textTime == "30 Minutes") {
        return 30 * 60 * 1000;
    } else if(textTime == "2 Minutes") {
        return 2 * 60 * 1000;
    }
}

Fungsi di atas digunakan untuk mengonversi teks "1 Day, 12 Hours, 3 Hours, 30 Minutes, dan 2 Minutes" menjadi integer milisecond.

Hal ini dilakukan karena pada HTML, nilai ini disajikan dalam sebuah dropdown yang value-nya berupa string.

Mengenai rumusnya cukup jelas. 1 Day misalnya, adalah 24 jam x 60 menit x 60 detik x 1000 milidetik.

Sisanya tidak jauh beda. Silakan tafsirkan sendiri.

Fungsi getBaseURL

Sekarang, di bawah fungsi textToMS pada file "index.js", tulis kode ini:

function getBaseURL(req){
    var port = app.get('port');
    var trailing = port == 80 || port == 443 ? "": (":" + port);
    return (req.secure?'https://':'http://') + req.hostname + trailing;

    //Jika Anda men-deploy aplikasi ini di heroku, replace kode
    //di atas dengan ini:
    //return "https://namasubdomainanda.herokuapp.com";
    //misalnya (yang punya saya):
    //return "https://ls-node-js-pasta.herokuapp.com";
}

Fungsi ini digunakan untuk mendapatkan base URL dari aplikasi ini.

Fungsi ini membutuhkan argument "req" yang didapatkan pada saat routing, misalnya:

app.get('/manage/:manageID', function (req, res) {
 console.log(getBaseURL(req));
})

Tetapi itu tidak mutlak diperlukan jika Anda mendeploy-nya ke Heroku seperti yang dijelaskan pada komentar di fungsi tersebut:

//Jika Anda men-deploy aplikasi ini di heroku, replace kode
//di atas dengan ini:
//return "https://namasubdomainanda.herokuapp.com";
//misalnya (yang punya saya):
//return "https://ls-node-js-pasta.herokuapp.com";

Jika Anda menjalankan aplikasi ini di localhost, Anda tidak perlu mengikuti komentar itu.

Saya memilih cara manual untuk mendapatkan base URL di Heroku, karena cara ini adalah yang paling mudah mengingat Heroku sepertinya menggunakan proxy untuk mengakses server yang sebenarnya.

Fungsi getBaseURL ini akan diperlukan ketika kita membuka halaman "Paste Manager" di mana di sana aplikasi ini akan meng-generate URL untuk mengakses halaman "Paste Manager" dan halaman "Paste" yang telah dibahas pada subbab "Rancangan User Interface".


Fungsi getTheExcerpt

Kali fungsi yang agak spesial.

Fungsi ini ditulis di file template (yang berekstensi .ejs).

Fungsi ini ditulis di file tersebut karena ini merupakan fungsi utility untuk template rendering.

Sekarang, buka file "functions.ejs", lalu tulis kode ini:

<%
getTheExcerpt = function(wholeText){
    var strLen = wholeText.length;
    if(strLen < 100){
        return wholeText.substring(0, strLen);
    }
    return wholeText.substring(0, 100);
}
%>

Yang dilakukan oleh fungsi ini adalah mengambil potongan teks dari sebuah teks penuh.

Tadi kita lihat pada bagian database bahwa kolom "newpaste" dari tabel "pastes" menyimpan teks penuh dari paste yang kita buat.

Padahal pada halaman trends kita hanya memerlukan potongan kecil dari teks tersebut.

Oleh karena itulah kita menggunakan fungsi ini, sehingga yang tampil pada row di halaman "Trends" adalah hanya potongannya seperti ini:


Membuat Halaman 404 Not Found

Sekarang kita telah menyelesaikan kira-kira 50 persen dari seluruh script dalam project ini.

Mungkin Anda masih ingat routing di file "index.js" yang masih belum diisi dengan response.

Mulai sekarang kita akan mengisinya sambil membuat user interfacenya dalam HTML, CSS, dan Javascript.

Perlu saya ingatkan bahwa mulai saat ini, sering-seringlah mengecek kesesuaian kode di buku ini dengan yang ada dalam source code buku ini.

Pertama-tama, buka file "404.ejs" dalam folder "ls-node-js-pasta/views".

File ini masih kosong, tetapi pada folder yang sama di dalam SOURCE CODE BUKU INI sudah ada isinya.
Langsung copy-paste saja dari source code tadi ke file "404.ejs" yang masih kosong ini.

Isinya kira-kira begini:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>404: ERROR NOT FOUND - Node.js Pasta</title>

        <meta name="description" content="Node.js Pasta - Content Not Found">
        <meta name="author" content="Lusfikar Sheba">
   
        <!-- YANG INI - 1 -->
        <link rel="shortcut icon" type="image/png" href="/views/img/favicon.png"/>
        <link href="/views/css/bootstrap.min.css" rel="stylesheet">
        <link href="/views/css/bootstrap-theme.min.css" rel="stylesheet">
        <script src="/views/js/jquery-3.2.1.min.js"></script>
        <script src="/views/js/bootstrap.min.js"></script>
    </head>
<body>

<div class="container">
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Node.js Pasta</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/trends">Trends</a></li>
                    <li><div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/"><span class="glyphicon glyphicon-plus-sign"></span> New Paste</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="jumbotron">
                <h1>404: ERROR NOT FOUND</h1>
                <p>not found not found not found not found not found not found not found not found not found .</p>
                <p>not found not found not found not found not found not found not found not found not found .</p>
                <p>not found not found not found not found not found not found not found not found not found .</p>
            </div>
        </div>
    </div>
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <p class="navbar-text pull-right">Copyright &#169; Lusfikar Sheba 2017</p>
        </div>
    </nav>
</body>
</html>

Perhatikan pada kode bagian "//YANG INI - 1".

Di situ kita mengimpor package-package non NPM, yaitu:

  • Bootstrap
  • JQuery


Di situ kita juga mendaftarkan file gambar "favicon.png" pada folder "ls-node-js-pasta/views/img/" agar muncul di tab di browser.

Halaman ini akan muncul apabila user mengakses URL yang salah.

Agar bisa seperti itu, buka file "index.js" versi Anda (bukan yang dari source code buku ini), kemudian fokuskan perhatian Anda pada route ini:

app.get('*', function(req, res){

});

Kemudian tambahkan kode-kode ini di dalam route tersebut:

res.status(404);
res.render('404');

Sehingga menjadi:

app.get('*', function(req, res){
    res.status(404);
    res.render('404');
});

Dengan demikian, URL apapun yang tidak terdaftar di route akan diarahkan ke halaman "404".
Fungsi res.status adalah HTTP Status Code yang diberikan pada route tersebut.

Daftar lengkapnya ada di:

https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html

Sedangkan fungsi res.render digunakan untuk menampilkan template "404.ejs" sebagai HTML di sisi browser pengguna.

Jadi di sini tidak perlu ditulis lengkap dengan ekstensinya (".ejs").

Sekarang Anda bisa coba jalankan aplikasi ini.

Jalankan perintah ini di command line (dengan titik di sebelah node):

node .

Kemudian buka browser anda ke:

http://localhost:5000

Maka nanti halaman 404 akan ditampilkan.

Membuat Halaman Home (New Paste)

Setelah membuat halaman 404, sekarang kita membuat halaman "Home".

Pada halaman ini, kita akan menampilkan form untuk membuat halaman pembuatan paste.

Buka file kosong "home.ejs" yang telah kita buat sebelumnya.

Halaman ini masih kosong, maka copy dari source code buku ini dari file yang sama (ada di folder views).

Maka isinya akan seperti ini:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Node.js Pasta</title>

        <meta name="description" content="Node.js Pasta. Share your pastes here!">
        <meta name="author" content="Lusfikar Sheba">

        <!-- [ Social Media meta tag ] -->
        <meta content='summary' name='twitter:card'/>
        <meta content='@ciyy_space' name='twitter:site'/>
        <meta content='@ciyy_space' name='twitter:creator'/>

        <meta content='<%=hostname%>' property='og:url'/>
        <meta content='Node.js Pasta' property='og:site_name'/>
        <meta content='Node.js Pasta' property='og:title'/>
        <meta content='Node.js Pasta' name='twitter:title'/>
        <meta content='website' property='og:type'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>
        <meta content='Node.js Pasta. Share your pastes here!' property='og:description'/>
        <meta content='Node.js Pasta. Share your pastes here!' name='twitter:description'/>
        <meta content='en_US' property='og:locale'/>
        <!-- [/ Social Media meta tag ] -->

        <link rel="shortcut icon" type="image/png" href="/views/img/favicon.png"/>
        <link href="/views/css/bootstrap.min.css" rel="stylesheet">
        <link href="/views/css/bootstrap-theme.min.css" rel="stylesheet">
        <script src="/views/js/jquery-3.2.1.min.js"></script>
        <script src="/views/js/bootstrap.min.js"></script>
    </head>
<body>
<% include ./functions %>
<div class="container">
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Node.js Pasta</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/trends">Trends</a></li>
                    <li><div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/"><span class="glyphicon glyphicon-plus-sign"></span> New Paste</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="jumbotron">
                <h1>Apa itu Node.js Pasta?</h1>
                <p>Node.js Pasta adalah alat untuk berbagi teks yang di-paste-kan ke website ini.</p>
                <p>Anda bisa membuat, mengubah, dan menghapus paste Anda TANPA LOGIN di sini!</p>
                <p>Jika berminat dengan website semacam ini, <a href="https://projects.co.id/public/browse_users/view/99bc11/ciyyspace" target="_blank" rel="nofollow">pekerjakan saya di sini</a> atau <a href="javascript:void(0)" target="_blank" rel="nofollow">beli bukunya di sini</a>.</p>
            </div>
        </div>
    </div>

    <form action="/new-paste" method="post">
        <div class="row">
            <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>New Paste</b></h3>
                    </div>
                    <div class="panel-body">
                        <textarea id="pc-ta-newpaste" name="pcTaNewpaste" class="form-control" rows="10"></textarea>
                    </div>
                </div>
            </div>
        </div>

        <div class="row">
            <div class= "col-lg-6 col-md-6 col-sm-6 col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>Settings</b></h3>
                    </div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="pc-sl-syntaxhi">Syntax Highlighting:</label>
                            <select id="pc-sl-syntaxhi" name="pcSlSyntaxhi" class="form-control">
                                <%
                                var options = [ "text", "html", "css", "javascript" ];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    %><option value="<%=options[i] %>"><%=options[i] %></option><%
                                }
                                %>
                            </select> 
                              
                            <label for="pc-sl-pasteex">Paste Expiration:</label>
                            <select id="pc-sl-pasteex" name="pcSlPasteex" class="form-control">
                                <%
                                var options = [ "1 Day", "12 Hours", "3 Hours", "30 Minutes", "2 Minutes"];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    %><option value="<%=options[i] %>"><%=options[i] %></option><%
                                }
                                %>
                            </select>

                            <label for="pc-sl-pasteexpos">Paste Exposure:</label>
                            <select id="pc-sl-pasteexpos" name="pcSlPasteexpos" class="form-control">
                                <%
                                var options = [ "Public", "Unlisted" ];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    %><option value="<%=options[i] %>"><%=options[i] %></option><%
                                }
                                %>
                            </select>

                            <label for="pc-tx-title">Title:</label>
                            <input id="pc-tx-title" type="text" name="pcTxTitle" class="form-control">
                            <br/>
                            <input id="pc-sb-newpaste" name="pcSbNewpaste" class="btn btn-danger btn-block" type="submit" value="Create New Paste" />
                        </div>
                    </div>
                </div>
            </div>

            <div class= "col-lg-6 col-md-6 col-sm-6 col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>Recent Pastes</b></h3>
                    </div>
                    <div class="panel-body" style="min-height: 336px; max-height: 336px; overflow-y: scroll;">
                        <div class="list-group">
                            <% if (locals.therows) { %>
                                <% therows.forEach(function(singleRow) { %>
                                    <a href="<%= hostname %>/paste/<%= singleRow.pasteperm %>" target="_blank" class="list-group-item">
                                        <h4 class="list-group-item-heading"><%= singleRow.title %></h4>
                                        <p class="list-group-item-text"><i>Expired in <%= singleRow.pasteex %>, <%= singleRow.hits %> Hit(s)</i></p>
                                        <p class="list-group-item-text"><%= getTheExcerpt(singleRow.newpaste) %></p>
                                    </a>
                                <% }); %>
                            <% } %>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </form>
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <p class="navbar-text pull-right">Copyright &#169; Lusfikar Sheba 2017</p>
        </div>
    </nav>
</body>
</html>

Di sana ada beberapa baris kode yang bentuknya seperti ini:

<select id="pc-sl-syntaxhi" name="pcSlSyntaxhi" class="form-control">
          <%
          var options = [ "text", "html", "css", "javascript" ];
          for ( var i = 0; i < options.length; i++ )
         {
                %><option value="<%=options[i] %>"><%=options[i] %></option><%
         }
         %>
 </select> 

Kode tersebut tugasnya adalah menampilkan item dari sebuah dropdown.

Yang barusan itu untuk dropdown jenis syntax highlighting yang pilihannya "text", "html", "css", dan "javascript".

Nanti string-string tersebut akan diinjeksikan ke variabel javascript pada halaman "Paste" setelah disimpan dalam database.

Sekarang di sisi server, ingat kembali tentang route ini:

app.get('/', function (req, res) { //YANG INI - 5

});

Ubah jadi:

app.get('/', function (req, res) {
    db.all("SELECT * FROM pastes WHERE pasteexpos='Public' LIMIT 20", function(err, rows) {
        res.render('home', { 
            therows : rows, //YANG INI - 1
            hostname: getBaseURL(req) //YANG INI - 2
        });
    });
});

Di sini ketika user mengakses halaman "Home", maka server akan menarik data dari database.

Data yang diambil adalah:

SELECT * FROM pastes WHERE pasteexpos='Public' LIMIT 20

Yang artinya hanya semua kolom dari tabel pastes yang paste exposurenya "Public" dan batasi hanya 20 row.

Di sini kita menggunakan db.all jadi yang diterima pada rows di agrument callbacknya adalah semua hasil dari query di atas. Bukan satu per satu seperti db.each.

Selanjutnya saat query tersebut selesai dieksekusi, callback dipanggil dan pada akhirnya server merender halaman "Home" dengan menginjeksikan rows sebagai therows (//YANG INI - 1) dan base URL sebagai hostname (//YANG INI - 2).

Maka nilai ini pada file "home.ejs":

<meta content='<%=hostname%>' property='og:url'/>

Akan di-replace dengan hostname dari injeksi di server tadi.

Sekarang anda bisa coba jalankan aplikasi ini:

node .

Dan buka browser ke:

http://localhost:5000

Nanti halaman 404 akan digantikan halaman "Home" lengkap dengan form-nya.

Membuat Halaman Manage

Setelah selesai membuat halaman "Home" dan route-nya, kita akan membuat halaman "Manage".

Di halaman ini, user akan bisa menampilkan paste yang telah dibuat di halaman "Home" dan mengedit atau menghapusnya.

Di sini juga akan di-generate URL manager dan URL paste

Pertama-tama bukalah file "manage.ejs" dari folder "ls-node-js-pasta/views".

Kira-kira isinya seperti ini:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Manage <%= therows[0].title %> - Node.js Pasta</title>

        <meta name="description" content="Manage <%= therows[0].title %> with Node.js Pasta">
        <meta name="author" content="Lusfikar Sheba">

        <!-- [ Social Media meta tag ] -->
        <meta content='summary' name='twitter:card'/>
        <meta content='@ciyy_space' name='twitter:site'/>
        <meta content='@ciyy_space' name='twitter:creator'/>

        <meta content='<%=hostname%>/manage/<%=therows[0].pastemanagerperm %>' property='og:url'/>
        <meta content='Manage <%= therows[0].title %> - Node.js Pasta' property='og:site_name'/>
        <meta content='Manage <%= therows[0].title %> - Node.js Pasta' property='og:title'/>
        <meta content='Manage <%= therows[0].title %> - Node.js Pasta' name='twitter:title'/>
        <meta content='article' property='og:type'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>
        <meta content='Manage <%= therows[0].title %> with Node.js Pasta' property='og:description'/>
        <meta content='Manage <%= therows[0].title %> with Node.js Pasta' name='twitter:description'/>
        <meta content='en_US' property='og:locale'/>
        <!-- [/ Social Media meta tag ] -->

        <link rel="shortcut icon" type="image/png" href="/views/img/favicon.png"/>
        <link href="/views/css/bootstrap.min.css" rel="stylesheet">
        <link href="/views/css/bootstrap-theme.min.css" rel="stylesheet">
        <script src="/views/js/jquery-3.2.1.min.js"></script>
        <script src="/views/js/bootstrap.min.js"></script>
    </head>
<body>

<div class="container">
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Node.js Pasta</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/trends">Trends</a></li>
                    <li><div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/"><span class="glyphicon glyphicon-plus-sign"></span> New Paste</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <form method="post">
        <div class="row">
            <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>This Paste</b></h3>
                    </div>
                    <div class="panel-body">
                        <textarea id="pc-ta-newpaste" name="pcTaNewpaste" class="form-control" rows="10">
                            <%= therows[0].newpaste %>
                        </textarea>
                    </div>
                </div>
            </div>
        </div>

        <div class="row">
            <div class= "col-lg-6 col-md-6 col-sm-6 col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>Settings</b></h3>
                    </div>
                    <div class="panel-body">
                        <div class="form-group">
                            <label for="pc-sl-syntaxhi">Syntax Highlighting:</label>
                            <select id="pc-sl-syntaxhi" name="pcSlSyntaxhi" class="form-control">
                                <%
                                var options = [ "text", "html", "css", "javascript" ];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    var selected = ( therows[0].syntaxhi == options[i] ) ? "selected" : "";
                                    %><option value="<%=options[i] %>" <%=selected %>><%=options[i] %></option><%
                                }
                                %>
                            </select> 
                              
                            <label for="pc-sl-pasteex">Paste Expiration:</label>
                            <select id="pc-sl-pasteex" name="pcSlPasteex" class="form-control">
                                <%
                                var options = [ "1 Day", "12 Hours", "3 Hours", "30 Minutes", "2 Minutes"];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    var selected = ( therows[0].pasteex == options[i] ) ? "selected" : "";
                                    %><option value="<%=options[i] %>" <%=selected %>><%=options[i] %></option><%
                                }
                                %>
                            </select>

                            <label for="pc-sl-pasteexpos">Paste Exposure:</label>
                            <select id="pc-sl-pasteexpos" name="pcSlPasteexpos" class="form-control">
                                <%
                                var options = [ "Public", "Unlisted" ];
                                for ( var i = 0; i < options.length; i++ )
                                {
                                    var selected = ( therows[0].pasteexpos == options[i] ) ? "selected" : "";
                                    %><option value="<%=options[i] %>" <%=selected %>><%=options[i] %></option><%
                                }
                                %>
                            </select>

                            <label for="pc-tx-title">Title:</label>
                            <input id="pc-tx-title" type="text" name="pcTxTitle" class="form-control" value="<%= therows[0].title %>">
                            <br/>
                            <input formaction="/manage/save-paste/<%=therows[0].pastemanagerperm %>" id="pc-sb-savepaste" name="pcSbSavepaste" class="btn btn-danger btn-block" type="submit" value="Save Paste" />
                            <input formaction="/manage/delete-paste/<%=therows[0].pastemanagerperm %>" id="pc-sb-delpaste" name="pcSbDelpaste" class="btn btn-danger btn-block" type="submit" value="Delete Paste" />
                        </div>
                    </div>
                </div>
            </div>

            <div class= "col-lg-6 col-md-6 col-sm-6 col-xs-6">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title"><b>Links for This Paste</b></h3>
                    </div>
                    <div class="panel-body">
                        <label for="pc-tx-managerurl">Manager URL:</label>
                        <div id="pc-tx-managerurl" class="input-group">
                            <input id="pc-in-tx-managerurl" type="text" class="form-control" value="<%=hostname%>/manage/<%=therows[0].pastemanagerperm %>">
                            <span class="input-group-btn">
                                <button id="pc-btn-copy-managerurl" class="btn btn-default" type="button">Copy!</button>
                            </span>
                        </div>

                        <label for="pc-tx-pasteurl">Paste URL:</label>
                        <div id="pc-tx-pasteurl" class="input-group">
                            <input id="pc-in-tx-pasteurl" type="text" class="form-control" value="<%=hostname%>/paste/<%=therows[0].pasteperm %>">
                            <span class="input-group-btn">
                                <button id="pc-btn-copy-pasteurl" class="btn btn-default" type="button">Copy!</button>
                            </span>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </form>
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <p class="navbar-text pull-right">Copyright &#169; Lusfikar Sheba 2017</p>
        </div>
    </nav>
    <script>
        document.getElementById("pc-btn-copy-managerurl").addEventListener("click", function() {
            var succ = copyToClipboard(document.getElementById("pc-in-tx-managerurl"));
            if(succ == true){
                alert("URL has been copied to clipboard");
            }
        });

        document.getElementById("pc-btn-copy-pasteurl").addEventListener("click", function() {
            var succ = copyToClipboard(document.getElementById("pc-in-tx-pasteurl"));
            if(succ == true){
                alert("URL has been copied to clipboard");
            }
        });
        
        function copyToClipboard(elem) {
              // create hidden text element, if it doesn't already exist
            var targetId = "_hiddenCopyText_";
            var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA";
            var origSelectionStart, origSelectionEnd;
            if (isInput) {
                // can just use the original source element for the selection and copy
                target = elem;
                origSelectionStart = elem.selectionStart;
                origSelectionEnd = elem.selectionEnd;
            } else {
                // must use a temporary form element for the selection and copy
                target = document.getElementById(targetId);
                if (!target) {
                    var target = document.createElement("textarea");
                    target.style.position = "absolute";
                    target.style.left = "-9999px";
                    target.style.top = "0";
                    target.id = targetId;
                    document.body.appendChild(target);
                }
                target.textContent = elem.textContent;
            }
            // select the content
            var currentFocus = document.activeElement;
            target.focus();
            target.setSelectionRange(0, target.value.length);
            
            // copy the selection
            var succeed;
            try {
                  succeed = document.execCommand("copy");
            } catch(e) {
                succeed = false;
            }
            // restore original focus
            if (currentFocus && typeof currentFocus.focus === "function") {
                currentFocus.focus();
            }
            
            if (isInput) {
                // restore prior selection
                elem.setSelectionRange(origSelectionStart, origSelectionEnd);
            } else {
                // clear temporary content
                target.textContent = "";
            }
            return succeed;
        }
    </script>
</body>
</html>

Perhatikan pada bagian:

<title>Manage <%= therows[0].title %> - Node.js Pasta</title>

Di sini kita menuliskan judul halaman dengan cara yang mirip hostname pada halaman "Home".

Bedanya karena yang diinjeksi di server adalah row dari hasil query yang berupa array, maka di sana ada index 0 ("[0]").

Adapun ".title" di atas adalah nama kolom dari database.

Selanjutnya pada bagian ini:

<textarea id="pc-ta-newpaste" name="pcTaNewpaste" class="form-control" rows="10">
          <%= therows[0].newpaste %>
</textarea>

Kita mengisi textarea paste yang berisi konten paste dengan nilai yang telah disimpan dari database.

Jadi ketika kita mengisi "abcde" pada halaman "Home", nilai itu akan dibawa ke halaman "Manage" di bagian textarea tersebut.

Lebih jelasnya, lihat kodenya di server (index.js):

app.get('/manage/:manageID', function (req, res) {
    db.all("SELECT * FROM pastes WHERE pastemanagerperm = '" + req.params.manageID + "'", function(err, rows) {
        if(rows.length > 0){
            res.render('manage', { 
                therows : rows,
                hostname: getBaseURL(req)
            });
        }else{
            res.status(404);
            res.render('404');
        }
    });
})

Di kode tersebut, kita meminta pada database untuk menampilkan row dari tabel pastes yang manage id-nya adalah "req.params.manageID".

Atau lebih tepatnya yang dicetak tebal dan digaris bawah ini, misalnya:

http://localhost:5000/manage/yanginilho

Cukup jelas bukan? ManageID di database disimpan dalam kolom pastemanagerperm.

Setelah query tersebut dijalankan, maka halaman "Manage" akan dirender di dalam callback.

Jika query tersebut hasilnya nihil, maka buka halaman 404.

Jadi di mana nilai textarea tadi?

Jawabannya adalah di dalam variabel therows yang diinjeksikan tadi.

Selanjutnya, bagian ini akan menampilkan URL manager yang digenerate:

<div id="pc-tx-managerurl" class="input-group">
       <input id="pc-in-tx-managerurl" type="text" class="form-control" value="<%=hostname%>/manage/<%=therows[0].pastemanagerperm %>">
       <span class="input-group-btn">
                <button id="pc-btn-copy-managerurl" class="btn btn-default" type="button">Copy!</button>
       </span>
 </div>

Tepatnya bagian yang dicetak tebal tadi:

value="<%=hostname%>/manage/<%=therows[0].pastemanagerperm %>"

Hal yang sama berlaku juga pada URL paste yang digenerate.

URL tersebut bisa dicopy melalui sebuah button bernama "Copy" yang dicetak tebal tadi:

<button id="pc-btn-copy-managerurl" class="btn btn-default" type="button">Copy!</button>

Tapi tidak bisa jika tanpa javascript ini:

document.getElementById("pc-btn-copy-managerurl").addEventListener("click", function() {
        var succ = copyToClipboard(document.getElementById("pc-in-tx-managerurl"));
        if(succ == true){
            alert("URL has been copied to clipboard");
        }
});


Lalu bagaimanakah fungsi copyToClipboard? Jawabnya adalah:

function copyToClipboard(elem) {
 // create hidden text element, if it doesn't already exist
 var targetId = "_hiddenCopyText_";
 var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA";
 var origSelectionStart, origSelectionEnd;
 if (isInput) {
  // can just use the original source element for the selection and copy
  target = elem;
  origSelectionStart = elem.selectionStart;
  origSelectionEnd = elem.selectionEnd;
 } else {
  // must use a temporary form element for the selection and copy
  target = document.getElementById(targetId);
  if (!target) {
   var target = document.createElement("textarea");
   target.style.position = "absolute";
   target.style.left = "-9999px";
   target.style.top = "0";
   target.id = targetId;
   document.body.appendChild(target);
  }
  target.textContent = elem.textContent;
 }
 // select the content
 var currentFocus = document.activeElement;
 target.focus();
 target.setSelectionRange(0, target.value.length);
 
 // copy the selection
 var succeed;
 try {
    succeed = document.execCommand("copy");
 } catch(e) {
  succeed = false;
 }
 // restore original focus
 if (currentFocus && typeof currentFocus.focus === "function") {
  currentFocus.focus();
 }
 
 if (isInput) {
  // restore prior selection
  elem.setSelectionRange(origSelectionStart, origSelectionEnd);
 } else {
  // clear temporary content
  target.textContent = "";
 }
 return succeed;
}

Isi dari paste tersebut akan diupdate perubahannya atau dihapus melalui form ini:

<input formaction="/manage/save-paste/<%=therows[0].pastemanagerperm %>" id="pc-sb-savepaste" name="pcSbSavepaste" class="btn btn-danger btn-block" type="submit" value="Save Paste" />
<input formaction="/manage/delete-paste/<%=therows[0].pastemanagerperm %>" id="pc-sb-delpaste" name="pcSbDelpaste" class="btn btn-danger btn-block" type="submit" value="Delete Paste" />

Di sini kita tidak menggunakan tag "form" tapi "formaction", jadi dua tombol yang berbeda bisa memiliki actionnya masing masing, yakni yang dicetak tebal tadi.

Di sisi server (index.js), penge-save-an paste terjadi seperti ini:

app.post('/manage/save-paste/:manageID', function(req, res) {
    db.run(
        "UPDATE pastes SET newpaste=?,syntaxhi=?,pasteex=?,pasteexpos=?,title=? WHERE pastemanagerperm=?",
        req.body.pcTaNewpaste,
        req.body.pcSlSyntaxhi,
        req.body.pcSlPasteex,
        req.body.pcSlPasteexpos,
        req.body.pcTxTitle == "" ? "Untitled" : req.body.pcTxTitle,
        req.params.manageID
    );

    console.log("saved: " + req.params.manageID);

    setTimeout(function(){
        console.log("deleting the row automatically...");
        db.run("DELETE FROM pastes WHERE pastemanagerperm = '" + req.params.manageID + "'");
        console.log("deleted the row automatically...");
    }, textToMS(req.body.pcSlPasteex));

    res.redirect("/manage/"+ req.params.manageID);
});

Perhatikan bahwa kita akan mengeksekusi query update ("UPDATE paste SET....".

Kemudian setelah di-update, kita akan mengeset timer kembali:

setTimeout(function(){
        console.log("deleting the row automatically...");
        db.run("DELETE FROM pastes WHERE pastemanagerperm = '" + req.params.manageID + "'");
        console.log("deleted the row automatically...");
}, textToMS(req.body.pcSlPasteex));

Dengan cara yang sama dengan bagian pembuatan database di bab sebelumnya.

Hal itu dilakukan karena di halaman "Manage" kita bisa meng-update Paste Expiration.

Selanjutnya penghapusan paste di sisi server (index.js):

app.post('/manage/delete-paste/:manageID', function(req, res) {
    db.run("DELETE FROM pastes WHERE pastemanagerperm = '" + req.params.manageID + "'");
    console.log("deleted: " + req.params.manageID);
    res.redirect("/");
});

Tidak terlalu rumit. Kita hanya perlu melakukan query ("DELETE FROM pastes ....".

Kemudian, dilanjutkan dengan redirect ke halaman "Home" (res.redirect("/");).

Membuat Halaman Paste

Kita telah melewati bagian tersulit dari aplikasi ini.

Saatnya bersantai...

Sekarang buka file "paste.ejs" dari folder "ls-node-js-pasta/views".

Isinya kira-kira seperti ini:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title><%= therows[0].title %> - Node.js Pasta</title>

        <meta name="description" content="Share <%= therows[0].title %> with Node.js Pasta">
        <meta name="author" content="Lusfikar Sheba">

        <!-- [ Social Media meta tag ] -->
        <meta content='summary' name='twitter:card'/>
        <meta content='@ciyy_space' name='twitter:site'/>
        <meta content='@ciyy_space' name='twitter:creator'/>

        <meta content='<%=hostname%>/paste/<%=therows[0].pasteperm %>' property='og:url'/>
        <meta content='<%= therows[0].title %> - Node.js Pasta' property='og:site_name'/>
        <meta content='<%= therows[0].title %> - Node.js Pasta' property='og:title'/>
        <meta content='<%= therows[0].title %> - Node.js Pasta' name='twitter:title'/>
        <meta content='article' property='og:type'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>
        <meta content='Share <%= therows[0].title %> with Node.js Pasta' property='og:description'/>
        <meta content='Share <%= therows[0].title %> with Node.js Pasta' name='twitter:description'/>
        <meta content='en_US' property='og:locale'/>
        <!-- [/ Social Media meta tag ] -->

        <link rel="shortcut icon" type="image/png" href="/views/img/favicon.png"/>
        <link href="/views/css/bootstrap.min.css" rel="stylesheet">
        <link href="/views/css/bootstrap-theme.min.css" rel="stylesheet">
        <link href="/views/css/font-awesome.min.css" rel="stylesheet">
        <script src="/views/js/jquery-3.2.1.min.js"></script>
        <script src="/views/js/bootstrap.min.js"></script>
        
        <style type="text/css" media="screen">
            #editor { 
                position: relative;
                height: 300px;
            }
        </style>
    </head>
<body>

<div class="container">
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Node.js Pasta</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/trends">Trends</a></li>
                    <li><div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/"><span class="glyphicon glyphicon-plus-sign"></span> New Paste</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><b>This Paste</b></h3>
                </div>
                <div class="panel-body">
                    <div id="editor">
                        <%= therows[0].newpaste %>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><b>Links for This Paste</b></h3>
                </div>
                <div class="panel-body">
                    <label for="pc-tx-pasteurl">Paste URL:</label>
                    <div id="pc-tx-pasteurl" class="input-group">
                        <input id="pc-in-tx-pasteurl" type="text" class="form-control" value="<%=hostname%>/paste/<%=therows[0].pasteperm %>">
                        <span class="input-group-btn">
                            <button id="pc-btn-copy-pasteurl" class="btn btn-default" type="button">Copy!</button>
                        </span>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><b>Share</b></h3>
                </div>
                <div class="panel-body">
                    <div style="text-align: center">
                        <a href="http://www.facebook.com/share.php?u=<%=hostname%>/paste/<%=therows[0].pasteperm %>&t=<%=therows[0].title %>" class="fa fa-facebook fa-5x" target="_blank"></a>
                        <a href="http://twitter.com/share?original_referer=<%=hostname%>/paste/<%=therows[0].pasteperm %>&source=web&text=<%=therows[0].title %>" class="fa fa-twitter fa-5x" target="_blank"></a>
                        <a href="https://plus.google.com/share?url=<%=hostname%>/paste/<%=therows[0].pasteperm %>" class="fa fa-google fa-5x" target="_blank"></a>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <p class="navbar-text pull-right">Copyright &#169; Lusfikar Sheba 2017</p>
        </div>
    </nav>

    <script src="/views/js/ace/ace.js" type="text/javascript" charset="utf-8"></script>
    <script>
        var editor = ace.edit("editor");
        editor.setTheme("ace/theme/github");
        editor.getSession().setMode("ace/mode/<%=therows[0].syntaxhi %>");
        editor.setReadOnly(true);
    </script>
    <script>
        document.getElementById("pc-btn-copy-pasteurl").addEventListener("click", function() {
            var succ = copyToClipboard(document.getElementById("pc-in-tx-pasteurl"));
            if(succ == true){
                alert("URL has been copied to clipboard");
            }
        });

        function copyToClipboard(elem) {
                // create hidden text element, if it doesn't already exist
            var targetId = "_hiddenCopyText_";
            var isInput = elem.tagName === "INPUT" || elem.tagName === "TEXTAREA";
            var origSelectionStart, origSelectionEnd;
            if (isInput) {
                // can just use the original source element for the selection and copy
                target = elem;
                origSelectionStart = elem.selectionStart;
                origSelectionEnd = elem.selectionEnd;
            } else {
                // must use a temporary form element for the selection and copy
                target = document.getElementById(targetId);
                if (!target) {
                    var target = document.createElement("textarea");
                    target.style.position = "absolute";
                    target.style.left = "-9999px";
                    target.style.top = "0";
                    target.id = targetId;
                    document.body.appendChild(target);
                }
                target.textContent = elem.textContent;
            }
            // select the content
            var currentFocus = document.activeElement;
            target.focus();
            target.setSelectionRange(0, target.value.length);
            
            // copy the selection
            var succeed;
            try {
                    succeed = document.execCommand("copy");
            } catch(e) {
                succeed = false;
            }
            // restore original focus
            if (currentFocus && typeof currentFocus.focus === "function") {
                currentFocus.focus();
            }
            
            if (isInput) {
                // restore prior selection
                elem.setSelectionRange(origSelectionStart, origSelectionEnd);
            } else {
                // clear temporary content
                target.textContent = "";
            }
            return succeed;
        }
    </script>
</body>
</html>

Tidak ada yang rumit di sini, fungsi  copyToClipboard yang sudah dijelaskan juga ada di sini.

Injeksi therows ke dalam file "paste.ejs" yang sudah dijelaskan juga ada.

Berarti tinggal satu hal lagi yang belum dijelaskan.

Dan itu adalah membuat twitter card dan facebook open graph.

Twitter card adalah semacam screenshot yang ditampilkan saat kita men-share sebuah halaman web ke social media Twitter.

Facebook open graph juga mirip dengan itu.

Contohnya yang seperti ini:


Screenshot-screenshot tadi saya dapatkan dari debugger milik twitter dan facebook.

Alamatnya di sini:

https://cards-dev.twitter.com/validator

https://developers.facebook.com/tools/debug/

Jadi, ketika Anda mempelajari tentang ini, lakukan percobaan dengan menggunakan website tersebut.

Dari sisi coding, yang menentukan tampilan tadi ada di bagian ini (ada di paste.ejs):

<!-- [ Social Media meta tag ] -->
<meta content='summary' name='twitter:card'/>
<meta content='@ciyy_space' name='twitter:site'/>
<meta content='@ciyy_space' name='twitter:creator'/>

<meta content='<%=hostname%>/paste/<%=therows[0].pasteperm %>' property='og:url'/>
<meta content='<%= therows[0].title %> - Node.js Pasta' property='og:site_name'/>
<meta content='<%= therows[0].title %> - Node.js Pasta' property='og:title'/>
<meta content='<%= therows[0].title %> - Node.js Pasta' name='twitter:title'/>
<meta content='article' property='og:type'/>
<meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>
<meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>
<meta content='Share <%= therows[0].title %> with Node.js Pasta' property='og:description'/>
<meta content='Share <%= therows[0].title %> with Node.js Pasta' name='twitter:description'/>
<meta content='en_US' property='og:locale'/>
<!-- [/ Social Media meta tag ] -->

Untuk twitter, yang perlu diperhatikan adalah:

<meta content='summary' name='twitter:card'/>
<meta content='<%= therows[0].title %> - Node.js Pasta' name='twitter:title'/>
<meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>

Bagian pertama menentukan apakah tampilannya besar atau kecil. Dalam hal ini kecil.

Bagian kedua menentukan bagaimana title-nya.

Bagian ketiga menentukan apa gambarnya.

Untuk gambar, paling aman ambil resolusi 200x200 pixel.

Sisanya cukup jelas, yang penting di bagian meta name ada teks:

name='twitter:blablabla'

Jadi kalau:

name='twitter:description'

Ya... berarti untuk deskripsi dari link yang di-share.

Untuk Facebook, yang perlu diperhatikan adalah:

<meta content='<%=hostname%>/paste/<%=therows[0].pasteperm %>' property='og:url'/>
<meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>

Bagian pertama harus diisi URL dari paste.

Bagian kedua gambarnya untuk yang kecil 200x200 pixel.

Ciri khas dari Facebook adalah ada teks:

property='og:blablabla'

Bentuk lainnya bisa ditafsirkan sendiri.

Membuat Halaman Trends

Saat ini, kita telah menyentuh pembahasan terakhir.

Trends berguna untuk menampilkan paste dengan hits terbanyak.

Diurutkan dari yang terbanyak ke yang tersedikit dari atas ke bawah.

Pertama-tama, buka file "trends.ejs" dari folder "ls-node-js-pasta/views".

Isinya kira-kira begini:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <title>Trends - Node.js Pasta</title>

        <meta name="description" content="Node.js Pasta Trends!">
        <meta name="author" content="Lusfikar Sheba">

        <!-- [ Social Media meta tag ] -->
        <meta content='summary' name='twitter:card'/>
        <meta content='@ciyy_space' name='twitter:site'/>
        <meta content='@ciyy_space' name='twitter:creator'/>

        <meta content='<%=hostname%>/trends' property='og:url'/>
        <meta content='Trends - Node.js Pasta' property='og:site_name'/>
        <meta content='Trends - Node.js Pasta' property='og:title'/>
        <meta content='Trends - Node.js Pasta' name='twitter:title'/>
        <meta content='website' property='og:type'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' property='og:image'/>
        <meta content='<%=hostname%>/views/img/ciyy-200x200.png' name='twitter:image'/>
        <meta content='Node.js Pasta Trends!' property='og:description'/>
        <meta content='Node.js Pasta Trends!' name='twitter:description'/>
        <meta content='en_US' property='og:locale'/>
        <!-- [/ Social Media meta tag ] -->

        <link rel="shortcut icon" type="image/png" href="/views/img/favicon.png"/>
        <link href="/views/css/bootstrap.min.css" rel="stylesheet">
        <link href="/views/css/bootstrap-theme.min.css" rel="stylesheet">
        <script src="/views/js/jquery-3.2.1.min.js"></script>
        <script src="/views/js/bootstrap.min.js"></script>
    </head>
<body>
<% include ./functions %>
<div class="container">
    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Node.js Pasta</a>
            </div>
            <div id="navbar" class="navbar-collapse collapse">
                <ul class="nav navbar-nav navbar-right">
                    <li><a href="/trends">Trends</a></li>
                    <li><div class="btn-nav"><a class="btn btn-primary btn-small navbar-btn" href="/"><span class="glyphicon glyphicon-plus-sign"></span> New Paste</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <div class="row">
        <div class= "col-lg-12 col-md-12 col-sm-12 col-xs-12">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><b>Trending Pastes</b></h3>
                </div>
                <div class="panel-body" style="min-height: 336px; max-height: 336px; overflow-y: scroll;">
                    <div class="list-group">
                        <% if (locals.therows) { %>
                            <% therows.forEach(function(singleRow) { %>
                                <a href="<%= hostname %>/paste/<%= singleRow.pasteperm %>" target="_blank" class="list-group-item">
                                    <h4 class="list-group-item-heading"><%= singleRow.title %></h4>
                                    <p class="list-group-item-text"><i>Expired in <%= singleRow.pasteex %>, <%= singleRow.hits %> Hit(s)</i></p>
                                    <p class="list-group-item-text"><%= getTheExcerpt(singleRow.newpaste) %></p>
                                </a>
                            <% }); %>
                        <% } %>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <nav class="navbar navbar-default navbar-inverse">
        <div class="container-fluid">
            <p class="navbar-text pull-right">Copyright &#169; Lusfikar Sheba 2017</p>
        </div>
    </nav>
</body>
</html>

Tidak ada hal baru di sini, kecuali bagian:

<div class="list-group">
 <% if (locals.therows) { %>
  <% therows.forEach(function(singleRow) { %>
   <a href="<%= hostname %>/paste/<%= singleRow.pasteperm %>" target="_blank" class="list-group-item">
    <h4 class="list-group-item-heading"><%= singleRow.title %></h4>
    <p class="list-group-item-text"><i>Expired in <%= singleRow.pasteex %>, <%= singleRow.hits %> Hit(s)</i></p>
    <p class="list-group-item-text"><%= getTheExcerpt(singleRow.newpaste) %></p>
   </a>
  <% }); %>
 <% } %>
</div>

Perhatikan bagian:

<% if (locals.therows) { %>

Di sini saya melakukan pengecekan apakah rows diinjeksi atau tidak.
Karena apa yang saya tulis di sisi server ini (index.js):

app.get('/trends', function (req, res) {
    db.all("SELECT * FROM pastes WHERE pasteexpos='Public' ORDER BY hits DESC LIMIT 20", function(err, rows) {
        res.render('trends', { 
            therows : rows,
            hostname: getBaseURL(req)
        });
    });
});

Lihat di sana bahwa saya tidak melakukan pengecekan jumlah rows di server.

Bandingkan dengan sisi server halaman "Manage":

app.get('/manage/:manageID', function (req, res) {
    db.all("SELECT * FROM pastes WHERE pastemanagerperm = '" + req.params.manageID + "'", function(err, rows) {
        if(rows.length > 0){
            res.render('manage', { 
                therows : rows,
                hostname: getBaseURL(req)
            });
        }else{
            res.status(404);
            res.render('404');
        }
    });
})

Di sisi server halaman "Manage" ada "if(rows.length > 0".

Sedangkan di sisi server halaman "Trends" tidak ada dan langsung render saja.

Apabila kita tidak melakukan pengecekan sama sekali maka di halaman "Trends" akan error atau undefined.

Maka itu, saya melakukan pengecekan di dalam template "paste.ejs":

<% if (locals.therows) { %>

Selanjutnya saya melakukan loop terhadap isi dari array "therows" diwakili oleh variabel "singleRow":

<% therows.forEach(function(singleRow) { %>

Dan pada akhirnya saya mengambil excerpt dari singleRow:

<%= getTheExcerpt(singleRow.newpaste) %>

Fungsi getTheExcerpt ini telah kita definisikan pada bab sebelumnya.

Tapi itu tidak bisa digunakan tanpa ini:

<% include ./functions %>

Dengan kode tersebut, kita meng-include template "functions.ejs" agar fungsi getTheExcerpt dapat digunakan.

Mencoba Aplikasi Ini

Sekarang, kita telah selesai membahas pengerjaan aplikasi Node.js Pasta.

Mungkin ada satu atau beberapa kode yang tertinggal, Anda tinggal mengopy-paste saja dari source code buku ini.

Atau boleh juga langsung jalankan source code buku ini. Yang penting Anda mengerti cara kerjanya.

Jadi... Jalankan perintah ini:

node .

Kemudian saksikan hasilnya di browser Anda.

Men-Deploy Aplikasi Ini di Heroku

Untuk mencoba tampilan sharing di social media, kita membutuhkan sebuah hosting.

Yang kita bahas adalah heroku ( https://www.heroku.com ).

Daftarkan akun Anda terlebih dahulu.

Kemudian, buat app baru dan beri nama yang available:



Setelah selesai, Anda siap untuk menguploadnya. Pastikan Anda sudah menginstall semua software yang dibutuhkan di bab awal.

Sekarang buka command line, masuk ke folder "ls-node-js-pasta".

Jalankan perintah ini:

heroku login

Isi email dan password Anda.

Kemudian jalankan perintah ini:

heroku git:clone -a ls-node-js-pasta

git add .

git commit -am "make it better"

git push heroku master

Tunggu sampai proses upload selesai.

Setelah itu buka browser ke:

https://nama_subdomain_anda.herokuapp.com

Untuk mengupdate saja, cukup dengan perintah ini:

heroku login

git add .

git commit -am "make it better"

git push heroku master

Nama subdomain sama dengan nama App yang telah kita berikan.

Kalau Anda tidak sabar dan ingin melihat langsung aplikasi ini yang sudah jadi, kunjungi:

https://ls-node-js-pasta.herokuapp.com/

Penutup

Ya.. Sampai di sini pembahasan kita tentang cara membuat aplikasi Node.js Pasta.

Aplikasi ini masih banyak kekurangan.

Anda bisa mencoba menambahkan sendiri fitur API dan embed paste.

Setelah Anda sukses, ceritakan kepada saya di website saya ( http://www.ciyy.space ) dan belilah beberapa buku lagi agar semakin sukses.




EmoticonEmoticon

Catatan: Hanya anggota dari blog ini yang dapat mengirim komentar.