Restructured frontend, data design, and created the "frontend" for the backend

This commit is contained in:
Barunes Padhy 2024-05-10 21:35:44 +03:00
parent 511b3fe625
commit 99720dd46d
29 changed files with 2007 additions and 113 deletions

View File

@ -0,0 +1 @@
VITE_APP_VIEW_TYPE=editableview

1
frontend/.env.view Normal file
View File

@ -0,0 +1 @@
VITE_APP_VIEW_TYPE=view

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,10 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 3000",
"build": "vite build",
"dev:view": "REACT_APP_VIEW_TYPE=view vite --host 0.0.0.0 --port 3000 --mode view",
"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",
"preview": "vite preview"
},
@ -13,6 +15,17 @@
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@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",
"bootstrap": "^5.3.3",
"html-react-parser": "^5.1.10",
@ -20,7 +33,8 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3",
"reactstrap": "^9.2.2",
"sass": "^1.75.0"
"sass": "^1.75.0",
"tiptap": "^1.32.2"
},
"devDependencies": {
"@types/react": "^18.2.66",

View File

@ -1,13 +1,6 @@
{
"name": "John Doe",
"greetingLine": "Hi! My name is",
"tagLine": "<span>Me like tech. Checkout my blog where I have nice stuff!</span>",
"introContent": "<span>Write something about yourself here!</span>",
"profilePhoto": "homepage/media/profile.png",
"links": {
"instagram": ""
},
"contact":{
"email":"",
"phone": ""
}
"builtWith": true
}

View File

@ -4,16 +4,16 @@ import {useEffect, useState} from 'react';
import 'bootstrap/dist/css/bootstrap.min.css';
//Import Views
import Home from './components/home';
import CategoryList from './components/category-list';
import BlogList from './components/blog-list';
import Blog from './components/blog';
import Home from './components/viewable/home';
import CategoryList from './components/viewable/category-list';
import BlogList from './components/viewable/blog-list';
import Blog from './components/viewable/blog';
//Import Shared Views
import Header from './components/shared/navbar';
import Footer from './components/shared/footer';
import Notification from './components/shared/notification';
import Header from './components/viewable/shared/navbar';
import Footer from './components/viewable/shared/footer';
import Notification from './components/viewable/shared/notification';
//Import Services
import DataService from './services/data-service'
@ -53,7 +53,7 @@ function App() {
<div className="app-container">
<Router>
<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}`}>
<Routes>
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />

View 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;

View 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

View 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

View 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;

View 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;

View 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

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import DataService from '../../services/data-service';
import DataService from '../../../services/data-service';
import { Link } from 'react-router-dom';
import { Container, Row, Col, Button, Spinner, ListGroup, ListGroupItem, ButtonGroup } from 'reactstrap';

View File

@ -19,8 +19,10 @@ const Footer = (props) => {
<Container className='p-1'>
<Row>
<Col md="12">
<div className="text-center text-md-left mt-3">
{new Date().getFullYear()}, <a href="/">{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }</a>
<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 />
Built with <a className={`${ThemeConfig[GlobalTheme].linkBackground} ${ThemeConfig[GlobalTheme].linkTextColor}`} href="https://github.com/barunespadhy/rangolio">Rangolio</a>
</div>
</Col>
</Row>

View File

@ -13,7 +13,7 @@ import {
Button, ButtonGroup, Label, Input
} from 'reactstrap';
import { useState, useEffect } from 'react';
import MediaService from '../../services/media-service'
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';
@ -51,7 +51,6 @@ function Header(props) {
src={MediaService.getMedia(UserData.profilePhoto)}
/> : ""
}
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`} size="lg">
{ UserData ? UserData.name : <Spinner> Loading... </Spinner> }
</Button>
@ -60,10 +59,8 @@ function Header(props) {
<Nav className="ml-lg-auto" navbar>
<NavItem>
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
>
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
<Link to="/categories">
<FontAwesomeIcon icon={faPen} /> Blogs
</Link>
</Button>

View File

@ -7,7 +7,7 @@ function Notification(props) {
<Collapse isOpen={props.isOpen} {...props}>
<Card>
<CardBody>
<Alert>{props.notificationMessage}</Alert>
<Alert>{props.message}</Alert>
</CardBody>
</Card>
</Collapse>

View 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>
)
}

View File

@ -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;

View File

@ -1,8 +1,8 @@
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 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 {

View File

@ -1,8 +1,8 @@
import { useEffect, useState } from 'react';
import parse from 'html-react-parser';
import DataService from '../services/data-service';
import MediaService from '../services/media-service'
import DataService from '../../services/data-service';
import MediaService from '../../services/media-service'
import CategoryBar from './shared/category-bar';
import {

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
//import services
import DataService from '../services/data-service';
import DataService from '../../services/data-service';
//import views
import CardListViewer from './shared/card-list-viewer';

View 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;

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import MediaService from '../../services/media-service'
import MediaService from '../../../services/media-service'
import {
Spinner,
Card,

View 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;

View 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;

View 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;

View 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;

View File

@ -1,7 +1,7 @@
a {
text-decoration: none; /* Removes underline */
color: inherit; /* Inherits color from parent */
border: none; /* Removes any borders */
text-decoration: none !important; /* Removes underline */
color: inherit !important; /* Inherits color from parent */
border: none !important; /* Removes any borders */
}
.app-container {
@ -15,7 +15,6 @@ a {
padding-left: 5px;
padding-right: 5px;
transition-duration: 0.1s;
}
.blogContent a:hover{
@ -28,3 +27,15 @@ a {
.blogContent{
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;
}

View File

@ -1,11 +1,15 @@
import React from 'react'
import React, { Suspense, lazy } from 'react';
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
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(
<React.StrictMode>
<App />
<ViewComponent />
</React.StrictMode>,
)