Skip to content
This repository has been archived by the owner on Dec 18, 2024. It is now read-only.

Commit

Permalink
feat: add editable texts & news banner
Browse files Browse the repository at this point in the history
  • Loading branch information
LarsSelbekk committed Jan 20, 2024
1 parent 8fc0c7a commit e1221c7
Show file tree
Hide file tree
Showing 17 changed files with 646 additions and 45 deletions.
3 changes: 3 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ module.exports = {
"error",
{
replacements: {
prop: false,
props: false,
ref: false,
},
},
],
"unicorn/no-null": "off",
// .textContent is very different from .innerText, not interchangeable
"unicorn/prefer-dom-node-text-content": "off",

/** @see https://medium.com/weekly-webtips/how-to-sort-imports-like-a-pro-in-typescript-4ee8afd7258a */
"import/order": [
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,25 @@
"react-hook-form": "^7.42.0",
"react-jwt": "^1.1.8",
"react-qr-barcode-scanner": "^1.0.6",
"react-quill": "^2.0.0",
"react-redux": "^8.0.5",
"sanitize-html": "^2.11.0",
"sharp": "^0.31.3",
"string-similarity": "^4.0.4",
"swr": "^2.0.0",
"validator": "^13.7.0"
},
"devDependencies": {
"@boklisten/bl-model": "^0.25.16",
"@boklisten/bl-model": "^0.25.30",
"@commitlint/cli": "^17.7.1",
"@commitlint/config-conventional": "^17.7.0",
"@testing-library/react": "^13.4.0",
"@types/draft-js": "^0.11.10",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-draft-wysiwyg": "^1.13.4",
"@types/react-draft-wysiwyg": "^1.13.5",
"@types/react-redux": "^7.1.25",
"@types/sanitize-html": "^2.9.5",
"@types/string-similarity": "^4.0.0",
"@types/validator": "^13.7.10",
"@typescript-eslint/eslint-plugin": "^6.6.0",
Expand Down
51 changes: 36 additions & 15 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ export const get = async <T = any>(
headers: getHeaders(),
})
.catch(async (error) => {
if (error.status === 404 || error.response.status === 404) {
throw new Error("Not found");
if ((error as AxiosError)?.response?.status === 404) {
throw new NotFoundError(`API request resulted in 404: ${url}`);
}
console.log(error);

console.error(error);
if (
(error.status === 401 || error.response.status === 401) &&
(error?.status === 401 || error?.response?.status === 401) &&
!noRetryTokens
) {
await fetchNewTokens();
return await get(url, query, true);
}

throw new Error("Unknown API error");
throw new Error(`Unknown API error: ${error}`);
});
};

Expand All @@ -45,6 +44,36 @@ export const add = async (collection: string, data: unknown) => {
});
};

export const put = async <T = unknown>(
url: string,
data: T,
noRetryTokens?: boolean,
): Promise<void> => {
if (!url || url.length === 0) {
throw new Error("url is undefined");
}

await axios
.put<T>(apiPath(url), data, {
headers: getHeaders(),
})
.catch(async (error) => {
if (
(error.status === 401 || error.response.status === 401) &&
!noRetryTokens
) {
await fetchNewTokens();
return await put(url, data, true);
}

if ((error as AxiosError).response?.status === 404) {
throw new NotFoundError(`API request resulted in 404: ${url}`);
}

throw new Error("Unknown API error", error);
});
};

