Skip to content

Commit

Permalink
feat(frontend): ✨ rich text editor for description
Browse files Browse the repository at this point in the history
lexical rich text editor implemented
  • Loading branch information
lazaronazareno committed Sep 10, 2024
1 parent d8bcdb9 commit df1aae1
Show file tree
Hide file tree
Showing 22 changed files with 1,301 additions and 20 deletions.
16 changes: 11 additions & 5 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@
"deploy": "vercel deploy --prod --token=$VERCEL_TOKEN"
},
"dependencies": {
"@lexical/html": "^0.17.1",
"@lexical/link": "^0.17.1",
"@lexical/list": "^0.17.1",
"@lexical/react": "^0.17.1",
"@lexical/rich-text": "^0.17.1",
"lexical": "^0.17.1",
"mapbox-gl": "^3.6.0",
"next": "14.2.6",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.3.0",
"sharp": "^0.33.5",
"zod": "^3.23.8",
"mapbox-gl": "^3.6.0",
"react-map-gl": "^7.1.7",
"swiper": "^11.1.12"
"sharp": "^0.33.5",
"swiper": "^11.1.12",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/mapbox-gl": "^3.4.0",
Expand All @@ -31,4 +37,4 @@
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
}
4 changes: 4 additions & 0 deletions frontend/public/icons/arrow-clockwise.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions frontend/public/icons/arrow-counterclockwise.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions frontend/public/icons/journal-text.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/justify.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/text-center.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/text-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/text-paragraph.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/text-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/type-bold.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/type-italic.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/type-strikethrough.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/public/icons/type-underline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions frontend/src/actions/albumActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ export async function createAlbumAction(
markerCoordinates: CountryWithoutDescription | null,
tags: string[],
token: string | null,
description: string,
prevState: any,
formData: FormData
) {
const title = formData.get('title') as string
const description = formData.get('description') as string
console.log(token)
const fields = {
title: title,
description: description,
Expand Down
29 changes: 16 additions & 13 deletions frontend/src/components/CreateAlbumForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Country, PhotoFromAlbum } from "@/interfaces/album"
import { TagsInput } from "./TagsInput"
import ImageDescription from "./ImageInput"
import { useRouter } from "next/navigation"
import LexicalEditor from "./LexicalEditor"
import { EditorState } from "lexical"


const INITIAL_STATE = {
Expand All @@ -21,13 +23,15 @@ export default function CreateAlbumForm() {
const [markerCoordinates, setMarkerCoordinates] = useState<Country | null>(null)
const [albumImages, setAlbumImages] = useState<PhotoFromAlbum[]>([])
const [tags, setTags] = useState<string[]>([])
const [description, setDescription] = useState<string>("");

/* const [confirmSend, setConfirmSend] = useState<boolean>(false) */
//cambiar por una cookie
let token = null
if (typeof window !== 'undefined') {
token = localStorage.getItem('token')
}
const createAlbumComplete = createAlbumAction.bind(null, albumImages, markerCoordinates, tags, token)
const createAlbumComplete = createAlbumAction.bind(null, albumImages, markerCoordinates, tags, token, description)
const [formState, formAction] = useFormState(
createAlbumComplete,
INITIAL_STATE
Expand Down Expand Up @@ -65,9 +69,15 @@ export default function CreateAlbumForm() {
)
}

//asi es como se manejaria la descripcion del json para poder renderizarla y verla como se edito
const handleEditorChange = (editorState: EditorState) => {
const json = editorState.toJSON()
setDescription(JSON.stringify(json))
}

useEffect(() => {
if (formState.success) {
router.push(`/feed`)
/* router.push(`/feed`) */
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formState?.success]);
Expand All @@ -94,21 +104,14 @@ export default function CreateAlbumForm() {
<SelectMap markerCoordinates={markerCoordinates} setMarkerCoordinates={setMarkerCoordinates} tags={tags} setTags={setTags} />

<div className="flex items-end justify-between">
<label htmlFor="description">Describe tu viaje</label>
<input
className="border-b border-gray-400 focus-visible:outline-none"
type="text"
id="description"
name="description"
required
/>
<LexicalEditor onChange={handleEditorChange} />
</div>
{formState?.errors?.description && <p className="-mt-4 mx-4 text-end text-red-500 text-xs">{formState?.errors?.description}</p>}

<div className="w-full flex justify-center">
<label htmlFor="doc" className="relative flex flex-col items-center p-4 w-[296px] h-24 rounded-3xl border border-gray-400 border-dashed bg-gray-100 cursor-pointer">
<Image className="h-9 w-auto" width={360} height={360} src={ImagesIcon} alt="image icon" />
<h4 className="text-xl font-medium text-gray-700">Subir Imágenes</h4>
<Image className="h-9 w-auto cursor-pointer" width={360} height={360} src={ImagesIcon} alt="image icon" />
<h4 className="text-xl font-medium text-gray-700 cursor-pointer">Subir Imágenes</h4>
<input
type='file'
name='albumImages'
Expand Down Expand Up @@ -175,7 +178,7 @@ export default function CreateAlbumForm() {
</div>
)} */}

<SubmitButton className="text-xl bg-slate-400 rounded h-12 mx-4 mb-4 text-white shadow-[0_4px_4px_0px_rgba(0,0,0,0.15)]" loadingText="Cargando..." text="Guardar" />
<SubmitButton className="text-xl rounded h-12 mx-4 mb-4 text-white shadow-[0_4px_4px_0px_rgba(0,0,0,0.15)]" loadingText="Cargando..." text="Guardar" />
{formState?.createAlbumError && <p className="-mt-4 mx-4 text-end text-red-500 text-xs">{formState?.message}: {formState?.createAlbumError}</p>}
{formState?.success && <p className="-mt-4 mx-4 text-end text-green-500 text-xs">{formState?.success}</p>}
</form>
Expand Down
45 changes: 45 additions & 0 deletions frontend/src/components/LexicalEditor/ReadOnly.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { $generateHtmlFromNodes } from '@lexical/html'
import { LexicalComposer } from '@lexical/react/LexicalComposer'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { useEffect, useState } from 'react'

function ConvertToHtml({ savedContent }: { savedContent: string }) {
const [editor] = useLexicalComposerContext()
const [htmlContent, setHtmlContent] = useState<string>('')

useEffect(() => {
editor.update(() => {
const editorState = editor.parseEditorState(savedContent)
const html = $generateHtmlFromNodes(editor, null)
setHtmlContent(html)
})
}, [editor, savedContent])

return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />
}

const theme = {
}

const ReadOnlyEditor = ({ savedContent }: { savedContent: string }) => {
const editorConfig = {
namespace: 'ohmytrip description',
nodes: [],
theme: theme,
onError(error: Error) {
throw error
},
}

return (
<LexicalComposer initialConfig={editorConfig}>
<div className="editor-container">
<div className="editor-inner bg-[#868f7a8a]">
<ConvertToHtml savedContent={savedContent} />
</div>
</div>
</LexicalComposer>
)
}

export default ReadOnlyEditor
156 changes: 156 additions & 0 deletions frontend/src/components/LexicalEditor/Toolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$getSelection,
$isRangeSelection,
FORMAT_TEXT_COMMAND,
FORMAT_ELEMENT_COMMAND,
$isTextNode,
LexicalEditor,
} from 'lexical';
import { ChangeEvent, useState } from 'react';
import {
INSERT_ORDERED_LIST_COMMAND,
INSERT_UNORDERED_LIST_COMMAND,
INSERT_CHECK_LIST_COMMAND,
REMOVE_LIST_COMMAND
} from "@lexical/list";

const blockTypeToBlockName = {
bullet: "Bulleted List",
number: "Numbered List",
check: "Check List",
paragraph: "Normal"
};

const Toolbar: React.FC = () => {
const [editor] = useLexicalComposerContext();
const [blockType, setBlockType] = useState<keyof typeof blockTypeToBlockName>(
"paragraph"
);

const formatList = (listType) => {
console.log(blockType);
if (listType === "number" && blockType !== "number") {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
setBlockType("number");
} else if (listType === "bullet" && blockType !== "bullet") {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
setBlockType("bullet");
} else if (listType === "check" && blockType !== "check") {
editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined);
setBlockType("check");
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
setBlockType("paragraph");
}
};

const applyTextFormat = (formatType: 'bold' | 'italic' | 'underline' | 'strikethrough') => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, formatType);
}
});
};

