Restructured frontend, data design, and created the "frontend" for the backend
This commit is contained in:
parent
511b3fe625
commit
99720dd46d
1
frontend/.env.editableview
Normal file
1
frontend/.env.editableview
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_VIEW_TYPE=editableview
|
||||||
1
frontend/.env.view
Normal file
1
frontend/.env.view
Normal file
@ -0,0 +1 @@
|
|||||||
|
VITE_APP_VIEW_TYPE=view
|
||||||
859
frontend/package-lock.json
generated
859
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -4,8 +4,10 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0 --port 3000",
|
"dev:view": "REACT_APP_VIEW_TYPE=view vite --host 0.0.0.0 --port 3000 --mode view",
|
||||||
"build": "vite build",
|
"dev:editableview": "REACT_APP_VIEW_TYPE=editableview vite --host 0.0.0.0 --port 3000 --mode editableview",
|
||||||
|
"build:view": "REACT_APP_VIEW_TYPE=view vite build",
|
||||||
|
"build:editableview": "REACT_APP_VIEW_TYPE=editableview vite build",
|
||||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
@ -13,6 +15,17 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@tiptap/extension-blockquote": "^2.3.2",
|
||||||
|
"@tiptap/extension-color": "^2.3.2",
|
||||||
|
"@tiptap/extension-highlight": "^2.3.2",
|
||||||
|
"@tiptap/extension-image": "^2.3.2",
|
||||||
|
"@tiptap/extension-link": "^2.3.2",
|
||||||
|
"@tiptap/extension-list-item": "^2.3.2",
|
||||||
|
"@tiptap/extension-text-align": "^2.3.2",
|
||||||
|
"@tiptap/extension-text-style": "^2.3.2",
|
||||||
|
"@tiptap/extension-underline": "^2.3.2",
|
||||||
|
"@tiptap/react": "^2.3.2",
|
||||||
|
"@tiptap/starter-kit": "^2.3.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"bootstrap": "^5.3.3",
|
"bootstrap": "^5.3.3",
|
||||||
"html-react-parser": "^5.1.10",
|
"html-react-parser": "^5.1.10",
|
||||||
@ -20,7 +33,8 @@
|
|||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"reactstrap": "^9.2.2",
|
"reactstrap": "^9.2.2",
|
||||||
"sass": "^1.75.0"
|
"sass": "^1.75.0",
|
||||||
|
"tiptap": "^1.32.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.66",
|
"@types/react": "^18.2.66",
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "John Doe",
|
"name": "John Doe",
|
||||||
"greetingLine": "Hi! My name is",
|
"introContent": "<span>Write something about yourself here!</span>",
|
||||||
"tagLine": "<span>Me like tech. Checkout my blog where I have nice stuff!</span>",
|
|
||||||
"profilePhoto": "homepage/media/profile.png",
|
"profilePhoto": "homepage/media/profile.png",
|
||||||
"links": {
|
"builtWith": true
|
||||||
"instagram": ""
|
|
||||||
},
|
|
||||||
"contact":{
|
|
||||||
"email":"",
|
|
||||||
"phone": ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import {useEffect, useState} from 'react';
|
|||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
//Import Views
|
//Import Views
|
||||||
import Home from './components/home';
|
import Home from './components/viewable/home';
|
||||||
import CategoryList from './components/category-list';
|
import CategoryList from './components/viewable/category-list';
|
||||||
import BlogList from './components/blog-list';
|
import BlogList from './components/viewable/blog-list';
|
||||||
import Blog from './components/blog';
|
import Blog from './components/viewable/blog';
|
||||||
|
|
||||||
|
|
||||||
//Import Shared Views
|
//Import Shared Views
|
||||||
import Header from './components/shared/navbar';
|
import Header from './components/viewable/shared/navbar';
|
||||||
import Footer from './components/shared/footer';
|
import Footer from './components/viewable/shared/footer';
|
||||||
import Notification from './components/shared/notification';
|
import Notification from './components/viewable/shared/notification';
|
||||||
|
|
||||||
//Import Services
|
//Import Services
|
||||||
import DataService from './services/data-service'
|
import DataService from './services/data-service'
|
||||||
@ -53,7 +53,7 @@ function App() {
|
|||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
<Router>
|
<Router>
|
||||||
<Header className="header" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
<Header className="header" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||||
<Notification isOpen={isOpen} notificationMessage={notificationMessage} />
|
<Notification isOpen={isOpen} message={notificationMessage} />
|
||||||
<div className={`p-0 ${themeConfig[globalTheme].background}`}>
|
<div className={`p-0 ${themeConfig[globalTheme].background}`}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
||||||
|
|||||||
71
frontend/src/AppEditable.jsx
Normal file
71
frontend/src/AppEditable.jsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import './App.css';
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
|
//Import Views
|
||||||
|
import Home from './components/editable/home';
|
||||||
|
import CategoryList from './components/editable/category-list';
|
||||||
|
import BlogList from './components/editable/blog-list';
|
||||||
|
import Blog from './components/editable/blog';
|
||||||
|
|
||||||
|
|
||||||
|
//Import Shared Views
|
||||||
|
import Header from './components/editable/shared/navbar';
|
||||||
|
import Footer from './components/editable/shared/footer';
|
||||||
|
import Notification from './components/editable/shared/notification';
|
||||||
|
|
||||||
|
//Import Services
|
||||||
|
import DataService from './services/data-service'
|
||||||
|
|
||||||
|
function AppEditable() {
|
||||||
|
const [userData, setUserData] = useState(null);
|
||||||
|
const [themeConfig, setThemeConfig] = useState(null);
|
||||||
|
const [globalTheme, setGlobalTheme] = useState("lightTheme");
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [notificationMessage, setNotificationMessage] = useState("")
|
||||||
|
|
||||||
|
const notificationToggler = (message) => {
|
||||||
|
setIsOpen(true)
|
||||||
|
setNotificationMessage(message)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpen(false)
|
||||||
|
}, 3500)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData('shared/user-data').then( response =>
|
||||||
|
setUserData(response.data)
|
||||||
|
)
|
||||||
|
DataService.getData('shared/theme-config').then( response =>{
|
||||||
|
setThemeConfig(response.data)
|
||||||
|
setGlobalTheme(response.data.defaultTheme)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},[])
|
||||||
|
|
||||||
|
const themeSwitcher = (theme) => {
|
||||||
|
setGlobalTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeConfig)
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<Router>
|
||||||
|
<Header className="header" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||||
|
<Notification isOpen={isOpen} message={notificationMessage} />
|
||||||
|
<div className={`p-0 ${themeConfig[globalTheme].background}`}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
||||||
|
<Route path="/categories" element={<CategoryList notificationToggler={notificationToggler} GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
<Route path="/categories/:categoryID" element={<BlogList notificationToggler={notificationToggler} GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
<Route path="/blog/:blogID" element={<Blog notificationToggler={notificationToggler} GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
<Footer className="footer" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppEditable;
|
||||||
96
frontend/src/components/editable/blog-list.jsx
Normal file
96
frontend/src/components/editable/blog-list.jsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import DataService from '../../services/data-service';
|
||||||
|
import MediaService from '../../services/media-service';
|
||||||
|
import CardListViewer from './shared/card-list-viewer';
|
||||||
|
import CategoryBar from './shared/category-bar';
|
||||||
|
import {
|
||||||
|
Spinner,
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
CardImg,
|
||||||
|
CardTitle,
|
||||||
|
CardText,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
ButtonGroup
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
function BlogList(props) {
|
||||||
|
|
||||||
|
const { categoryID } = useParams();
|
||||||
|
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
|
||||||
|
const [categoryData, setCategoryData] = useState('loading');
|
||||||
|
const [featuredBlogData, setFeaturedBlogData] = useState('loading');
|
||||||
|
const [currentPage, setCurrentPage] = useState('loading');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData(`category/${categoryID}/category-data`).then(response =>{
|
||||||
|
setCategoryData(response.data);
|
||||||
|
if (response.data.featuredBlog){
|
||||||
|
DataService.getData(`blogs/${response.data.featuredBlog}/blog-data`).then(response =>
|
||||||
|
setFeaturedBlogData(response.data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
setFeaturedBlogData("nodata")
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [categoryID]);
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig) {
|
||||||
|
return (
|
||||||
|
<Container fluid className={` mb-2 p-0 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<CategoryBar currentPage={categoryID} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>
|
||||||
|
<Row className="justify-content-center align-items-center">
|
||||||
|
<Col className="d-flex flex-column align-items-center">
|
||||||
|
<div className="w-100">
|
||||||
|
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{"width": "100%"}}>
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle style={{ display: "grid" }} className={`${ThemeConfig[GlobalTheme].textColor} justify-content-center`} tag="h1">
|
||||||
|
{`Blogs in ${categoryData.name}`}<Button className='mt-2' outline>Add New</Button>
|
||||||
|
</CardTitle>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="" style={{ width: '70%', margin: 'auto', display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
<h3 className={`${ThemeConfig[GlobalTheme].textColor}`}>
|
||||||
|
{`All blogs`}
|
||||||
|
</h3>
|
||||||
|
{
|
||||||
|
categoryData === 'loading' ? <Spinner /> :
|
||||||
|
categoryData.blogMetadata.map((item, index) => (
|
||||||
|
<CardListViewer
|
||||||
|
key={item.id}
|
||||||
|
totalItems={categoryData.blogMetadata.length}
|
||||||
|
cardType={"longCard"}
|
||||||
|
resourceType={"blog"}
|
||||||
|
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||||
|
bgColor={ThemeConfig[GlobalTheme].background}
|
||||||
|
itemObject={item}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button outline>Save Data</Button>
|
||||||
|
<Button outline>Publish Data</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogList
|
||||||
165
frontend/src/components/editable/blog.jsx
Normal file
165
frontend/src/components/editable/blog.jsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import parse from 'html-react-parser';
|
||||||
|
|
||||||
|
import DataService from '../../services/data-service';
|
||||||
|
import MediaService from '../../services/media-service'
|
||||||
|
import CategoryBar from './shared/category-bar';
|
||||||
|
import EditorComponent from './shared/tiptap';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Container,Row, Col,Spinner, UncontrolledCollapse, Button, ButtonGroup, Card, CardBody
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
function Blog(props) {
|
||||||
|
|
||||||
|
const { blogID } = useParams();
|
||||||
|
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
|
||||||
|
const [blogData, setBlogData] = useState([]);
|
||||||
|
const [blogContent, setBlogContent] = useState()
|
||||||
|
|
||||||
|
const replace = (node) => {
|
||||||
|
if (node.type === 'tag') {
|
||||||
|
if (node.name === 'a') {
|
||||||
|
const newClasses = `${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`;
|
||||||
|
const existingClasses = node.attribs.class ? `${node.attribs.class} ` : '';
|
||||||
|
node.attribs.class = `${existingClasses}${newClasses}`;
|
||||||
|
node.attribs.rel = 'noopener noreferrer';
|
||||||
|
node.attribs.target = '_blank';
|
||||||
|
}
|
||||||
|
if (node.name === 'img') {
|
||||||
|
const newClasses = `img-fluid mt-2 mb-2 rounded`;
|
||||||
|
const existingClasses = node.attribs.class ? `${node.attribs.class} ` : '';
|
||||||
|
node.attribs.class = `${existingClasses}${newClasses}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData(`blogs/${blogID}/blog-data`).then(response =>{
|
||||||
|
setBlogData(response.data)
|
||||||
|
const parsedContent = parse(response.data.contentBody, { replace });
|
||||||
|
setBlogContent(parsedContent);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (blogData.contentBody){
|
||||||
|
const parsedContent = parse(blogData.contentBody, { replace });
|
||||||
|
setBlogContent(parsedContent);
|
||||||
|
}
|
||||||
|
}, [GlobalTheme])
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig && blogData) {
|
||||||
|
return (
|
||||||
|
<Container fluid className={`${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<CategoryBar currentPage={blogData.parentCategory} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>
|
||||||
|
<Row className="mb-4">
|
||||||
|
<Col className="p-0">
|
||||||
|
<img
|
||||||
|
src={MediaService.getMedia(blogData.coverImage)}
|
||||||
|
alt="Banner"
|
||||||
|
style={{ width: '100%', height: 'auto', maxHeight: '20vh', objectFit: 'cover' }}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row className="mr-2 ml-2 mb-2 mt-1 blogContent">
|
||||||
|
<Col xs="3" className="d-none d-md-block"></Col>
|
||||||
|
<Col xs={`${window.screen.width >= 765 ? '6':''}`}>
|
||||||
|
<h1 className={`${ThemeConfig[GlobalTheme].textColor}`}>{blogData.name}</h1>
|
||||||
|
<h4 className={`${ThemeConfig[GlobalTheme].textColor}`}>{blogData.description}</h4>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
id="toggler"
|
||||||
|
style={{
|
||||||
|
marginBottom: '1rem'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</Button>
|
||||||
|
<UncontrolledCollapse toggler="#toggler">
|
||||||
|
<Card style={{overflowX: 'auto'}}>
|
||||||
|
<CardBody>
|
||||||
|
<ButtonGroup
|
||||||
|
vertical
|
||||||
|
className="my-2"
|
||||||
|
>
|
||||||
|
<Button outline>
|
||||||
|
<Link className="p-3" to="#" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
|
props.notificationToggler("Link copied")
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
}}>
|
||||||
|
Copy Link
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button outline>
|
||||||
|
<Link className="p-3" to="#" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(window.location.href)}`, 'facebook-share-dialog', 'width=800,height=600');
|
||||||
|
return false;
|
||||||
|
}}>
|
||||||
|
Facebook
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button outline>
|
||||||
|
<Link className="p-3" to="#" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(`https://www.reddit.com/submit?url=${window.location.href}&title=${blogData.name}`, 'facebook-share-dialog', 'width=800,height=600');
|
||||||
|
return false;
|
||||||
|
}}>
|
||||||
|
Reddit
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button outline>
|
||||||
|
<Link className="p-3" to="#" onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
window.open(`https://twitter.com/intent/tweet?text=Check%20out%20this%20article!&url=${window.location.href}`, 'facebook-share-dialog', 'width=800,height=600');
|
||||||
|
return false;
|
||||||
|
}}>
|
||||||
|
X
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</UncontrolledCollapse>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
<Col xs="3" className="d-none d-md-block"></Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className={`my-2 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<Col>
|
||||||
|
<hr style={{"borderColor": `${ThemeConfig[GlobalTheme].borderColor}`}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className="mr-2 ml-2 mt-1">
|
||||||
|
<Col xs="3" className="d-none d-md-block"></Col>
|
||||||
|
|
||||||
|
<Col className={`blogContent ${ThemeConfig[GlobalTheme].textColor}`} style={{marginBottom: '25px'}}>
|
||||||
|
<EditorComponent content={blogData.contentBody}/>
|
||||||
|
<ButtonGroup className='mt-4'>
|
||||||
|
<Button outline>Save Data</Button>
|
||||||
|
<Button outline>Publish Data</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
<Col xs="3" className="d-none d-md-block"></Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<Spinner />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blog
|
||||||
81
frontend/src/components/editable/category-list.jsx
Normal file
81
frontend/src/components/editable/category-list.jsx
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
//import services
|
||||||
|
import DataService from '../../services/data-service';
|
||||||
|
|
||||||
|
//import views
|
||||||
|
import CardListViewer from './shared/card-list-viewer';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Spinner,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
CardImg,
|
||||||
|
CardTitle,
|
||||||
|
CardText,
|
||||||
|
CardBody,
|
||||||
|
Button,
|
||||||
|
ButtonGroup
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function Blogs(props) {
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
|
||||||
|
const [categoryMetadata, setCategoryMetadata] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData('category/category-metadata').then(response =>
|
||||||
|
setCategoryMetadata(response.data)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig) {
|
||||||
|
return (
|
||||||
|
<Container fluid className={`p-0 mb-2 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<Row className="justify-content-center align-items-center">
|
||||||
|
|
||||||
|
<Col className="d-flex flex-column align-items-center">
|
||||||
|
<div className="w-100">
|
||||||
|
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{"width": "100%"}}>
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle style={{ display: "grid" }} className={`${ThemeConfig[GlobalTheme].textColor} justify-content-center`} tag="h1">
|
||||||
|
{"Categories"}<Button className='mt-2' outline>Add New</Button>
|
||||||
|
</CardTitle>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="" style={{ width: '70%', margin: 'auto' }}>
|
||||||
|
|
||||||
|
{categoryMetadata.length > 0 ?
|
||||||
|
categoryMetadata.map((item, index) => (
|
||||||
|
<CardListViewer
|
||||||
|
key={item.id}
|
||||||
|
totalItems={categoryMetadata.length}
|
||||||
|
cardType={"longCard"}
|
||||||
|
resourceType={"categories"}
|
||||||
|
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||||
|
bgColor={ThemeConfig[GlobalTheme].background}
|
||||||
|
itemObject={item}
|
||||||
|
/>
|
||||||
|
)) : <Spinner />}
|
||||||
|
<ButtonGroup className='mt-4'>
|
||||||
|
<Button outline>Save Data</Button>
|
||||||
|
<Button outline>Publish Data</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blogs;
|
||||||
34
frontend/src/components/editable/home.jsx
Normal file
34
frontend/src/components/editable/home.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Container, Spinner, Input, InputGroup, InputGroupText, Button, ButtonGroup } from 'reactstrap';
|
||||||
|
import EditorComponent from './shared/tiptap';
|
||||||
|
import MediaService from '../../services/media-service'
|
||||||
|
|
||||||
|
function HomePage(props) {
|
||||||
|
const UserData = props.UserData ? props.UserData : <Spinner> Loading... </Spinner>
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
if (GlobalTheme && ThemeConfig)
|
||||||
|
return (
|
||||||
|
<Container fluid className={`p-0 mt-5 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<div className="d-flex flex-column justify-content-center align-items-center min-vh-82">
|
||||||
|
{UserData.profilePhoto !== "" ? <img style={{ width: '180px', height: '180px', objectFit: 'cover' }} className="rounded-circle" src={MediaService.getMedia(UserData.profilePhoto)} /> : ""}
|
||||||
|
<div className={`mt-5 ${ThemeConfig[GlobalTheme].textColor}`}>
|
||||||
|
<>
|
||||||
|
<InputGroup className='mb-5'>
|
||||||
|
<InputGroupText>
|
||||||
|
Name
|
||||||
|
</InputGroupText>
|
||||||
|
<Input defaultValue={UserData.name} />
|
||||||
|
</InputGroup>
|
||||||
|
<EditorComponent content={UserData.introContent}/>
|
||||||
|
</>
|
||||||
|
<ButtonGroup className='mt-4'>
|
||||||
|
<Button outline>Save Data</Button>
|
||||||
|
<Button outline>Publish Data</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
62
frontend/src/components/editable/shared/card-list-viewer.jsx
Normal file
62
frontend/src/components/editable/shared/card-list-viewer.jsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import MediaService from '../../../services/media-service'
|
||||||
|
import {
|
||||||
|
Spinner,
|
||||||
|
Card,
|
||||||
|
CardImg,
|
||||||
|
CardTitle,
|
||||||
|
CardText,
|
||||||
|
CardBody,
|
||||||
|
Input, InputGroup, InputGroupText
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function CardListViewer(props) {
|
||||||
|
|
||||||
|
|
||||||
|
const itemObject = props.itemObject
|
||||||
|
|
||||||
|
if (props.totalItems > 0 && itemObject && Object.keys(itemObject).length !== 0)
|
||||||
|
return (
|
||||||
|
<Card className={`my-2 ${props.bgColor}`} style={{"width": props.cardType === "smallCard" ? "18rem": "100%"}}>
|
||||||
|
{itemObject.coverImage !== "" ? <CardImg src={MediaService.getMedia(itemObject.coverImage)} style={{ "height": "180px", "objectFit": "cover" }} top width="100%" /> : ""}
|
||||||
|
<CardBody>
|
||||||
|
<CardTitle className={`${props.textColor}`} tag="h5">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText>
|
||||||
|
Name
|
||||||
|
</InputGroupText>
|
||||||
|
<Input defaultValue={itemObject.name} />
|
||||||
|
</InputGroup>
|
||||||
|
</CardTitle>
|
||||||
|
<CardText className={`${props.textColor}`}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText>
|
||||||
|
Description
|
||||||
|
</InputGroupText>
|
||||||
|
<Input defaultValue={itemObject.description} />
|
||||||
|
</InputGroup>
|
||||||
|
</CardText>
|
||||||
|
<CardText>
|
||||||
|
<small className={`${props.textColor}`}>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroupText>
|
||||||
|
Tagline
|
||||||
|
</InputGroupText>
|
||||||
|
<Input defaultValue={itemObject.tagLine} />
|
||||||
|
</InputGroup>
|
||||||
|
</small>
|
||||||
|
</CardText>
|
||||||
|
<CardText>
|
||||||
|
<Link className={`${props.textColor}`} to={`/${props.resourceType}/${itemObject.id}`}>
|
||||||
|
Open this resource
|
||||||
|
</Link>
|
||||||
|
</CardText>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return(<h3 className={`${props.textColor}`}>No items found in this section</h3>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardListViewer
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import DataService from '../../services/data-service';
|
import DataService from '../../../services/data-service';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Container, Row, Col, Button, Spinner, ListGroup, ListGroupItem, ButtonGroup } from 'reactstrap';
|
import { Container, Row, Col, Button, Spinner, ListGroup, ListGroupItem, ButtonGroup } from 'reactstrap';
|
||||||
|
|
||||||
@ -19,8 +19,10 @@ const Footer = (props) => {
|
|||||||
<Container className='p-1'>
|
<Container className='p-1'>
|
||||||
<Row>
|
<Row>
|
||||||
<Col md="12">
|
<Col md="12">
|
||||||
<div className="text-center text-md-left mt-3">
|
<div className="blogContent text-center text-md-left mt-3">
|
||||||
{new Date().getFullYear()}, <a href="/">{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }</a>
|
{new Date().getFullYear()}, <a className={`${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`} href="/">{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }</a>
|
||||||
|
<br />
|
||||||
|
Built with <a className={`${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`} href="https://github.com/barunespadhy/rangolio">Rangolio</a>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -13,7 +13,7 @@ import {
|
|||||||
Button, ButtonGroup, Label, Input
|
Button, ButtonGroup, Label, Input
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import MediaService from '../../services/media-service'
|
import MediaService from '../../../services/media-service'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faSun, faMoon, faPen } from '@fortawesome/free-solid-svg-icons';
|
import { faSun, faMoon, faPen } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -51,7 +51,6 @@ function Header(props) {
|
|||||||
src={MediaService.getMedia(UserData.profilePhoto)}
|
src={MediaService.getMedia(UserData.profilePhoto)}
|
||||||
/> : ""
|
/> : ""
|
||||||
}
|
}
|
||||||
|
|
||||||
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`} size="lg">
|
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`} size="lg">
|
||||||
{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }
|
{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }
|
||||||
</Button>
|
</Button>
|
||||||
@ -60,10 +59,8 @@ function Header(props) {
|
|||||||
<Nav className="ml-lg-auto" navbar>
|
<Nav className="ml-lg-auto" navbar>
|
||||||
<NavItem>
|
<NavItem>
|
||||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||||
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
|
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||||
>
|
|
||||||
<Link to="/categories">
|
<Link to="/categories">
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faPen} /> Blogs
|
<FontAwesomeIcon icon={faPen} /> Blogs
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
@ -7,7 +7,7 @@ function Notification(props) {
|
|||||||
<Collapse isOpen={props.isOpen} {...props}>
|
<Collapse isOpen={props.isOpen} {...props}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<Alert>{props.notificationMessage}</Alert>
|
<Alert>{props.message}</Alert>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
303
frontend/src/components/editable/shared/tiptap.jsx
Normal file
303
frontend/src/components/editable/shared/tiptap.jsx
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
import React, { useCallback } from 'react'
|
||||||
|
import {
|
||||||
|
Button, ButtonGroup, Label, Input } from 'reactstrap';
|
||||||
|
import { Color } from '@tiptap/extension-color'
|
||||||
|
import ListItem from '@tiptap/extension-list-item'
|
||||||
|
import TextStyle from '@tiptap/extension-text-style'
|
||||||
|
import Highlight from '@tiptap/extension-highlight'
|
||||||
|
import TextAlign from '@tiptap/extension-text-align'
|
||||||
|
import Underline from '@tiptap/extension-underline'
|
||||||
|
import Blockquote from '@tiptap/extension-blockquote'
|
||||||
|
import Link from '@tiptap/extension-link'
|
||||||
|
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||||
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faBold, faItalic,
|
||||||
|
faUnderline, faAlignLeft,
|
||||||
|
faAlignCenter, faAlignRight,
|
||||||
|
faAlignJustify, faHighlighter,
|
||||||
|
faStrikethrough, faCode,
|
||||||
|
faParagraph, faListUl,
|
||||||
|
faListOl, faQuoteLeft,
|
||||||
|
faQuoteRight, faRulerHorizontal,
|
||||||
|
faRotateLeft, faRotateRight,
|
||||||
|
faBars, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
const MenuBar = (props) => {
|
||||||
|
const { editor } = useCurrentEditor()
|
||||||
|
|
||||||
|
const setLink = useCallback(() => {
|
||||||
|
const previousUrl = editor.getAttributes('link').href
|
||||||
|
const url = window.prompt('URL', previousUrl)
|
||||||
|
|
||||||
|
// cancelled
|
||||||
|
if (url === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// empty
|
||||||
|
if (url === '') {
|
||||||
|
editor.chain().focus().extendMarkRange('link').unsetLink()
|
||||||
|
.run()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// update link
|
||||||
|
editor.chain().focus().extendMarkRange('link').setLink({ href: url })
|
||||||
|
.run()
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('left')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAlignLeft}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('center')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAlignCenter}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('right')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAlignRight}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('justify')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faAlignJustify}/>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.toggleBold()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('bold')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBold}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.toggleItalic()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('italic')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faItalic}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('underline')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faUnderline}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHighlight().run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('highlight')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHighlighter}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.toggleStrike()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('strike')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faStrikethrough}/>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Button
|
||||||
|
onClick={setLink}
|
||||||
|
style={{marginLeft: '10px'}}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('link')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faLink}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
style={{marginLeft: '10px'}}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('codeBlock')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCode}/>
|
||||||
|
</Button>
|
||||||
|
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('paragraph')}
|
||||||
|
>
|
||||||
|
p
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 1 })}
|
||||||
|
>
|
||||||
|
h1
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 2 })}
|
||||||
|
>
|
||||||
|
h2
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 3 })}
|
||||||
|
>
|
||||||
|
h3
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 4 })}
|
||||||
|
>
|
||||||
|
h4
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 5 })}
|
||||||
|
>
|
||||||
|
h5
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('heading', { level: 6 })}
|
||||||
|
>
|
||||||
|
h6
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('bulletList')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faListUl}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('orderedList')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faListOl}/>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
style={{marginLeft: '10px'}}
|
||||||
|
outline
|
||||||
|
active={editor.isActive('blockquote')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faQuoteLeft}/> <FontAwesomeIcon icon={faQuoteRight}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||||
|
outline
|
||||||
|
style={{marginLeft: '10px'}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRulerHorizontal}/>
|
||||||
|
</Button>
|
||||||
|
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.undo()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotateLeft}/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
disabled={
|
||||||
|
!editor.can()
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.redo()
|
||||||
|
.run()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotateRight}/>
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = [
|
||||||
|
Color.configure({ types: [TextStyle.name, ListItem.name] }),
|
||||||
|
TextStyle.configure({ types: [ListItem.name] }),
|
||||||
|
StarterKit.configure({
|
||||||
|
bulletList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
orderedList: {
|
||||||
|
keepMarks: true,
|
||||||
|
keepAttributes: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
Blockquote,
|
||||||
|
TextAlign.configure({
|
||||||
|
types: ['heading', 'paragraph'],
|
||||||
|
}),
|
||||||
|
Highlight,
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
autolink: true,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
|
||||||
|
export default (props) => {
|
||||||
|
if (props.content)
|
||||||
|
return (
|
||||||
|
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={props.content}></EditorProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,28 +0,0 @@
|
|||||||
import { Container, Spinner } from 'reactstrap';
|
|
||||||
import MediaService from '../services/media-service'
|
|
||||||
|
|
||||||
function HomePage(props) {
|
|
||||||
const UserData = props.UserData ? props.UserData : <Spinner> Loading... </Spinner>
|
|
||||||
const GlobalTheme = props.GlobalTheme;
|
|
||||||
const ThemeConfig = props.ThemeConfig;
|
|
||||||
if (GlobalTheme && ThemeConfig)
|
|
||||||
return (
|
|
||||||
<Container fluid className={`p-0 mt-5 ${ThemeConfig[GlobalTheme].background}`}>
|
|
||||||
<div className="d-flex flex-column justify-content-center align-items-center min-vh-82">
|
|
||||||
{UserData.profilePhoto !== "" ? <img style={{ width: '180px', height: '180px', objectFit: 'cover' }} className="rounded-circle" src={MediaService.getMedia(UserData.profilePhoto)} /> : ""}
|
|
||||||
<h3 className={`${ThemeConfig[GlobalTheme].textColor}`}>
|
|
||||||
<center>
|
|
||||||
{`${UserData.greetingLine} ${UserData.name}`}
|
|
||||||
</center>
|
|
||||||
</h3>
|
|
||||||
<h5 className={`${ThemeConfig[GlobalTheme].textColor}`}>
|
|
||||||
<>
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: `<span">${UserData.tagLine}</span>` }} />
|
|
||||||
</>
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default HomePage;
|
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import DataService from '../services/data-service';
|
import DataService from '../../services/data-service';
|
||||||
import MediaService from '../services/media-service';
|
import MediaService from '../../services/media-service';
|
||||||
import CardListViewer from './shared/card-list-viewer';
|
import CardListViewer from './shared/card-list-viewer';
|
||||||
import CategoryBar from './shared/category-bar';
|
import CategoryBar from './shared/category-bar';
|
||||||
import {
|
import {
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import parse from 'html-react-parser';
|
import parse from 'html-react-parser';
|
||||||
|
|
||||||
import DataService from '../services/data-service';
|
import DataService from '../../services/data-service';
|
||||||
import MediaService from '../services/media-service'
|
import MediaService from '../../services/media-service'
|
||||||
import CategoryBar from './shared/category-bar';
|
import CategoryBar from './shared/category-bar';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
//import services
|
//import services
|
||||||
import DataService from '../services/data-service';
|
import DataService from '../../services/data-service';
|
||||||
|
|
||||||
//import views
|
//import views
|
||||||
import CardListViewer from './shared/card-list-viewer';
|
import CardListViewer from './shared/card-list-viewer';
|
||||||
44
frontend/src/components/viewable/home.jsx
Normal file
44
frontend/src/components/viewable/home.jsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { Container, Spinner } from 'reactstrap';
|
||||||
|
import parse from 'html-react-parser';
|
||||||
|
import MediaService from '../../services/media-service'
|
||||||
|
|
||||||
|
function HomePage(props) {
|
||||||
|
|
||||||
|
const replace = (node) => {
|
||||||
|
if (node.type === 'tag') {
|
||||||
|
if (node.name === 'a') {
|
||||||
|
const newClasses = `${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`;
|
||||||
|
const existingClasses = node.attribs.class ? `${node.attribs.class} ` : '';
|
||||||
|
node.attribs.class = `${existingClasses}${newClasses}`;
|
||||||
|
node.attribs.rel = 'noopener noreferrer';
|
||||||
|
node.attribs.target = '_blank';
|
||||||
|
}
|
||||||
|
if (node.name === 'img') {
|
||||||
|
const newClasses = `img-fluid mt-2 mb-2 rounded`;
|
||||||
|
const existingClasses = node.attribs.class ? `${node.attribs.class} ` : '';
|
||||||
|
node.attribs.class = `${existingClasses}${newClasses}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const UserData = props.UserData ? props.UserData : <Spinner> Loading... </Spinner>
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
const introContent = props.UserData ? parse(props.UserData.introContent, { replace }) : ""
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig)
|
||||||
|
return (
|
||||||
|
<Container fluid className={`p-0 mt-5 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<div className="d-flex flex-column justify-content-center align-items-center min-vh-82">
|
||||||
|
{UserData.profilePhoto !== "" ? <img style={{ width: '180px', height: '180px', objectFit: 'cover' }} className="rounded-circle" src={MediaService.getMedia(UserData.profilePhoto)} /> : ""}
|
||||||
|
<div className={`mt-5 ${ThemeConfig[GlobalTheme].textColor}`}>
|
||||||
|
<>
|
||||||
|
<div className={`blogContent ${ThemeConfig[GlobalTheme].textColor}`}>{introContent}</div>
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import MediaService from '../../services/media-service'
|
import MediaService from '../../../services/media-service'
|
||||||
import {
|
import {
|
||||||
Spinner,
|
Spinner,
|
||||||
Card,
|
Card,
|
||||||
55
frontend/src/components/viewable/shared/category-bar.jsx
Normal file
55
frontend/src/components/viewable/shared/category-bar.jsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import DataService from '../../../services/data-service';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Container, Row, Col, Button, Spinner, ListGroup, ListGroupItem, ButtonGroup } from 'reactstrap';
|
||||||
|
|
||||||
|
function CategoryBar(props) {
|
||||||
|
|
||||||
|
const [categoryMetadata, setCategoryMetadata] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData('category/category-metadata').then(response =>
|
||||||
|
setCategoryMetadata(response.data)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const rowStyle = {
|
||||||
|
height: 'auto',
|
||||||
|
width: 'auto',
|
||||||
|
overflowX: 'auto',
|
||||||
|
display: 'grid',
|
||||||
|
};
|
||||||
|
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
if (GlobalTheme && ThemeConfig)
|
||||||
|
return (
|
||||||
|
<Container fluid className={`${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<Row style={rowStyle}>
|
||||||
|
<center style={{marginTop: '1.5em', marginBottom: '1.5em'}}>
|
||||||
|
<Col>
|
||||||
|
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||||
|
{categoryMetadata.length > 0 ?
|
||||||
|
categoryMetadata.map((item, index) => (
|
||||||
|
<Button
|
||||||
|
key={item.id}
|
||||||
|
className="btn-lg"
|
||||||
|
color={`${ThemeConfig[GlobalTheme].categoryNavigator}`}
|
||||||
|
outline
|
||||||
|
active={props.currentPage === item.id}
|
||||||
|
|
||||||
|
>
|
||||||
|
<Link className="p-3" to={`/categories/${item.id}`}>
|
||||||
|
{item.name}
|
||||||
|
</Link></Button>
|
||||||
|
)) : <Spinner />
|
||||||
|
}
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</center>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryBar;
|
||||||
35
frontend/src/components/viewable/shared/footer.jsx
Normal file
35
frontend/src/components/viewable/shared/footer.jsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
// Import necessary components from Argon Design System
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Nav,
|
||||||
|
NavLink,
|
||||||
|
Spinner
|
||||||
|
} from 'reactstrap';
|
||||||
|
|
||||||
|
const Footer = (props) => {
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
const UserData = props.UserData;
|
||||||
|
|
||||||
|
if (UserData)
|
||||||
|
return (
|
||||||
|
<footer className={`footer p-4 text-white ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] : ""}`} id="site-footer">
|
||||||
|
<Container className='p-1'>
|
||||||
|
<Row>
|
||||||
|
<Col md="12">
|
||||||
|
<div className="blogContent text-center text-md-left mt-3">
|
||||||
|
{new Date().getFullYear()}, <a className={`${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`} href="/">{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }</a>
|
||||||
|
<br />
|
||||||
|
{ UserData.builtWith ? <span>Built with <a target="_blank" className={`${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`} href="https://github.com/barunespadhy/rangolio">Rangolio</a></span>:""}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
92
frontend/src/components/viewable/shared/navbar.jsx
Normal file
92
frontend/src/components/viewable/shared/navbar.jsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// Update import paths based on your Argon source location
|
||||||
|
import {
|
||||||
|
Navbar,
|
||||||
|
NavbarBrand,
|
||||||
|
UncontrolledCollapse,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Nav,
|
||||||
|
NavItem,
|
||||||
|
NavLink,
|
||||||
|
Container,
|
||||||
|
Spinner,
|
||||||
|
Button, ButtonGroup, Label, Input
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import MediaService from '../../../services/media-service'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faSun, faMoon, faPen } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function Header(props) {
|
||||||
|
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
const UserData = props.UserData;
|
||||||
|
|
||||||
|
const [collapseClasses, setCollapseClasses] = useState('');
|
||||||
|
const [themeSelected, setThemeSelected] = useState('lightTheme');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
props.ThemeSwitcher(themeSelected)
|
||||||
|
}, [themeSelected])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setThemeSelected(props.ThemeConfig.defaultTheme)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig && UserData)
|
||||||
|
return (
|
||||||
|
<header className="header-global" id="site-header">
|
||||||
|
<Navbar className={`navbar-horizontal ${ThemeConfig[GlobalTheme].navBar['navBarTheme']} ${ThemeConfig[GlobalTheme].navBar['background']}`}
|
||||||
|
expand="lg">
|
||||||
|
<Container>
|
||||||
|
<NavbarBrand>
|
||||||
|
<Link to="/">
|
||||||
|
{
|
||||||
|
UserData.profilePhoto !== "" ?
|
||||||
|
<img
|
||||||
|
style={{ width: '40px', height: '40px', objectFit: 'cover', 'marginRight': '10px' }}
|
||||||
|
className="rounded-circle"
|
||||||
|
src={MediaService.getMedia(UserData.profilePhoto)}
|
||||||
|
/> : ""
|
||||||
|
}
|
||||||
|
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`} size="lg">
|
||||||
|
{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</NavbarBrand>
|
||||||
|
<Nav className="ml-lg-auto" navbar>
|
||||||
|
<NavItem>
|
||||||
|
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||||
|
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||||
|
<Link to="/categories">
|
||||||
|
<FontAwesomeIcon icon={faPen} /> Blogs
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
|
||||||
|
outline
|
||||||
|
onClick={() => {setThemeSelected('lightTheme')}}
|
||||||
|
active={themeSelected === 'lightTheme'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faSun} /> Light Theme
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
|
||||||
|
outline
|
||||||
|
onClick={() => {setThemeSelected('darkTheme')}}
|
||||||
|
active={themeSelected === 'darkTheme'}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faMoon}/> Dark Theme
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</NavItem>
|
||||||
|
</Nav>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
18
frontend/src/components/viewable/shared/notification.jsx
Normal file
18
frontend/src/components/viewable/shared/notification.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Collapse, Button, CardBody, Card, Alert } from 'reactstrap';
|
||||||
|
|
||||||
|
function Notification(props) {
|
||||||
|
return (
|
||||||
|
<React.StrictMode>
|
||||||
|
<Collapse isOpen={props.isOpen} {...props}>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<Alert>{props.message}</Alert>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Collapse>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification;
|
||||||
@ -1,7 +1,7 @@
|
|||||||
a {
|
a {
|
||||||
text-decoration: none; /* Removes underline */
|
text-decoration: none !important; /* Removes underline */
|
||||||
color: inherit; /* Inherits color from parent */
|
color: inherit !important; /* Inherits color from parent */
|
||||||
border: none; /* Removes any borders */
|
border: none !important; /* Removes any borders */
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
@ -15,7 +15,6 @@ a {
|
|||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
transition-duration: 0.1s;
|
transition-duration: 0.1s;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blogContent a:hover{
|
.blogContent a:hover{
|
||||||
@ -28,3 +27,15 @@ a {
|
|||||||
.blogContent{
|
.blogContent{
|
||||||
font-size: 20px
|
font-size: 20px
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tiptap blockquote {
|
||||||
|
padding-left: 1rem;
|
||||||
|
border-left: 2px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap.ProseMirror {
|
||||||
|
margin-top: 20px;
|
||||||
|
border: solid grey;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
@ -1,11 +1,15 @@
|
|||||||
import React from 'react'
|
import React, { Suspense, lazy } from 'react';
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
const ViewComponent = lazy(() =>
|
||||||
|
import.meta.env.VITE_APP_VIEW_TYPE === 'editableview'
|
||||||
|
? import('./AppEditable.jsx')
|
||||||
|
: import('./App.jsx')
|
||||||
|
);
|
||||||
|
console.log(import.meta.env.VITE_APP_VIEW_TYPE)
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ViewComponent />
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
Loading…
Reference in New Issue
Block a user