Added initial blog frontend structure
12
README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Rangolio
|
||||||
|
Rangolio is a no-frills, simple solution built with (R)eact and Dj(ango) to create portf(olio) websites. This platform currently features an initial landing page that can be segmented into various sections, catering to diverse content needs. Additionally, Rangolio extends its functionality to include blog posting, making it a versatile tool for showcasing your work and thoughts.
|
||||||
|
|
||||||
|
Rangolio operates in two ways, one is local backend mode, and the other is live backend mode.
|
||||||
|
|
||||||
|
## Local backend mode
|
||||||
|
Local backend mode is for those usecases when you don't have a dedicated server, and host your webpages on a static hosting service, like [github pages](https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages). This mode does not require you to have a live server, and fetches information from a flat json file, which will be stored in the public folder.
|
||||||
|
|
||||||
|
Website content can be modified using another interface (to be created soon), which modifies those flat json files, from which the frontend fetches the information. Understandably, the backend will be running locally, and the JSON files modified by the local backend will have to be comitted to the hosting service.
|
||||||
|
|
||||||
|
## Live backend mode
|
||||||
|
In live backend mode, the entire backend and frontend can be deployed to your own server, and content can be modified anytime, anywhere as long as you have access to your server.
|
||||||
8
frontend/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4739
frontend/package-lock.json
generated
Normal file
34
frontend/package.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.5.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"axios": "^1.6.8",
|
||||||
|
"bootstrap": "^5.3.3",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.22.3",
|
||||||
|
"reactstrap": "^9.2.2",
|
||||||
|
"sass": "^1.75.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.66",
|
||||||
|
"@types/react-dom": "^18.2.22",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-react": "^7.34.1",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.6",
|
||||||
|
"vite": "^5.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
|
||||||
|
"name": "Blog 1",
|
||||||
|
"description": "A subtitle for Blog 1",
|
||||||
|
"tagLine": "Read blog",
|
||||||
|
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||||
|
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||||
|
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.0 MiB |
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
|
||||||
|
"name": "Blog 2",
|
||||||
|
"description": "A subtitle for Blog 2",
|
||||||
|
"tagLine": "Read blog",
|
||||||
|
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||||
|
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||||
|
"contentBody": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec non ipsum in nunc pretium gravida id ut lectus. Duis ligula nisl, egestas a tortor nec, scelerisque hendrerit urna. Nullam gravida, ante id aliquet ultrices, justo metus aliquet augue, vestibulum posuere massa lacus at est. Cras vitae dolor euismod, volutpat quam eu, cursus enim. Maecenas in magna ut augue ultrices laoreet. Maecenas sapien sem, mollis sed ipsum nec, viverra vehicula quam. Sed et pulvinar justo. Quisque et vestibulum dui. Aliquam laoreet tempus neque, et eleifend nulla vestibulum eget. Cras tempus justo at nunc facilisis auctor. Duis facilisis tortor eu risus aliquam dapibus nec sit amet est. Maecenas ante lectus, sagittis eu facilisis sit amet, convallis eu ex. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean imperdiet vulputate ipsum sed scelerisque. Donec sit amet rutrum est. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Etiam gravida augue sem, quis aliquet justo varius non. Nam tortor nulla, bibendum sit amet finibus sed, ultricies non tellus. Aliquam condimentum risus ut felis porta iaculis. Nullam gravida mauris lacinia finibus hendrerit. Sed nec consectetur erat, sed tincidunt velit. Donec sit amet nulla at sem blandit imperdiet ut eu nisl. Sed condimentum, lectus quis sodales commodo, arcu turpis sodales ex, ac placerat mi turpis sit amet mi. Nullam lorem velit, porta eu quam eu, hendrerit egestas nibh. Sed non pellentesque arcu. Nam felis lectus, scelerisque in semper a, faucibus sit amet nibh. Cras est ligula, pretium id maximus nec, hendrerit eu ante. Maecenas tincidunt est ante, sed vestibulum mauris dignissim nec. Phasellus varius varius leo in pharetra. Aenean vestibulum id dui cursus lobortis. Mauris vel orci massa. Nullam vitae lorem mattis, lobortis dui in, pharetra velit. Aenean non urna ac felis volutpat consequat sit amet sed nisi. Quisque rutrum nisi ac erat ultricies tempor. Etiam nec pellentesque metus. Nulla tempus mi a ex rutrum, ut luctus tellus porta. Interdum et malesuada fames ac ante ipsum primis in faucibus. Phasellus et quam sit amet arcu mattis commodo quis non risus. Sed luctus non dui ullamcorper consectetur. Duis eros magna, tempus ut aliquam sit amet, tempor vel nibh. Vestibulum hendrerit odio convallis elit pretium dictum. Proin ligula dolor, finibus eget lacus sed, facilisis fringilla lorem. Quisque quis lacus sit amet massa blandit fringilla vel vitae tortor. Vivamus dictum nibh vel justo ullamcorper faucibus. Nullam vitae augue pretium erat semper rhoncus. Etiam tempus, arcu feugiat hendrerit pretium, nisi justo sollicitudin velit, sagittis elementum ante metus sed ante. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Etiam quis ligula euismod, venenatis arcu vitae, condimentum arcu. Donec vitae nisl aliquam, rutrum arcu vel, sollicitudin justo. Vestibulum nec sagittis massa. Etiam maximus, erat vitae dapibus vulputate, velit lectus imperdiet est, sed lobortis ante erat lobortis arcu. Maecenas mollis nunc ut nisi tristique tempus. Nulla tempor est non dui scelerisque, eget semper augue consequat."
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 882 KiB |
@ -0,0 +1,18 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "72e4d550-a19b-4b62-bf5a-13f98813d31a",
|
||||||
|
"name": "Blog 1",
|
||||||
|
"description": "A subtitle for Blog 1",
|
||||||
|
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||||
|
"tagLine": "Read more",
|
||||||
|
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e",
|
||||||
|
"name": "Blog 2",
|
||||||
|
"description": "A subtitle for Blog 2",
|
||||||
|
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||||
|
"tagLine": "Read more",
|
||||||
|
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||||
|
"name": "Technology",
|
||||||
|
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
|
||||||
|
"tagLine": "Read articles about tech",
|
||||||
|
"description": "I have been working in tech for long, and here are my thoughts of random stuff",
|
||||||
|
"featuredBlog": "b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.0 MiB |
@ -0,0 +1,3 @@
|
|||||||
|
[{
|
||||||
|
|
||||||
|
}]
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
|
||||||
|
"name": "Gaming",
|
||||||
|
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
|
||||||
|
"tagLine": "Read articles about games",
|
||||||
|
"description": "I like to game, and here are my thoughts on games"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 818 KiB |
16
frontend/public/data/category/category-metadata.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||||
|
"name": "Technology",
|
||||||
|
"coverImage": "category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png",
|
||||||
|
"tagLine": "Read articles about tech",
|
||||||
|
"description": "I have been working in tech for long, and here are my thoughts of random stuff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b9e0d686-351d-49af-8e3d-b62023f44dbe",
|
||||||
|
"name": "Gaming",
|
||||||
|
"coverImage": "category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png",
|
||||||
|
"tagLine": "Read articles about games",
|
||||||
|
"description": "I like to game, and here are my thoughts on games"
|
||||||
|
}
|
||||||
|
]
|
||||||
BIN
frontend/public/data/homepage/media/profile.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
42
frontend/public/data/shared/theme-config.json
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"darkTheme": {
|
||||||
|
"theme": "Dark Mode",
|
||||||
|
"background": "bg-dark",
|
||||||
|
"textColor": "text-white",
|
||||||
|
"captionColor": "#8a8a8a",
|
||||||
|
"fontAwesomeIcon": "faSun",
|
||||||
|
"borderColor": "white",
|
||||||
|
"categoryNavigator": "light",
|
||||||
|
"navBar": {
|
||||||
|
"navBarTheme": "navbar-dark",
|
||||||
|
"background": "bg-secondary",
|
||||||
|
"buttonColor": "light",
|
||||||
|
"shadow": "-webkit-box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75); -moz-box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75); box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75);"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"background": "bg-secondary",
|
||||||
|
"text": "bg-white",
|
||||||
|
"shadow": "1px -29px 20px -22px rgba(0,0,0,0.75)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lightTheme":{
|
||||||
|
"theme": "Light Mode",
|
||||||
|
"background": "bg-light",
|
||||||
|
"textColor": "text-black",
|
||||||
|
"captionColor": "#605f5f",
|
||||||
|
"fontAwesomeIcon": "faMoon",
|
||||||
|
"borderColor": "black",
|
||||||
|
"categoryNavigator": "dark",
|
||||||
|
"navBar": {
|
||||||
|
"navBarTheme": "navbar-light",
|
||||||
|
"background": "bg-primary",
|
||||||
|
"buttonColor": "light",
|
||||||
|
"shadow": "-webkit-box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75); -moz-box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75); box-shadow: 1px 28px 20px -22px rgba(0,0,0,0.75);"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"background": "bg-primary",
|
||||||
|
"text": "bg-white",
|
||||||
|
"shadow": "1px -29px 20px -22px rgba(0,0,0,0.75)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/public/data/shared/user-data.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "John Doe",
|
||||||
|
"greetingLine": "Hi! My name is",
|
||||||
|
"tagLine": "Me like tech. Checkout my blog where I have nice stuff!",
|
||||||
|
"profilePhoto": "homepage/media/profile.png",
|
||||||
|
"links": {
|
||||||
|
"instagram": ""
|
||||||
|
},
|
||||||
|
"contact":{
|
||||||
|
"email":"",
|
||||||
|
"phone": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
0
frontend/src/App.css
Normal file
54
frontend/src/App.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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/views/home';
|
||||||
|
import CategoryList from './components/views/category-list';
|
||||||
|
import BlogList from './components/views/blog-list';
|
||||||
|
import Blog from './components/views/blog';
|
||||||
|
|
||||||
|
|
||||||
|
//Import Shared Views
|
||||||
|
import Header from './components/views/shared/navbar';
|
||||||
|
import Footer from './components/views/shared/footer';
|
||||||
|
|
||||||
|
//Import Services
|
||||||
|
import DataService from './services/data-service'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [userData, setUserData] = useState(null);
|
||||||
|
const [themeConfig, setThemeConfig] = useState(null);
|
||||||
|
const [globalTheme, setGlobalTheme] = useState("lightTheme");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData('shared/user-data').then( response =>
|
||||||
|
setUserData(response.data)
|
||||||
|
)
|
||||||
|
DataService.getData('shared/theme-config').then( response =>
|
||||||
|
setThemeConfig(response.data)
|
||||||
|
)
|
||||||
|
},[])
|
||||||
|
|
||||||
|
const themeSwitcher = (theme) => {
|
||||||
|
setGlobalTheme(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Router>
|
||||||
|
<Header ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
||||||
|
<Route path="/categories" element={<CategoryList GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
<Route path="/categories/:categoryID" element={<BlogList GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
<Route path="/blog/:blogID" element={<Blog GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||||
|
</Routes>
|
||||||
|
<Footer ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||||
|
</Router>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
frontend/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
14
frontend/src/components/elements/toggle-button.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
function ToggleButton(props){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label className="custom-toggle" onClick={props.themeSwitcher}>
|
||||||
|
<input type="checkbox" id={props.Id} onChange={props.ThemeSwitcher}/>
|
||||||
|
<span className="custom-toggle-slider rounded-circle" />
|
||||||
|
</label>
|
||||||
|
<span className="clearfix" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ToggleButton;
|
||||||
107
frontend/src/components/views/blog-list.jsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
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
|
||||||
|
} from 'reactstrap';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
function BlogList(props) {
|
||||||
|
|
||||||
|
const { categoryID } = useParams();
|
||||||
|
|
||||||
|
const GlobalTheme = props.GlobalTheme;
|
||||||
|
const ThemeConfig = props.ThemeConfig;
|
||||||
|
|
||||||
|
const [blogMetadata, setBlogMetadata] = useState('loading');
|
||||||
|
const [categoryData, setCategoryData] = useState('loading');
|
||||||
|
const [featuredBlogData, setFeaturedBlogData] = useState('loading');
|
||||||
|
const [currentPage, setCurrentPage] = useState('loading');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData(`category/${categoryID}/blog-metadata`).then(response =>{
|
||||||
|
setBlogMetadata(response.data)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
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={`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 min-vh-100">
|
||||||
|
<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}`}
|
||||||
|
</CardTitle>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="" style={{ width: '70%', margin: 'auto', display: 'flex', flexWrap: 'wrap', gap: '1rem' }}>
|
||||||
|
<h3 className={`${ThemeConfig[GlobalTheme].textColor}`}>
|
||||||
|
{`Featured`}
|
||||||
|
</h3>
|
||||||
|
{
|
||||||
|
featuredBlogData === 'loading' ? <Spinner /> :
|
||||||
|
<CardListViewer
|
||||||
|
key={featuredBlogData.id}
|
||||||
|
totalItems={featuredBlogData === 'nodata' ? 0 : 1}
|
||||||
|
cardType={"longCard"}
|
||||||
|
resourceType={"blog"}
|
||||||
|
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||||
|
bgColor={ThemeConfig[GlobalTheme].background}
|
||||||
|
itemObject={featuredBlogData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
blogMetadata === 'loading' ? <Spinner /> :
|
||||||
|
blogMetadata.map((item, index) => (
|
||||||
|
<CardListViewer
|
||||||
|
key={item.id}
|
||||||
|
totalItems={blogMetadata.length}
|
||||||
|
cardType={"smallCard"}
|
||||||
|
resourceType={"blog"}
|
||||||
|
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||||
|
bgColor={ThemeConfig[GlobalTheme].background}
|
||||||
|
itemObject={item}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BlogList
|
||||||
119
frontend/src/components/views/blog.jsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import DataService from '../../services/data-service';
|
||||||
|
import MediaService from '../../services/media-service'
|
||||||
|
import CategoryBar from './shared/category-bar';
|
||||||
|
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([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
DataService.getData(`blogs/${blogID}/blog-data`).then(response =>
|
||||||
|
setBlogData(response.data)
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig) {
|
||||||
|
return (
|
||||||
|
<Container fluid className={`min-vh-100 ${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">
|
||||||
|
<Col>
|
||||||
|
<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>
|
||||||
|
<CardBody>
|
||||||
|
<ButtonGroup
|
||||||
|
className="my-2"
|
||||||
|
>
|
||||||
|
<Button outline>
|
||||||
|
<Link 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 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 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>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className={`my-2 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<Col>
|
||||||
|
<hr style={{"borderColor": `${ThemeConfig[GlobalTheme].borderColor}`}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<Row className="mr-5 ml-5 mt-1">
|
||||||
|
<Col>
|
||||||
|
<p style={{marginLeft: '50px', marginRight: '50px'}} className={`${ThemeConfig[GlobalTheme].textColor}`}>
|
||||||
|
{blogData.contentBody}
|
||||||
|
</p>
|
||||||
|
<p style={{marginLeft: '50px', marginRight: '50px'}} className={`${ThemeConfig[GlobalTheme].textColor} mt-1`}>
|
||||||
|
{blogData.contentBody}
|
||||||
|
</p>
|
||||||
|
<p style={{marginLeft: '50px', marginRight: '50px'}} className={`${ThemeConfig[GlobalTheme].textColor} mt-1`}>
|
||||||
|
{blogData.contentBody}
|
||||||
|
</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (<Spinner />)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blog
|
||||||
72
frontend/src/components/views/category-list.jsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
} 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 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<Row className="justify-content-center align-items-center">
|
||||||
|
<Col className="d-flex flex-column align-items-center min-vh-100">
|
||||||
|
{/* Top Section - Categories */}
|
||||||
|
<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"}
|
||||||
|
</CardTitle>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
{/* Bottom Section - Category Metadata or Spinner */}
|
||||||
|
<div className="" style={{ width: '70%', margin: 'auto' }}>
|
||||||
|
{categoryMetadata.length > 0 ?
|
||||||
|
categoryMetadata.map((item, index) => (
|
||||||
|
<CardListViewer
|
||||||
|
totalItems={categoryMetadata.length}
|
||||||
|
cardType={"longCard"}
|
||||||
|
resourceType={"categories"}
|
||||||
|
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||||
|
bgColor={ThemeConfig[GlobalTheme].background}
|
||||||
|
itemObject={item}
|
||||||
|
/>
|
||||||
|
)) : <Spinner />}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blogs;
|
||||||
28
frontend/src/components/views/home.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
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 ${ThemeConfig[GlobalTheme].background}`}>
|
||||||
|
<div className="d-flex flex-column justify-content-center align-items-center vh-100">
|
||||||
|
{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}`}>
|
||||||
|
<center>
|
||||||
|
{`${UserData.tagLine}`}
|
||||||
|
</center>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage;
|
||||||
43
frontend/src/components/views/shared/card-list-viewer.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import MediaService from '../../../services/media-service'
|
||||||
|
import {
|
||||||
|
Spinner,
|
||||||
|
Card,
|
||||||
|
CardImg,
|
||||||
|
CardTitle,
|
||||||
|
CardText,
|
||||||
|
CardBody
|
||||||
|
} 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>
|
||||||
|
<Link to={`/${props.resourceType}/${itemObject.id}`}>
|
||||||
|
<CardTitle className={`${props.textColor}`} tag="h5">
|
||||||
|
{itemObject.name}
|
||||||
|
</CardTitle>
|
||||||
|
<CardText className={`${props.textColor}`}>
|
||||||
|
{itemObject.description}
|
||||||
|
</CardText>
|
||||||
|
<CardText>
|
||||||
|
<small className={`${props.textColor}`}>
|
||||||
|
{itemObject.tagLine}
|
||||||
|
</small>
|
||||||
|
</CardText>
|
||||||
|
</Link>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
else
|
||||||
|
return(<h3 className={`${props.textColor}`}>No items found in this section</h3>)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CardListViewer
|
||||||
54
frontend/src/components/views/shared/category-bar.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
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 to={`/categories/${item.id}`}>
|
||||||
|
{item.name}
|
||||||
|
</Link></Button>
|
||||||
|
)) : <Spinner />
|
||||||
|
}
|
||||||
|
</ButtonGroup>
|
||||||
|
</Col>
|
||||||
|
</center>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CategoryBar;
|
||||||
32
frontend/src/components/views/shared/footer.jsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className={`footer p-4 text-white ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] : ""}`}>
|
||||||
|
<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>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
115
frontend/src/components/views/shared/navbar.jsx
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
// 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 ToggleButton from '../../elements/toggle-button';
|
||||||
|
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])
|
||||||
|
|
||||||
|
const onExiting = () => {
|
||||||
|
setCollapseClasses('collapsing-out');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onExited = () => {
|
||||||
|
setCollapseClasses('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (GlobalTheme && ThemeConfig && UserData)
|
||||||
|
return (
|
||||||
|
<header className="header-global">
|
||||||
|
<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>
|
||||||
|
<button
|
||||||
|
aria-controls="navbar-default"
|
||||||
|
aria-expanded={false}
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
className="navbar-toggler"
|
||||||
|
data-target="#navbar-default"
|
||||||
|
data-toggle="collapse"
|
||||||
|
id="navbar-default"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="navbar-toggler-icon" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<UncontrolledCollapse navbar toggler="#navbar-default" className={collapseClasses} onExiting={onExiting} onExited={onExited}>
|
||||||
|
<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>
|
||||||
|
</UncontrolledCollapse>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
||||||
5
frontend/src/config.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"ssl": true,
|
||||||
|
"baseUrl": "",
|
||||||
|
"localBackendMode": true
|
||||||
|
}
|
||||||
5
frontend/src/index.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
a {
|
||||||
|
text-decoration: none; /* Removes underline */
|
||||||
|
color: inherit; /* Inherits color from parent */
|
||||||
|
border: none; /* Removes any borders */
|
||||||
|
}
|
||||||
11
frontend/src/main.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App.jsx'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
10
frontend/src/services/data-service.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import Config from '../config.json';
|
||||||
|
|
||||||
|
const filePostfix = Config.localBackendMode ? ".json" : ""
|
||||||
|
|
||||||
|
const getData = (endPoint) => {
|
||||||
|
return axios.get(`${Config.baseUrl}/data/${endPoint}${filePostfix}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { getData }
|
||||||
9
frontend/src/services/media-service.jsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Config from '../config.json';
|
||||||
|
|
||||||
|
const pathPrefix = Config.localBackendMode ? '/public' : Config.baseUrl;
|
||||||
|
|
||||||
|
const getMedia = (mediaPath) => {
|
||||||
|
return `${pathPrefix}/data/${mediaPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { getMedia };
|
||||||
7
frontend/vite.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||