
CÓMO CREAR UN CLON DE EVERNOTE CON NEXT.JS Y FIREBASE
Introducción a la Creación de un Clon de Evernote
Next.js es un framework de renderizado del lado del servidor basado en React, optimizado para motores de búsqueda, lo que lo convierte en una excelente opción para desarrollar aplicaciones web modernas. En este tutorial, construiremos un clon de Evernote, una aplicación para tomar notas, utilizando Next.js para la interfaz de usuario, Firebase como base de datos y SASS para los estilos. Este proyecto te permitirá aprender cómo integrar estas tecnologías para crear una aplicación funcional con capacidades de creación, lectura, edición y eliminación de notas.
El stack tecnológico incluye Next.js para la interfaz, Firebase para almacenar datos y gestionar el hospedaje, y SASS para estilizar los componentes. A continuación, te guiaremos paso a paso para que puedas replicar este proyecto, desde la configuración inicial hasta la implementación de funcionalidades avanzadas como la edición y eliminación de notas.
Creación de un Proyecto con Next.js
Para comenzar, necesitas tener Node.js instalado en tu sistema. Descarga e instala la versión más reciente desde el sitio oficial de Node.js. Una vez configurado, crea un nuevo proyecto de Next.js ejecutando el siguiente comando en tu terminal:
npx create-next-app@latest evernote-clone
Si prefieres Yarn, usa:
yarn create next-app evernote-clone
Este comando generará una estructura de proyecto similar a la de una aplicación React. Navega al directorio del proyecto (cd evernote-clone
) y ejecuta el comando para iniciar el servidor de desarrollo:
npm run dev
Esto iniciará la aplicación en http://localhost:3000
. Next.js incluye un código inicial que puedes limpiar para comenzar desde cero. Abre el archivo pages/index.js
y reemplaza su contenido con lo siguiente:
import Head from "next/head";
import styles from "../styles/Home.module.css";
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Evernote Clone</title>
<meta
name="description"
content="Clon de Evernote con Next.js"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}></main>
</div>
);
}
Este código establece la estructura básica de la página principal, incluyendo un título y metadatos optimizados para SEO.
Instalación de Dependencias
Para construir la aplicación, instalaremos Firebase, SASS y React Quill con un solo comando:
npm install firebase sass react-quill
- Firebase: Gestionará la base de datos en la nube para almacenar las notas.
- SASS: Proporciona características avanzadas para estilizar la aplicación.
- React Quill: Ofrece un editor de texto enriquecido para las notas.
Configuración del Layout Inicial
Dividiremos la interfaz en dos secciones: una columna izquierda para crear y listar notas, y una columna derecha para mostrar los detalles de una nota seleccionada. Actualiza el archivo pages/index.js
:
import Head from "next/head";
import styles from "../styles/Home.module.css";
import NoteOperations from "./components/NoteOperations";
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Evernote Clone</title>
<meta
name="description"
content="Clon de Evernote con Next.js"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.container}>
<div className={styles.left}>
<NoteOperations />
</div>
<div className={styles.right}>Derecha</div>
</div>
</main>
</div>
);
}
Crea un archivo de estilos en styles/Evernote.module.scss
con el siguiente contenido:
@import url("https://fonts.googleapis.com/css2?family=Roboto&display=swap");
$dark-red: rgb(119, 27, 27);
$black: black;
$whiteSmoke: whitesmoke;
$gray: gray;
.container {
display: flex;
margin: 10px;
}
.left {
width: 20rem;
}
.right {
flex: 1;
}
Este código configura un diseño flexible con dos columnas, utilizando la fuente Roboto para mejorar la estética.
Creación del Componente NoteOperations
Crea un directorio components
dentro de pages
y añade un archivo NoteOperations.js
:
import styles from "../../styles/Evernote.module.scss";
import { useState } from "react";
export default function NoteOperations() {
const [isInputVisible, setInputVisible] = useState(false);
const inputToggle = () => {
setInputVisible(!isInputVisible);
};
return (
<>
<div className={styles.btnContainer}>
<button onClick={inputToggle} className={styles.button}>
Agregar Nueva Nota
</button>
</div>
{isInputVisible && (
<div className={styles.inputContainer}>
<input
className={styles.input}
placeholder="Ingresa el título..."
/>
</div>
)}
</>
);
}
Añade los siguientes estilos al archivo Evernote.module.scss
:
.button {
width: 15rem;
height: 2rem;
cursor: pointer;
background-color: $black;
color: $whiteSmoke;
border: $black;
font-family: "Roboto";
}
.input {
width: 15rem;
height: 2rem;
outline: none;
border-radius: 5px;
border: 1px solid $gray;
margin: 5px 0;
}
.btnContainer {
margin-bottom: 10px;
}
Este componente incluye un botón que, al hacer clic, muestra u oculta un campo de entrada para el título de la nota. El uso del estado de React (useState
) permite controlar la visibilidad del campo.
Configuración de Firebase
Crea un proyecto en Firebase desde la consola en https://console.firebase.google.com/
. Una vez creado, selecciona “Añadir aplicación web” para obtener las credenciales de configuración. Crea un archivo firebaseConfig.js
en el directorio raíz:
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "TU_API_KEY",
authDomain: "TU_AUTH_DOMAIN",
projectId: "TU_PROJECT_ID",
storageBucket: "TU_STORAGE_BUCKET",
messagingSenderId: "TU_MESSAGING_SENDER_ID",
appId: "TU_APP_ID",
};
const app = initializeApp(firebaseConfig);
const database = getFirestore(app);
export { app, database };
Reemplaza las claves con las proporcionadas por Firebase. Este archivo configura la conexión con Firestore, la base de datos en la nube de Firebase.
Guardar Notas en Firestore
Actualiza el componente NoteOperations.js
para integrar Firebase y permitir el guardado de notas:
import styles from "../../styles/Evernote.module.scss";
import { useState } from "react";
import { app, database } from "../../firebaseConfig";
import { collection, addDoc } from "firebase/firestore";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
const dbInstance = collection(database, "notes");
export default function NoteOperations() {
const [isInputVisible, setInputVisible] = useState(false);
const [noteTitle, setNoteTitle] = useState("");
const [noteDesc, setNoteDesc] = useState("");
const inputToggle = () => {
setInputVisible(!isInputVisible);
};
const addDesc = (value) => {
setNoteDesc(value);
};
const saveNote = () => {
addDoc(dbInstance, {
noteTitle,
noteDesc,
}).then(() => {
setNoteTitle("");
setNoteDesc("");
});
};
return (
<>
<div className={styles.btnContainer}>
<button onClick={inputToggle} className={styles.button}>
Agregar Nueva Nota
</button>
</div>
{isInputVisible && (
<div className={styles.inputContainer}>
<input
className={styles.input}
placeholder="Ingresa el título..."
onChange={(e) => setNoteTitle(e.target.value)}
value={noteTitle}
/>
<div className={styles.ReactQuill}>
<ReactQuill onChange={addDesc} value={noteDesc} />
</div>
<button onClick={saveNote} className={styles.saveBtn}>
Guardar Nota
</button>
</div>
)}
</>
);
}
Añade los estilos para el botón de guardar y el editor React Quill en Evernote.module.scss
:
.saveBtn {
width: 15rem;
height: 2rem;
cursor: pointer;
background-color: $dark-red;
color: $whiteSmoke;
border: $dark-red;
font-family: "Roboto";
}
.ReactQuill {
width: 15rem;
}
Este código permite crear una nota con un título y contenido, guardándola en Firestore. El editor React Quill proporciona un área de texto enriquecido para el contenido de la nota.
Obtener y Mostrar Notas
Para mostrar las notas almacenadas, actualiza NoteOperations.js
para obtener datos de Firestore:
import styles from "../../styles/Evernote.module.scss";
import { useState, useEffect } from "react";
import { app, database } from "../../firebaseConfig";
import { collection, addDoc, getDocs } from "firebase/firestore";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
const dbInstance = collection(database, "notes");
export default function NoteOperations({ getSingleNote }) {
const [isInputVisible, setInputVisible] = useState(false);
const [noteTitle, setNoteTitle] = useState("");
const [noteDesc, setNoteDesc] = useState("");
const [notesArray, setNotesArray] = useState([]);
const inputToggle = () => {
setInputVisible(!isInputVisible);
};
const addDesc = (value) => {
setNoteDesc(value);
};
const getNotes = () => {
getDocs(dbInstance).then((data) => {
setNotesArray(
data.docs.map((item) => ({
...item.data(),
id: item.id,
}))
);
});
};
const saveNote = () => {
addDoc(dbInstance, {
noteTitle,
noteDesc,
}).then(() => {
setNoteTitle("");
setNoteDesc("");
getNotes();
});
};
useEffect(() => {
getNotes();
}, []);
return (
<>
<div className={styles.btnContainer}>
<button onClick={inputToggle} className={styles.button}>
Agregar Nueva Nota
</button>
</div>
{isInputVisible && (
<div className={styles.inputContainer}>
<input
className={styles.input}
placeholder="Ingresa el título..."
onChange={(e) => setNoteTitle(e.target.value)}
value={noteTitle}
/>
<div className={styles.ReactQuill}>
<ReactQuill onChange={addDesc} value={noteDesc} />
</div>
<button onClick={saveNote} className={styles.saveBtn}>
Guardar Nota
</button>
</div>
)}
<div className={styles.notesDisplay}>
{notesArray.map((note) => (
<div
key={note.id}
className={styles.notesInner}
onClick={() => getSingleNote(note.id)}
>
<h4>{note.noteTitle}</h4>
</div>
))}
</div>
</>
);
}
Añade los estilos correspondientes en Evernote.module.scss
:
.notesDisplay {
margin-top: 1rem;
}
.notesInner {
margin-top: 0.5rem;
border: 1px solid $dark-red;
border-radius: 10px;
width: 15rem;
text-align: center;
cursor: pointer;
font-family: "Roboto";
}
.notesInner:hover {
background-color: $dark-red;
color: $whiteSmoke;
}
Este código obtiene todas las notas de Firestore al cargar la página y las muestra como una lista de títulos en la columna izquierda. Al hacer clic en una nota, se llama a la función getSingleNote
para mostrar sus detalles.
Mostrar Detalles de una Nota
Crea un componente NoteDetails.js
en el directorio components
:
import { useState, useEffect } from "react";
import { app, database } from "../../firebaseConfig";
import { doc, getDoc, getDocs, collection } from "firebase/firestore";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import styles from "../../styles/Evernote.module.scss";
const dbInstance = collection(database, "notes");
export default function NoteDetails({ ID }) {
const [singleNote, setSingleNote] = useState({});
const getSingleNote = async () => {
if (ID) {
const singleNoteRef = doc(database, "notes", ID);
const data = await getDoc(singleNoteRef);
setSingleNote({ ...data.data(), id: data.id });
}
};
const getNotes = () => {
getDocs(dbInstance).then((data) => {
setSingleNote(
data.docs.map((item) => ({
...item.data(),
id: item.id,
}))[0]
);
});
};
useEffect(() => {
getNotes();
}, []);
useEffect(() => {
getSingleNote();
}, [ID]);
return (
<>
<h2>{singleNote.noteTitle}</h2>
<div dangerouslySetInnerHTML={{ __html: singleNote.noteDesc }} />
</>
);
}
Actualiza index.js
para incluir el componente NoteDetails
y manejar la selección de notas:
import Head from "next/head";
import styles from "../styles/Home.module.css";
import NoteOperations from "./components/NoteOperations";
import NoteDetails from "./components/NoteDetails";
import { useState } from "react";
export default function Home() {
const [ID, setID] = useState(null);
const getSingleNote = (id) => {
setID(id);
};
return (
<div className={styles.container}>
<Head>
<title>Evernote Clone</title>
<meta
name="description"
content="Clon de Evernote con Next.js"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<div className={styles.container}>
<div className={styles.left}>
<NoteOperations getSingleNote={getSingleNote} />
</div>
<div className={styles.right}>
<NoteDetails ID={ID} />
</div>
</div>
</main>
</div>
);
}
Este código muestra el título y contenido de una nota seleccionada en la columna derecha, utilizando dangerouslySetInnerHTML
para renderizar el contenido enriquecido de React Quill.
Edición y Eliminación de Notas
Actualiza NoteDetails.js
para agregar funcionalidades de edición y eliminación:
import { useState, useEffect } from "react";
import { app, database } from "../../firebaseConfig";
import {
doc,
getDoc,
getDocs,
collection,
updateDoc,
deleteDoc,
} from "firebase/firestore";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import styles from "../../styles/Evernote.module.scss";
const dbInstance = collection(database, "notes");
export default function NoteDetails({ ID }) {
const [singleNote, setSingleNote] = useState({});
const [isEdit, setIsEdit] = useState(false);
const [noteTitle, setNoteTitle] = useState("");
const [noteDesc, setNoteDesc] = useState("");
const getSingleNote = async () => {
if (ID) {
const singleNoteRef = doc(database, "notes", ID);
const data = await getDoc(singleNoteRef);
setSingleNote({ ...data.data(), id: data.id });
}
};
const getNotes = () => {
getDocs(dbInstance).then((data) => {
setSingleNote(
data.docs.map((item) => ({
...item.data(),
id: item.id,
}))[0]
);
});
};
const getEditData = () => {
setIsEdit(true);
setNoteTitle(singleNote.noteTitle);
setNoteDesc(singleNote.noteDesc);
};
const editNote = (id) => {
const collectionById = doc(database, "notes", id);
updateDoc(collectionById, {
noteTitle,
noteDesc,
}).then(() => {
window.location.reload();
});
};
const deleteNote = (id) => {
const collectionById = doc(database, "notes", id);
deleteDoc(collectionById).then(() => {
window.location.reload();
});
};
useEffect(() => {
getNotes();
}, []);
useEffect(() => {
getSingleNote();
}, [ID]);
return (
<>
<h2>{singleNote.noteTitle}</h2>
<div dangerouslySetInnerHTML={{ __html: singleNote.noteDesc }} />
<div>
<button className={styles.editBtn} onClick={getEditData}>
Editar
</button>
<button
className={styles.deleteBtn}
onClick={() => deleteNote(singleNote.id)}
>
Eliminar
</button>
</div>
{isEdit && (
<div className={styles.inputContainer}>
<input
className={styles.input}
placeholder="Ingresa el título..."
onChange={(e) => setNoteTitle(e.target.value)}
value={noteTitle}
/>
<div className={styles.ReactQuill}>
<ReactQuill onChange={setNoteDesc} value={noteDesc} />
</div>
<button
onClick={() => editNote(singleNote.id)}
className={styles.saveBtn}
>
Actualizar Nota
</button>
</div>
)}
</>
);
}
Añade los estilos para los botones de edición y eliminación en Evernote.module.scss
:
.editBtn,
.deleteBtn {
width: 5rem;
height: 2rem;
background-color: $dark-red;
color: $whiteSmoke;
border: none;
cursor: pointer;
margin: 10px 10px 10px 0;
}
Este código permite editar una nota existente, mostrando un formulario con los datos actuales, y eliminar una nota, actualizando la interfaz mediante una recarga de página.
Conclusiones
En este tutorial, hemos construido un clon funcional de Evernote utilizando Next.js y Firebase. Desde la configuración inicial del proyecto hasta la implementación de funciones para crear, leer, editar y eliminar notas, hemos cubierto los aspectos esenciales de una aplicación web moderna. Next.js proporciona un framework robusto para el renderizado del lado del servidor, Firebase ofrece una base de datos en la nube escalable, y SASS permite crear estilos reutilizables y mantenibles. Este proyecto es un punto de partida que puedes mejorar con un diseño más avanzado o funcionalidades adicionales, como autenticación de usuarios o búsqueda de notas. El código fuente está disponible en el repositorio de GitHub para referencia: https://github.com/nishant-666/Evernote-Next-Alt
.