Added initial blog frontend structure

This commit is contained in:
Barunes Padhy 2024-04-21 18:00:59 +03:00
parent f5e661d737
commit 338d667325
38 changed files with 5618 additions and 0 deletions

12
README.md Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

34
frontend/package.json Normal file
View 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"
}
}

View File

@ -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."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -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."
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 882 KiB

View File

@ -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"
}
]

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 818 KiB

View 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"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

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

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

54
frontend/src/App.jsx Normal file
View 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;

View 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

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

View 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

View 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

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

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

View 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

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

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

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

@ -0,0 +1,5 @@
{
"ssl": true,
"baseUrl": "",
"localBackendMode": true
}

5
frontend/src/index.css Normal file
View 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
View 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>,
)

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

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

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})