export const addWithEndpoint = async (
collection: string,
endpoint: string,
Expand Down Expand Up @@ -72,13 +101,5 @@ export class NotFoundError extends Error {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiFetcher = async <T = any>(url: string): Promise<T> => {
try {
return await get<{ data: T }>(url).then((response) => response.data.data);
} catch (error) {
if (!((error as AxiosError).response?.status === 404)) {
throw error;
}

throw new NotFoundError(`API request resulted in 404: ${url}`);
}
return await get<{ data: T }>(url).then((response) => response.data.data);
};
171 changes: 171 additions & 0 deletions src/components/editableText/EditableTextEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import "react-quill/dist/quill.snow.css";

import { Box, Button, Container, styled } from "@mui/material";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import React, { useRef, useState } from "react";
import ReactQuill from "react-quill";

import { put } from "api/api";
import { EditorProps } from "components/editableText/EditableTextElement";
import { EditableTextRenderer } from "components/editableText/EditableTextRenderer";
import BL_CONFIG from "utils/bl-config";
import useExitInterceptor from "utils/useExitInterceptor";

const Quill = styled(
dynamic<ReactQuill.ReactQuillProps>(import("react-quill"), { ssr: false }),
)({});

export const EditableTextEditor = ({ editableText }: EditorProps) => {
const initialValue = editableText.text ?? "";

const editorState = useRef<string>(initialValue);
const editorRef = useRef<HTMLDivElement>(null);

const [readOnly, setReadOnly] = useState(true);

const router = useRouter();
useExitInterceptor(!readOnly);

const onEdit = () => {
setReadOnly(false);
};

const onEditorSave = async () => {
if (!(await router.replace(router.asPath))) {
throw new Error("Unable to refresh");
}
};

const onSave = () => {
setReadOnly(true);
if (editorRef.current?.innerText.trim().length === 0) {
editorState.current = "";
}
put(`${BL_CONFIG.collection.editableText}/${editableText.id}/`, {
...editableText,
text: editorState.current,
})
.then(async () => {
await onEditorSave();
return;
})
.catch((error) => {
throw new Error("Failed to save editable text", { cause: error });
});
};

const onCancel = () => {
editorState.current = initialValue;
setReadOnly(true);
};

return (
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: 1,
padding: 0,
}}
data-testid="editor"
>
{readOnly ? (
<Button data-testid="edit-button" onClick={onEdit}>
Rediger
</Button>
) : (
<Container
sx={{
display: "flex",
flexDirection: "row",
gap: 1,
justifyContent: "center",
}}
>
<Button
data-testid="cancel-button"
onClick={onCancel}
color="warning"
variant="outlined"
>
Avbryt
</Button>
<Button
data-testid="save-button"
onClick={onSave}
variant="contained"
>
Lagre
</Button>
</Container>
)}
<Container
ref={editorRef}
sx={{
display: readOnly ? "none" : undefined,
}}
>
<Quill
formats={quillFormats}
modules={quillModules}
placeholder="Rediger meg ..."
defaultValue={initialValue}
onChange={(changedState) => {
editorState.current = changedState;
}}
sx={{
width: "100%",
"& .ql-editor": {
minHeight: "10em",
},
}}
/>
</Container>
{readOnly && <EditableTextRenderer editableText={editableText} />}
</Box>
);
};

const quillModules = {
toolbar: [
[{ header: "1" }, { header: "2" }, { header: "3" }],
// Disabled until bl-web no longer needs to be supported
// [{ size: [] }],
["bold", "italic", "underline", "strike", "blockquote"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link"],
// Disabled until bl-web no longer needs to be supported
// [{ align: [] }],
["clean"],
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
};

/*
* Quill editor formats
* See https://quilljs.com/docs/formats/
*/
const quillFormats = [
"header",
"size",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"indent",
"align",
"link",
];
25 changes: 25 additions & 0 deletions src/components/editableText/EditableTextElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";

import "react-draft-wysiwyg/dist/react-draft-wysiwyg.css";

import { isAdmin } from "api/auth";
import { EditableTextEditor } from "components/editableText/EditableTextEditor";
import { EditableTextRenderer } from "components/editableText/EditableTextRenderer";
import { MaybeEmptyEditableText } from "utils/types";
import useIsHydrated from "utils/useIsHydrated";

export interface EditorProps {
editableText: MaybeEmptyEditableText;
}

const EditableTextElement = ({ editableText }: EditorProps) => {
const hydrated = useIsHydrated();

if (hydrated && isAdmin()) {
return <EditableTextEditor editableText={editableText} />;
}

return <EditableTextRenderer editableText={editableText} />;
};

export default EditableTextElement;
26 changes: 26 additions & 0 deletions src/components/editableText/EditableTextRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import "react-quill/dist/quill.core.css";
import { Box } from "@mui/material";
import React from "react";

import { sanitizeQuillHtml } from "utils/sanitizeHtml";
import { MaybeEmptyEditableText } from "utils/types";

export const EditableTextRenderer = ({
editableText,
}: {
editableText: MaybeEmptyEditableText;
}) => {
if (!editableText.text) {
return null;
}
const content = sanitizeQuillHtml(editableText.text);
return (
<Box
className="ql-editor"
sx={{
width: "fit-content",
}}
dangerouslySetInnerHTML={{ __html: content }}
/>
);
};
34 changes: 34 additions & 0 deletions src/components/editableText/NewsBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Box } from "@mui/material";
import { ComponentProps } from "react";

import EditableTextElement from "components/editableText/EditableTextElement";
import theme from "utils/theme";

const NewsBanner = (props: ComponentProps<typeof EditableTextElement>) => {
if (
props.editableText.text === null ||
props.editableText.text.length === 0
) {
return <EditableTextElement {...props} />;
}
return (
<Box
sx={{
borderColor: theme.palette.warning.main,
borderWidth: 2,
borderStyle: "solid",
borderRadius: 1,
backgroundColor: theme.palette.warning.light,
color: theme.palette.warning.contrastText,
padding: 1,
my: 5,
mx: "auto",
width: "fit-content",
}}
>
<EditableTextElement {...props} />
</Box>
);
};

export default NewsBanner;
Loading

0 comments on commit e1221c7

Please sign in to comment.