Terakhir diperbaharui: Jul 1, 2021
Menambahkan fungsi Search, Sorting dan Authentication
Pada bagian ini kita akan menambahkan tiga fungsi penting pada aplikasi DinoTes. Search & Sorting untuk mempermudah user dalam menggunakan aplikasi dan meningkatkan keamanan dari aplikasi dengan menambahkan sistem Authentication.
Menambahkan Search
Secara teknis yang dimaksud dengan search disini adalah memberikan user kemampuan untuk memfilter notes sesuai dengan keyword yang dimasukan.
Update code dari component NotesList:
Buat sebuah Search Field tempat user memasukan keyword.
1...2 <div className="flex flex-row w-full justify-end">3 <input4 className="m-4 p-2 text-left border rounded focus:outline-none focus:ring focus:border-blue-300"5 placeholder="Search Notes..."6 onChange={handleChange}7 ></input>8 </div>9...
Tambahkan Hook state yang akan digunakan untuk proses filter.
1...2 const [keyword, setKeyword] = useState("");3 const [notes, setNotes] = useState("");4 const [filteredNotes, setFilteredNotes] = useState("");5 useEffect(() => {6 async function getNotes() {7 const { data } = await API.graphql({8 authMode: "API_KEY",9 query: listNotes,10 });11 setNotes(data.listNotes.items);12 setFilteredNotes(data.listNotes.items);13 }14 getNotes();15 }, []);16...
Tambahkan handler yang berfungsi untuk melakukan filter.
1...2 const isKeywordExist = (array, string) =>3 array.toLowerCase().includes(string);4 const applyFilter = () => {5 if (keyword) {6 const data = notes.filter(7 (note) =>8 isKeywordExist(note.title, keyword) ||9 isKeywordExist(note.content, keyword)10 );11 setFilteredNotes(data);12 } else {13 setFilteredNotes(notes);14 }15 };16...
Update handler handleChange().
1...2 const handleChange = (e) => {3 setKeyword(e.target.value);4 applyFilter();5 };6...
Pindah bagian code yang berfungsi untuk menampilkan semua notes ke dalam component tersendiri agar lebih mudah dikelola (optional).
1...2 const content =3 filteredNotes &&4 filteredNotes.map((note) => {5 return (6 <div className="text-left p-4 border rounded-md" key={note.id}>7 <h4 className="text-lg font-semibold text-purple-900">8 <Link href={`/edit/${note.id}`}>{note.title}</Link>9 </h4>10 <p>{note.content.slice(0, 101)}</p>11 </div>12 );13 });14...15 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 my-8 overflow-y-auto">16 {content}17 </div>18...
Final Code:
components/NotesList
1import React, { useEffect, useState } from 'react';2import { API } from '@aws-amplify/api';3import Link from 'next/link';45import { listNotes } from '../graphql/queries';67const NotesList = () => {8 const [keyword, setKeyword] = useState('');9 const [notes, setNotes] = useState('');10 const [filteredNotes, setFilteredNotes] = useState('');1112 useEffect(() => {13 async function getNotes() {14 const { data } = await API.graphql({15 authMode: 'API_KEY',16 query: listNotes17 });18 setNotes(data.listNotes.items);19 setFilteredNotes(data.listNotes.items);20 }2122 getNotes();23 }, []);2425 const isKeywordExist = (array, string) => array.toLowerCase().includes(string);2627 const applyFilter = () => {28 if (keyword) {29 const data = notes.filter((note) => isKeywordExist(note.title, keyword) || isKeywordExist(note.content, keyword));30 setFilteredNotes(data);31 } else {32 setFilteredNotes(notes);33 }34 };3536 const handleChange = (e) => {37 setKeyword(e.target.value);38 applyFilter();39 };4041 const content =42 filteredNotes &&43 filteredNotes.map((note) => {44 return (45 <div className="text-left p-4 border rounded-md" key={note.id}>46 <h4 className="text-lg font-semibold text-purple-900">47 <Link href={`/edit/${note.id}`}>{note.title}</Link>48 </h4>49 <p>{note.content.slice(0, 101)}</p>50 </div>51 );52 });5354 return (55 <div className="flex flex-col items-center justify-center m-4">56 <div className="flex flex-row w-full justify-end">57 <input58 className="m-4 p-2 text-left border rounded focus:outline-none focus:ring focus:border-blue-300"59 placeholder="Search Notes..."60 onChange={handleChange}61 ></input>62 </div>63 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 my-8 overflow-y-auto">{content}</div>64 </div>65 );66};6768export default NotesList;
Hasil Akhir:
Menambahkan Sorting
Fitur sorting yang ditambahkan adalah sorting berdasarkan waktu terakhir note diperbaharui.
Buat component button yang digunakan untuk melakukan sorting.
1...2 <div className="relative inline-block text-left" ref={node}>3 <div4 className="text-base text-right my-4 p-2 border rounded-md"5 onClick={handleVisible}6 >7 <FontAwesomeIcon icon={faSortAmountUp} size="lg" />8 </div>9 {visible && (10 <div className="origin-top-right absolute right-0 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">11 <ul12 className="py-1"13 role="menu"14 aria-orientation="vertical"15 aria-labelledby="options-menu"16 >17 <div18 className="block px-4 py-2 w-full text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-90"19 role="menuitem"20 onClick={(e) => handleClick(e, "newest")}21 >22 Newest Modified Date23 </div>24 <div25 className="block px-4 py-2 w-full text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-90"26 role="menuitem"27 onClick={(e) => handleClick(e, "oldest")}28 >29 Oldest Modified Date30 </div>31 </ul>32 </div>33 )}34 </div>35...
Tambahkan Hook state yang nantinya digunakan oleh handler untuk melakukan proses sorting.
1...2 const [visible, setVisible] = useState(false);3 const node = useRef();4...
Tambahkan handler untuk menghandle proses sorting.
1...2 const applySort = (option) => {3 if (option === "oldest") {4 const data = [...notes].sort(5 (a, b) =>6 new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()7 );8 setFilteredNotes(data);9 } else if (option === "newest") {10 const data = [...notes].sort(11 (a, b) =>12 new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()13 );14 setFilteredNotes(data);15 }16 };17...18 const handleVisible = (e) => {19 setVisible(!visible);20 };2122 const handleClick = (e, option) => {23 if (node.current.contains(e.target)) {24 applySort(option);25 return;26 }2728 setVisible(false);29 };30...
Final Code:
1import React, { useEffect, useState, useRef } from 'react';2import Link from 'next/link';3import { API } from '@aws-amplify/api';4import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';5import { faSortAmountUp } from '@fortawesome/free-solid-svg-icons';67import { listNotes } from '../graphql/queries';89const NotesList = () => {10 const [keyword, setKeyword] = useState('');11 const [notes, setNotes] = useState('');12 const [filteredNotes, setFilteredNotes] = useState('');13 const [visible, setVisible] = useState(false);14 const node = useRef();1516 useEffect(() => {17 async function getNotes() {18 const { data } = await API.graphql({19 authMode: 'API_KEY',20 query: listNotes21 });22 setNotes(data.listNotes.items);23 setFilteredNotes(data.listNotes.items);24 }2526 getNotes();2728 document.addEventListener('mousedown', handleClick);2930 return () => {31 document.removeEventListener('mousedown', handleClick);32 };33 }, []);3435 const isKeywordExist = (array, string) => array.toLowerCase().includes(string);3637 const applyFilter = () => {38 if (keyword) {39 const data = notes.filter((note) => isKeywordExist(note.title, keyword) || isKeywordExist(note.content, keyword));40 setFilteredNotes(data);41 } else {42 setFilteredNotes(notes);43 }44 };4546 const applySort = (option) => {47 if (option === 'oldest') {48 const data = [...notes].sort((a, b) => new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime());49 setFilteredNotes(data);50 } else if (option === 'newest') {51 const data = [...notes].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());52 setFilteredNotes(data);53 }54 };5556 const handleChange = (e) => {57 setKeyword(e.target.value);58 applyFilter();59 };6061 const handleVisible = (e) => {62 setVisible(!visible);63 };6465 const handleClick = (e, option) => {66 if (node.current.contains(e.target)) {67 applySort(option);68 return;69 }7071 setVisible(false);72 };7374 const content =75 filteredNotes &&76 filteredNotes.map((note) => {77 return (78 <div className="text-left p-4 border rounded-md" key={note.id}>79 <h4 className="text-lg font-semibold text-purple-900">80 <Link href={`/edit/${note.id}`}>{note.title}</Link>81 </h4>82 <p>{note.content.slice(0, 101)}</p>83 </div>84 );85 });8687 return (88 <div className="flex flex-col items-center justify-center m-4">89 <div className="flex flex-row w-full justify-end">90 <input91 className="m-4 p-2 text-left border rounded focus:outline-none focus:ring focus:border-blue-300"92 placeholder="Search Notes..."93 onChange={handleChange}94 ></input>95 <div className="relative inline-block text-left" ref={node}>96 <div className="text-base text-right my-4 p-2 border rounded-md" onClick={handleVisible}>97 <FontAwesomeIcon icon={faSortAmountUp} size="lg" />98 </div>99 {visible && (100 <div className="origin-top-right absolute right-0 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">101 <ul className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">102 <div103 className="block px-4 py-2 w-full text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-90"104 role="menuitem"105 onClick={(e) => handleClick(e, 'newest')}106 >107 Newest Modified Date108 </div>109 <div110 className="block px-4 py-2 w-full text-sm text-left text-gray-700 hover:bg-gray-100 hover:text-gray-90"111 role="menuitem"112 onClick={(e) => handleClick(e, 'oldest')}113 >114 Oldest Modified Date115 </div>116 </ul>117 </div>118 )}119 </div>120 </div>121 <div className="grid grid-cols-1 md:grid-cols-3 gap-4 my-8 overflow-y-auto">{content}</div>122 </div>123 );124};125126export default NotesList;
Hasil Akhir:
Menambahkan Sistem Autentikasi
Jika untuk membuat sistem autentikasi aplikasi DinoTes sebelumnya kita menggunakan library seperti Passport.js, kali ini kita akan manfaatkan fitur autentikasi milik AWS Amplify.
Beberapa kelebihan dari sistem autentikasi AWS Amplify:
- Untuk menambahkan sistem autentikasi yang perlu dilakukan adalah import method/class Auth dari package aws-amplify
- Tidak perlu membuat halaman login page, kita bisa manfaatkan component AmplifyAuthenticator
- Data user bisa dilihat dan dikelola dari Admin UI
Fitur autentikasi ini sudah ditambahkan di langkah awal konfigurasi AWS Amplify, untuk memeriksa apakah sudah terinstall gunakan perintah amplify status
.
Dimana menempatkan sistem autentikasi ini ?
Konsep dari sistem autentikasi DinoTes & AWS Amplify cukup sederhana, yaitu jika user tidak dalam kondisi sign in maka user akan diarahkan ke halaman login (component AmplifyAuthenticator).
Proses pengecekan apakah user sudah sign in apa belum akan kita tempatkan pada setiap halaman, yaitu pada file index.js, add.js dan [id].js
pages/index.js
1import { useState, useEffect } from 'react';2import { Auth } from 'aws-amplify';3import { AmplifyAuthenticator, AmplifySignUp } from '@aws-amplify/ui-react';4import { AuthState, onAuthUIStateChange } from '@aws-amplify/ui-components';56import Layout from '../components/Layout';7import NotesList from '../components/NotesList';89const Home = () => {10 const [authState, setAuthState] = useState(null);11 const [user, setUser] = useState(null);1213 useEffect(() => {14 onAuthUIStateChange((nextAuthState, authData) => {15 setAuthState(nextAuthState);16 });1718 Auth.currentAuthenticatedUser().then((user) => setUser(user));19 }, []);2021 return authState === AuthState.SignedIn || user ? (22 <div className="text-center">23 <Layout>24 <NotesList />25 </Layout>26 </div>27 ) : (28 <AmplifyAuthenticator>29 <AmplifySignUp slot="sign-up" formFields={[{ type: 'username' }, { type: 'password' }, { type: 'email' }]} />30 </AmplifyAuthenticator>31 );32};3334export default Home;
pages/add.js
1import React, { useState, useEffect } from 'react';2import Link from 'next/link';3import router from 'next/router';4import { Auth } from '@aws-amplify/auth';5import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';6import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';78import Layout from '../components/Layout';9import AddNoteForm from '../components/AddNoteForm';1011const Add = () => {12 const [user, setUser] = useState(null);1314 useEffect(() => {15 Auth.currentAuthenticatedUser()16 .then((user) => setUser(user))17 .catch(() => router.push('/'));18 }, []);19 if (!user) return null;20 return (21 <Layout>22 <div className="flex flex-col items-center justify-center m-4">23 <div className="flex w-full">24 <div className="text-lg font-semibold text-blue-500">25 <FontAwesomeIcon icon={faArrowLeft} /> <Link href="/">Back</Link>26 </div>27 </div>28 <AddNoteForm />29 </div>30 </Layout>31 );32};3334export default Add;
pages/[id].js
1import React, { useState, useEffect } from 'react';2import { useRouter } from 'next/router';3import Link from 'next/link';4import { Auth } from '@aws-amplify/auth';5import { API } from '@aws-amplify/api';6import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';7import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';8import { getNote } from '../../graphql/queries';910import Layout from '../../components/Layout';11import EditNoteForm from '../../components/EditNoteForm';1213const EditPage = () => {14 const [user, setUser] = useState(null);15 const [note, setNote] = useState(null);16 const router = useRouter();1718 const { id } = router.query;1920 useEffect(() => {21 Auth.currentAuthenticatedUser()22 .then((user) => setUser(user))23 .catch(() => router.push('/'));2425 async function getSingleNote() {26 const { data } = await API.graphql({27 authMode: 'API_KEY',28 query: getNote,29 variables: { id: id }30 });3132 setNote(data.getNote);33 }3435 getSingleNote();36 }, []);3738 if (!user) return null;39 if (!note) return null;4041 return (42 <Layout>43 <div className="flex flex-col items-center justify-center m-4">44 <div className="flex w-full">45 <div className="text-lg font-semibold text-blue-500">46 <FontAwesomeIcon icon={faArrowLeft} /> <Link href="/">Back</Link>47 </div>48 </div>49 <EditNoteForm note={note} />50 </div>51 </Layout>52 );53};5455export default EditPage;
Setelah ketiga file diatas diperbaharui code nya, maka jika kita mengakses halaman utama http://localhost:3000 kita akan dapatkan tampilan dari component Login AWS Amplify.
Registrasi / Sign Up
Untuk proses registrasi, kita tinggal klik link Create Account.
Tampilan Registrasi.
Yang perlu dilakukan adalah memasukan username, password dan alamat email, kemudian konfirmasi akun melalui email.
Kita bisa lihat daftar user yang melakukan proses sign up dari admin UI.
Sign In
Untuk sign in, kita tinggal masukan username dan password yang sudah dibuat sebelumnya pada proses registrasi.
Sign Out
Sedangkan untuk proses logout, kita perlu update code pada component Header.js.
components/Header.js
1import React, { useEffect, useState } from 'react';2import Link from 'next/link';3import { useRouter } from 'next/router';4import { Auth } from '@aws-amplify/auth';5import { AmplifySignOut } from '@aws-amplify/ui-react';6import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';7import { faFile, faTimes } from '@fortawesome/free-solid-svg-icons';89const Modal = (props) => {10 const [user, setUser] = useState(null);11 const router = useRouter();1213 useEffect(() => {14 Auth.currentAuthenticatedUser()15 .then((user) => {16 setUser(user);17 })18 .catch((err) => console.log(err));19 }, []);2021 const { close } = props;2223 const handleClick = () => {24 close();25 };2627 const handleChange = (nextAuthState) => {28 close();29 window.location.assign('/');30 };3132 if (!user) return null;3334 return (35 <div className="fixed z-10 inset-0 overflow-y-auto">36 <div className="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">37 <div className="fixed inset-0 transition-opacity">38 <div className="absolute inset-0 bg-gray-500 opacity-75" />39 </div>40 <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true" />41 <div className="inline-block bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg sm:w-full">42 <div className="flex justify-end">43 <div className="bg-white m-4" onClick={handleClick}>44 <FontAwesomeIcon icon={faTimes} />45 </div>46 </div>47 <div className="inline-block bg-white p-4 m-6 text-center sm:p-6">48 <div className="flex items-center justify-center">49 <img className="py-4" src="/header-logo.png" alt="logo" />50 </div>5152 <div className="text-lg text-gray-900">Hi {user.attributes.email}</div>53 </div>54 <div className="bg-gray-50 p-4 items-center justify-center sm:p-6 sm:flex sm:flex-row">55 <AmplifySignOut handleAuthStateChange={handleChange} />56 </div>57 </div>58 </div>59 </div>60 );61};6263const Header = () => {64 const [visible, setVisible] = useState(false);6566 const handleClick = () => {67 setVisible(!visible);68 };6970 return (71 <>72 {visible && <Modal close={handleClick} />}73 <div className="flex justify-between items-center border-b-2 border-gray-100 py-6 md:justify-start md:space-x-3">74 <img className="h-14 w-auto sm:h-16" src="/header-logo.png" alt="logo" onClick={handleClick} />75 <div className="invisible text-xl font-bold text-gray-900 md:visible" />76 <div className="md:flex items-center justify-end md:flex-1 lg:w-0">77 <Link href="/add">78 <button className="bg-purple-700 text-white text-base m-2 p-3 border rounded-md">79 <FontAwesomeIcon icon={faFile} />80 New Note81 </button>82 </Link>83 </div>84 </div>85 </>86 );87};8889export default Header;