Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Admin managing posts feature added successfully issue 339 #406

Merged
merged 4 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion admin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
},
"dependencies": {
"axios": "^1.7.2",
"dompurify": "^3.1.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.4.1",
"react-icons": "^5.2.1",
"react-router-dom": "^6.24.0",
"react-switch": "^7.0.0",
"recoil": "^0.7.7"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.13.1",
Expand Down
9 changes: 9 additions & 0 deletions admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Profile from "./pages/Profile";
import Users from "./pages/Users";
import Posts from "./pages/Posts";
import { Toaster } from "react-hot-toast";
import UpdatePost from "./components/UpdatePost";
// import axios from "axios";
// axios.defaults.baseURL = "http://localhost:3001/";

Expand Down Expand Up @@ -59,6 +60,14 @@ function App() {
</AuthenticatedRoute>
}
/>
<Route
path="/admin/update-post/:postId"
element={
<AuthenticatedRoute>
<UpdatePost />
</AuthenticatedRoute>
}
/>
<Route path="*" element={<PageNotFound />} />
</Routes>
<Toaster/>
Expand Down
95 changes: 95 additions & 0 deletions admin/src/components/CodeEditorAndPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useState } from 'react';
import PostPreview from "../components/PostPreview";
import Switch from "react-switch";
import toast from "react-hot-toast";

type Props = {
codeSnippet: string;
jsCodeSnippet: string;
setCodeSnippet: (codeSnippet: string) => void;
setJsCodeSnippet: (jsCodeSnippet: string) => void;
};

function CodeEditorAndPreview({
codeSnippet,
jsCodeSnippet,
setCodeSnippet,
setJsCodeSnippet
}: Props) {
const [isPreviewMode, setIsPreviewMode] = useState(false);

const handleToggle = () => {
setIsPreviewMode(!isPreviewMode);
};

const handleTextAreaClick = () => {
if (isPreviewMode) {
toast('Please disable preview mode to edit the code snippet', {
icon: '🚫',
});
}
};


return (
<div>
<div>
<label htmlFor="codeSnippet" className="block text-gray-700 text-sm font-semibold">
HTML
</label>
<textarea
id="codeSnippet"
value={codeSnippet}
onChange={(e) => setCodeSnippet(e.target.value)}
className="mt-1 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
readOnly={isPreviewMode}
onClick={handleTextAreaClick}
rows={5}
></textarea>
</div>
<div>
<label htmlFor="jsCodeSnippet" className="block text-gray-700 text-sm font-semibold">
Javascript
</label>
<textarea
id="jsCodeSnippet"
value={jsCodeSnippet}
onChange={(e) => setJsCodeSnippet(e.target.value)}
className="mt-1 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
readOnly={isPreviewMode}
onClick={handleTextAreaClick}
rows={4}
></textarea>
</div>
<div className="flex items-center mt-2 mb-2">
<label className="flex items-center">
<Switch
onChange={handleToggle}
checked={isPreviewMode}
offColor="#888"
onColor="#080"
uncheckedIcon={false}
checkedIcon={false}
disabled={codeSnippet == ""}
height={16}
width={32}
className="mr-2"
/>
<span className="text-gray-700 text-sm font-semibold">
{isPreviewMode ? 'Edit Code' : 'Preview Component'}
</span>
</label>
</div>
{isPreviewMode ? (
<PostPreview
jsCodeSnippet={jsCodeSnippet}
codeSnippet={codeSnippet}
/>
) : null}

</div>
);

}

export default CodeEditorAndPreview;
88 changes: 88 additions & 0 deletions admin/src/components/PostPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useEffect, useRef, useState } from "react";
import DOMPurify from "dompurify"

function PostPreview({
codeSnippet,
jsCodeSnippet,
}: {
codeSnippet: string;
jsCodeSnippet: string;
}) {
const ref = useRef<HTMLIFrameElement>(null);
const [height, setHeight] = useState("100px");

useEffect(() => {
const handleResize = (event: MessageEvent) => {
if (event.data.type === "setHeight") {
setHeight(event.data.height + "px");
}
};

window.addEventListener("message", handleResize);
return () => {
window.removeEventListener("message", handleResize);
};
}, []);

DOMPurify.addHook("uponSanitizeElement", (node, data) => {
if (data.tagName === "img" || data.tagName === "div") {
const src = node.getAttribute("src");
const style = node.getAttribute("style");
if (src && src.startsWith("http")) {
node.setAttribute("src", src);
}
if (style && style.includes("url(")) {
node.setAttribute("style", style);
}
}
});

const sanitizedSnippet = DOMPurify.sanitize(codeSnippet || "", {
ADD_ATTR: ["style", "background"],
});

return (
<div className="p-4 text-[#000435] bg-white dark:text-white dark:bg-[#fff] z-0 overflow-hidden rounded border-2 border-sky-400">
<iframe
ref={ref}
className="w-full border-0"
srcDoc={getPreviewTemplate(sanitizedSnippet, jsCodeSnippet)}
title="Preview"
sandbox="allow-scripts allow-modals"
style={{ minHeight: height, maxWidth: "100%" }}
/>
</div>
);
}

function getPreviewTemplate(sanitizedSnippet: string, jsCodeSnippet: string) {
return `
<!DOCTYPE html>
<html class='flex w-full h-full'>
<head>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
<script>
document.addEventListener('DOMContentLoaded', function() {
const setHeight = () => {
const height = document.body.scrollHeight;
window.parent.postMessage({ type: 'setHeight', height }, '*');
};

setHeight();

const observer = new MutationObserver(setHeight);
observer.observe(document.body, { childList: true, subtree: true });

window.addEventListener('beforeunload', () => observer.disconnect());
});
</script>
</head>
<body class='w-full h-full flex items-center justify-center min-w-full min-h-full'>
<div class='w-full h-full p-6'>${sanitizedSnippet}</div>
${jsCodeSnippet ? `<script defer>${jsCodeSnippet}</script>` : ""}
</body>
</html>
`;
}

export default PostPreview;
Loading
Loading