Search by

    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 <input
    4 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';
    4
    5import { listNotes } from '../graphql/queries';
    6
    7const NotesList = () => {
    8 const [keyword, setKeyword] = useState('');
    9 const [notes, setNotes] = useState('');
    10 const [filteredNotes, setFilteredNotes] = useState('');
    11
    12 useEffect(() => {
    13 async function getNotes() {
    14 const { data } = await API.graphql({
    15 authMode: 'API_KEY',
    16 query: listNotes
    17 });
    18 setNotes(data.listNotes.items);
    19 setFilteredNotes(data.listNotes.items);
    20 }
    21
    22 getNotes();
    23 }, []);
    24
    25 const isKeywordExist = (array, string) => array.toLowerCase().includes(string);
    26
    27 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 };
    35
    36 const handleChange = (e) => {
    37 setKeyword(e.target.value);
    38 applyFilter();
    39 };
    40
    41 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 });
    53
    54 return (
    55 <div className="flex flex-col items-center justify-center m-4">
    56 <div className="flex flex-row w-full justify-end">
    57 <input
    58 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};
    67
    68export 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 <div
    4 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 <ul
    12 className="py-1"
    13 role="menu"
    14 aria-orientation="vertical"
    15 aria-labelledby="options-menu"
    16 >
    17 <div
    18 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 Date
    23 </div>
    24 <div
    25 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 Date
    30 </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 };
    21
    22 const handleClick = (e, option) => {
    23 if (node.current.contains(e.target)) {
    24 applySort(option);
    25 return;
    26 }
    27
    28 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';
    6
    7import { listNotes } from '../graphql/queries';
    8
    9const 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();
    15
    16 useEffect(() => {
    17 async function getNotes() {
    18 const { data } = await API.graphql({
    19 authMode: 'API_KEY',
    20 query: listNotes
    21 });
    22 setNotes(data.listNotes.items);
    23 setFilteredNotes(data.listNotes.items);
    24 }
    25
    26 getNotes();
    27
    28 document.addEventListener('mousedown', handleClick);
    29
    30 return () => {
    31 document.removeEventListener('mousedown', handleClick);
    32 };
    33 }, []);
    34
    35 const isKeywordExist = (array, string) => array.toLowerCase().includes(string);
    36
    37 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 };
    45
    46 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 };
    55
    56 const handleChange = (e) => {
    57 setKeyword(e.target.value);
    58 applyFilter();
    59 };
    60
    61 const handleVisible = (e) => {
    62 setVisible(!visible);
    63 };
    64
    65 const handleClick = (e, option) => {
    66 if (node.current.contains(e.target)) {
    67 applySort(option);
    68 return;
    69 }
    70
    71 setVisible(false);
    72 };
    73
    74 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 });
    86
    87 return (
    88 <div className="flex flex-col items-center justify-center m-4">
    89 <div className="flex flex-row w-full justify-end">
    90 <input
    91 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 <div
    103 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 Date
    108 </div>
    109 <div
    110 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 Date
    115 </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};
    125
    126export 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';
    5
    6import Layout from '../components/Layout';
    7import NotesList from '../components/NotesList';
    8
    9const Home = () => {
    10 const [authState, setAuthState] = useState(null);
    11 const [user, setUser] = useState(null);
    12
    13 useEffect(() => {
    14 onAuthUIStateChange((nextAuthState, authData) => {
    15 setAuthState(nextAuthState);
    16 });
    17
    18 Auth.currentAuthenticatedUser().then((user) => setUser(user));
    19 }, []);
    20
    21 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};
    33
    34export 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';
    7
    8import Layout from '../components/Layout';
    9import AddNoteForm from '../components/AddNoteForm';
    10
    11const Add = () => {
    12 const [user, setUser] = useState(null);
    13
    14 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} /> &nbsp; <Link href="/">Back</Link>
    26 </div>
    27 </div>
    28 <AddNoteForm />
    29 </div>
    30 </Layout>
    31 );
    32};
    33
    34export 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';
    9
    10import Layout from '../../components/Layout';
    11import EditNoteForm from '../../components/EditNoteForm';
    12
    13const EditPage = () => {
    14 const [user, setUser] = useState(null);
    15 const [note, setNote] = useState(null);
    16 const router = useRouter();
    17
    18 const { id } = router.query;
    19
    20 useEffect(() => {
    21 Auth.currentAuthenticatedUser()
    22 .then((user) => setUser(user))
    23 .catch(() => router.push('/'));
    24
    25 async function getSingleNote() {
    26 const { data } = await API.graphql({
    27 authMode: 'API_KEY',
    28 query: getNote,
    29 variables: { id: id }
    30 });
    31
    32 setNote(data.getNote);
    33 }
    34
    35 getSingleNote();
    36 }, []);
    37
    38 if (!user) return null;
    39 if (!note) return null;
    40
    41 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} /> &nbsp; <Link href="/">Back</Link>
    47 </div>
    48 </div>
    49 <EditNoteForm note={note} />
    50 </div>
    51 </Layout>
    52 );
    53};
    54
    55export 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.

    dinotes amplify sign in

    Registrasi / Sign Up

    Untuk proses registrasi, kita tinggal klik link Create Account.

    Tampilan Registrasi.

    dinotes amplify sign up

    Yang perlu dilakukan adalah memasukan username, password dan alamat email, kemudian konfirmasi akun melalui email.

    dinotes amplify verification code

    dinotes amplify confirm verification code

    Kita bisa lihat daftar user yang melakukan proses sign up dari admin UI.

    dinotes amplify user management

    Sign In

    Untuk sign in, kita tinggal masukan username dan password yang sudah dibuat sebelumnya pada proses registrasi.

    dinotes amplify sign in 2

    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';
    8
    9const Modal = (props) => {
    10 const [user, setUser] = useState(null);
    11 const router = useRouter();
    12
    13 useEffect(() => {
    14 Auth.currentAuthenticatedUser()
    15 .then((user) => {
    16 setUser(user);
    17 })
    18 .catch((err) => console.log(err));
    19 }, []);
    20
    21 const { close } = props;
    22
    23 const handleClick = () => {
    24 close();
    25 };
    26
    27 const handleChange = (nextAuthState) => {
    28 close();
    29 window.location.assign('/');
    30 };
    31
    32 if (!user) return null;
    33
    34 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>
    51
    52 <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};
    62
    63const Header = () => {
    64 const [visible, setVisible] = useState(false);
    65
    66 const handleClick = () => {
    67 setVisible(!visible);
    68 };
    69
    70 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 &nbsp;&nbsp; New Note
    81 </button>
    82 </Link>
    83 </div>
    84 </div>
    85 </>
    86 );
    87};
    88
    89export default Header;

    dinotes amplify sign out