const applyElementFormat = (formatType: 'left' | 'center' | 'right' | 'justify') => {
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, formatType);
};

const changeFontSize = (event: ChangeEvent<HTMLSelectElement>) => {
const fontSize = event.target.value;
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.getNodes().forEach(node => {
if ($isTextNode(node)) {
// Aplicar el estilo del tamaño de fuente
node.setStyle(`font-size: ${fontSize}px`);
}
});
}
});
};

const changeFontFamily = (event: ChangeEvent<HTMLSelectElement>) => {
const fontFamily = event.target.value;
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.getNodes().forEach(node => {
if ($isTextNode(node)) {
// Aplicar el estilo de la fuente
node.setStyle(`font-family: ${fontFamily}`);
}
});
}
});
};

return (
<div className="toolbar">
{/* Formatos de texto */}
<button type='button' onClick={() => applyTextFormat('bold')}>Negrita</button>
<button type='button' onClick={() => applyTextFormat('italic')}>Cursiva</button>
<button type='button' onClick={() => applyTextFormat('underline')}>Subrayado</button>
<button type='button' onClick={() => applyTextFormat('strikethrough')}>Tachado</button>

{/* Alineación */}
<button type='button' onClick={() => applyElementFormat('left')}>Alinear Izquierda</button>
<button type='button' onClick={() => applyElementFormat('center')}>Centrar</button>
<button type='button' onClick={() => applyElementFormat('right')}>Alinear Derecha</button>
<button type='button' onClick={() => applyElementFormat('justify')}>Justificar</button>

{/* Tamaño de fuente */}
<label htmlFor="fontSize">Tamaño de letra:</label>
<select id="fontSize" onChange={changeFontSize}>
<option value="12">12</option>
<option value="14">14</option>
<option value="16">16</option>
<option value="18">18</option>
<option value="24">24</option>
<option value="32">32</option>
</select>

{/* Familia de fuente */}
<label htmlFor="fontFamily">Fuente:</label>
<select id="fontFamily" onChange={changeFontFamily}>
<option value="Arial">Arial</option>
<option value="Courier New">Courier New</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Verdana">Verdana</option>
</select>

<div className="toolbar">
<button
type='button'
disabled={false}
className={"toolbar-item spaced"}
onClick={() => formatList("bullet")}
>
<span className="text">Bullet List</span>
</button>
<button
type='button'
disabled={false}
className={"toolbar-item spaced"}
onClick={() => formatList("number")}
>
<span className="text">Numbered List</span>
</button>
<button
type='button'
disabled={false}
className={"toolbar-item spaced"}
onClick={() => formatList("check")}
>
<span className="text">Check List</span>
</button>
</div>
</div>
);
};

export default Toolbar;
Loading

0 comments on commit df1aae1

Please sign in to comment.