Created backend structure, made editable homepage
174
backend/.gitignore
vendored
Normal file
@ -0,0 +1,174 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/django
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=django
|
||||
|
||||
### Django ###
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
media
|
||||
|
||||
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||
# in your Git repository. Update and uncomment the following line accordingly.
|
||||
# <django-project-name>/staticfiles/
|
||||
|
||||
### Django.Python Stack ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
||||
# Django stuff:
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/django
|
||||
8
backend/apimanager/MediaHandler.py
Normal file
@ -0,0 +1,8 @@
|
||||
import os
|
||||
|
||||
class MediaHandler:
|
||||
def __init__(self, f, resource_type, resource_id):
|
||||
pass
|
||||
|
||||
def handleUploadedFile(self):
|
||||
pass
|
||||
0
backend/apimanager/__init__.py
Normal file
10
backend/apimanager/admin.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
UserData,
|
||||
Category,
|
||||
Blog
|
||||
)
|
||||
|
||||
admin.site.register(UserData)
|
||||
admin.site.register(Category)
|
||||
admin.site.register(Blog)
|
||||
6
backend/apimanager/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApimanagerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apimanager'
|
||||
8
backend/apimanager/initialize_data.py
Normal file
@ -0,0 +1,8 @@
|
||||
userdata = {
|
||||
"name": "Jhon Doe",
|
||||
"intro_content": "<p>Write something about yourself</p>",
|
||||
"profile_photo": "",
|
||||
"default_theme": "darkTheme",
|
||||
"dark_theme": "{\"darkTheme\": { \"theme\": \"Dark Mode\",\"background\": \"bg-dark\",\"textColor\": \"text-white\",\"linkBackground\": \"bg-light\",\"linkTextColor\": \"text-black\",\"captionColor\": \"#8a8a8a\",\"fontAwesomeIcon\": \"faSun\",\"borderColor\": \"secondary\",\"buttonColor\": \"light\",\"navBar\": {\"navBarTheme\": \"navbar-dark\",\"background\": \"bg-secondary\",\"buttonColor\": \"light\"},\"footer\": {\"background\": \"bg-secondary\",\"text\": \"bg-white\"}}}",
|
||||
"light_theme": "{\"lightTheme\":{\"theme\": \"Light Mode\",\"background\": \"bg-light\",\"textColor\": \"text-black\",\"linkBackground\": \"bg-dark\",\"linkTextColor\": \"text-white\",\"captionColor\": \"#605f5f\",\"fontAwesomeIcon\": \"faMoon\",\"borderColor\": \"secondary\",\"buttonColor\": \"dark\",\"navBar\": {\"navBarTheme\": \"navbar-light\",\"background\": \"bg-secondary\",\"buttonColor\": \"light\"},\"footer\": {\"background\": \"bg-secondary\",\"text\": \"bg-white\"}}}"
|
||||
}
|
||||
49
backend/apimanager/migrations/0001_initial.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-18 14:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Category',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('resourceid', models.SlugField()),
|
||||
('featuredid', models.SlugField()),
|
||||
('name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('description', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('tagline', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('coverimage', models.CharField(blank=True, max_length=500, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserData',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200)),
|
||||
('introContent', models.CharField(blank=True, max_length=100000, null=True)),
|
||||
('profilePhoto', models.CharField(blank=True, max_length=500, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Blog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('resourceid', models.SlugField()),
|
||||
('name', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('description', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('tagline', models.CharField(blank=True, max_length=200, null=True)),
|
||||
('coverimage', models.CharField(blank=True, max_length=500, null=True)),
|
||||
('contentbody', models.CharField(blank=True, max_length=100000, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='blogs', to='apimanager.category')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-18 15:43
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='blog',
|
||||
old_name='contentbody',
|
||||
new_name='content_body',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blog',
|
||||
old_name='coverimage',
|
||||
new_name='cover_image',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='blog',
|
||||
old_name='resourceid',
|
||||
new_name='resource_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='category',
|
||||
old_name='coverimage',
|
||||
new_name='cover_image',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='category',
|
||||
old_name='featuredid',
|
||||
new_name='featured_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='category',
|
||||
old_name='resourceid',
|
||||
new_name='resource_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='userdata',
|
||||
old_name='introContent',
|
||||
new_name='intro_content',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='userdata',
|
||||
old_name='profilePhoto',
|
||||
new_name='profile_photo',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-18 16:13
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0002_rename_contentbody_blog_content_body_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='blog',
|
||||
old_name='resource_id',
|
||||
new_name='blog_id',
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name='category',
|
||||
old_name='resource_id',
|
||||
new_name='category_id',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 07:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0003_rename_resource_id_blog_blog_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userdata',
|
||||
name='dark_theme',
|
||||
field=models.CharField(default='m', max_length=1500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdata',
|
||||
name='defaut_theme',
|
||||
field=models.CharField(default='m', max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdata',
|
||||
name='light_theme',
|
||||
field=models.CharField(default='m', max_length=1500),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 07:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0004_userdata_dark_theme_userdata_defaut_theme_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='dark_theme',
|
||||
field=models.CharField(max_length=1500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='defaut_theme',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='light_theme',
|
||||
field=models.CharField(max_length=1500),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 07:19
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0005_alter_userdata_dark_theme_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='userdata',
|
||||
old_name='defaut_theme',
|
||||
new_name='default_theme',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 07:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0006_rename_defaut_theme_userdata_default_theme'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='featured_id',
|
||||
field=models.CharField(blank=True, max_length=500, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 08:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0007_alter_category_featured_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='dark_theme',
|
||||
field=models.CharField(default='{"darkTheme": { "theme": "Dark Mode","background": "bg-dark","textColor": "text-white","linkBackground": "bg-light","linkTextColor": "text-black","captionColor": "#8a8a8a","fontAwesomeIcon": "faSun","borderColor": "secondary","buttonColor": "light","navBar": {"navBarTheme": "navbar-dark","background": "bg-secondary","buttonColor": "light"},"footer": {"background": "bg-secondary","text": "bg-white"}}}', max_length=1500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='default_theme',
|
||||
field=models.CharField(default='darkTheme', max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='intro_content',
|
||||
field=models.CharField(default='<p>Write something about yourself</p>', max_length=100000),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='light_theme',
|
||||
field=models.CharField(default='{"lightTheme":{"theme": "Light Mode","background": "bg-light","textColor": "text-black","linkBackground": "bg-dark","linkTextColor": "text-white","captionColor": "#605f5f","fontAwesomeIcon": "faMoon","borderColor": "secondary","buttonColor": "dark","navBar": {"navBarTheme": "navbar-light","background": "bg-secondary","buttonColor": "light"},"footer": {"background": "bg-secondary","text": "bg-white"}}}', max_length=1500),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='userdata',
|
||||
name='name',
|
||||
field=models.CharField(default='Jhon Doe', max_length=200),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,48 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 08:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0008_alter_userdata_dark_theme_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='blog',
|
||||
name='content_body',
|
||||
field=models.CharField(default='<p></p>', max_length=100000),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blog',
|
||||
name='description',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blog',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='blog',
|
||||
name='tagline',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='description',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='name',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='category',
|
||||
name='tagline',
|
||||
field=models.CharField(max_length=200),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.0.4 on 2024-05-19 15:55
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('apimanager', '0009_alter_blog_content_body_alter_blog_description_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='blog',
|
||||
old_name='category',
|
||||
new_name='parent_category',
|
||||
),
|
||||
]
|
||||
0
backend/apimanager/migrations/__init__.py
Normal file
28
backend/apimanager/models.py
Normal file
@ -0,0 +1,28 @@
|
||||
from django.db import models
|
||||
from .initialize_data import userdata
|
||||
|
||||
class UserData(models.Model):
|
||||
name = models.CharField(default=userdata["name"], null=False, blank=False, max_length=200)
|
||||
intro_content = models.CharField(default=userdata["intro_content"], null=False, blank=False, max_length=100000)
|
||||
profile_photo = models.CharField(null=True, blank=True, max_length=500)
|
||||
default_theme = models.CharField(default=userdata["default_theme"], null=False, blank=False, max_length=200)
|
||||
dark_theme = models.CharField(default=userdata["dark_theme"], null=False, blank=False, max_length=1500)
|
||||
light_theme = models.CharField(default=userdata["light_theme"], null=False, blank=False, max_length=1500)
|
||||
|
||||
class Category(models.Model):
|
||||
category_id = models.SlugField()
|
||||
featured_id = models.CharField(null=True, blank=True, max_length=500)
|
||||
name = models.CharField(null=False, blank=False, max_length=200)
|
||||
description = models.CharField(null=False, blank=False, max_length=200)
|
||||
tagline = models.CharField(null=False, blank=False, max_length=200)
|
||||
cover_image = models.CharField(null=True, blank=True, max_length=500)
|
||||
|
||||
|
||||
class Blog(models.Model):
|
||||
blog_id = models.SlugField()
|
||||
name = models.CharField(null=False, blank=False, max_length=200)
|
||||
description = models.CharField(null=False, blank=False, max_length=200)
|
||||
tagline = models.CharField(null=False, blank=False, max_length=200)
|
||||
cover_image = models.CharField(null=True, blank=True, max_length=500)
|
||||
content_body = models.CharField(default='<p></p>', null=False, blank=False, max_length=100000)
|
||||
parent_category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="blogs")
|
||||
66
backend/apimanager/serializers.py
Normal file
@ -0,0 +1,66 @@
|
||||
from django.contrib.auth import get_user_model, authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
from .models import (
|
||||
UserData,
|
||||
Category,
|
||||
Blog
|
||||
)
|
||||
|
||||
class UserDataSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserData
|
||||
fields = [
|
||||
'name',
|
||||
'intro_content',
|
||||
'profile_photo',
|
||||
]
|
||||
|
||||
class ThemeDataSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = UserData
|
||||
fields = [
|
||||
'default_theme',
|
||||
'dark_theme',
|
||||
'light_theme'
|
||||
]
|
||||
|
||||
class CategorySerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Category
|
||||
fields = [
|
||||
'category_id',
|
||||
'featured_id',
|
||||
'name',
|
||||
'description',
|
||||
'tagline',
|
||||
'cover_image'
|
||||
]
|
||||
|
||||
class BlogSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Blog
|
||||
fields = [
|
||||
'blog_id',
|
||||
'name',
|
||||
'description',
|
||||
'tagline',
|
||||
'cover_image',
|
||||
'content_body',
|
||||
'parent_category'
|
||||
]
|
||||
|
||||
|
||||
class MediaSerializer(serializers.Serializer):
|
||||
media = serializers.ListField(
|
||||
child=serializers.FileField(max_length=100000, allow_empty_file=False, use_url=False)
|
||||
)
|
||||
resource_type = serializers.CharField(allow_blank=False)
|
||||
resource_id = serializers.CharField(allow_blank=False)
|
||||
3
backend/apimanager/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
199
backend/apimanager/views.py
Normal file
@ -0,0 +1,199 @@
|
||||
#######################Django related imports####################
|
||||
import os
|
||||
import subprocess
|
||||
import ast
|
||||
import shutil
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponseRedirect
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.parsers import MultiPartParser, FormParser
|
||||
from rest_framework import generics, permissions, views, serializers, status
|
||||
#################################################################
|
||||
#API related imports
|
||||
from .models import (
|
||||
UserData,
|
||||
Category,
|
||||
Blog
|
||||
)
|
||||
from .serializers import (
|
||||
UserDataSerializer,
|
||||
ThemeDataSerializer,
|
||||
CategorySerializer,
|
||||
BlogSerializer,
|
||||
MediaSerializer
|
||||
)
|
||||
################################################################
|
||||
#Custom Imports
|
||||
from .MediaHandler import (
|
||||
MediaHandler,
|
||||
)
|
||||
|
||||
#UserData related views#############################################
|
||||
class UserDataUpdateAPIView(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = UserDataSerializer
|
||||
|
||||
def get_object(self):
|
||||
obj, created = UserData.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
class UserDataListAPIView(generics.ListAPIView):
|
||||
queryset = UserData.objects.all()
|
||||
serializer_class = UserDataSerializer
|
||||
#####################################################################
|
||||
|
||||
#ThemeData related views#############################################
|
||||
class ThemeDataUpdateAPIView(generics.RetrieveUpdateAPIView):
|
||||
serializer_class = ThemeDataSerializer
|
||||
|
||||
def get_object(self):
|
||||
obj, created = UserData.objects.get_or_create(pk=1)
|
||||
return obj
|
||||
|
||||
class ThemeDataListAPIView(generics.ListAPIView):
|
||||
queryset = UserData.objects.all()
|
||||
serializer_class = ThemeDataSerializer
|
||||
#####################################################################
|
||||
|
||||
#Category related views#############################################
|
||||
class CategoryCreateAPIView(generics.CreateAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
lookup_field = 'category_id'
|
||||
|
||||
class CategoryUpdateAPIView(generics.RetrieveUpdateAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
lookup_field = 'category_id'
|
||||
|
||||
class CategoryListAPIView(generics.ListAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
|
||||
class CategoryDeleteAPIView(generics.DestroyAPIView):
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
lookup_field = 'category_id'
|
||||
################################################################
|
||||
|
||||
#Blog related views##################################################
|
||||
class BlogCreateAPIView(generics.CreateAPIView):
|
||||
queryset = Blog.objects.all()
|
||||
serializer_class = BlogSerializer
|
||||
lookup_field = 'blog_id'
|
||||
|
||||
class BlogUpdateAPIView(generics.RetrieveUpdateAPIView):
|
||||
queryset = Blog.objects.all()
|
||||
serializer_class = BlogSerializer
|
||||
lookup_field = 'blog_id'
|
||||
|
||||
class BlogRetrieveAPIView(generics.RetrieveAPIView):
|
||||
queryset = Blog.objects.all()
|
||||
serializer_class = BlogSerializer
|
||||
lookup_field = 'blog_id'
|
||||
|
||||
class BlogsByCategoryAPIView(APIView):
|
||||
def get(self, request, category_id):
|
||||
try:
|
||||
category = Category.objects.get(category_id=category_id)
|
||||
except Category.DoesNotExist:
|
||||
return Response({'message': 'Category not found'}, status=404)
|
||||
|
||||
blogs = category.blogs.all()
|
||||
serializer = BlogSerializer(blogs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
class BlogDeleteAPIView(generics.DestroyAPIView):
|
||||
queryset = Blog.objects.all()
|
||||
serializer_class = BlogSerializer
|
||||
lookup_field = 'blog_id'
|
||||
####################################################################
|
||||
|
||||
'''
|
||||
class MediaView(APIView):
|
||||
parser_classes = (MultiPartParser, FormParser)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
file_serializer = FileSerializer(data=request.data)
|
||||
if file_serializer.is_valid():
|
||||
files = dict((f, f) for f in request.FILES.getlist('file'))
|
||||
nodeName = file_serializer.validated_data['nodeName']
|
||||
preferredFormat = file_serializer.validated_data['preferredFormat']
|
||||
for f in files.values():
|
||||
fileHandlerObject = FileHandler(f, preferredFormat, nodeName)
|
||||
fileProcessed = fileHandlerObject.handleUploadedFile()
|
||||
if not fileProcessed[0]:
|
||||
return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
return Response(file_serializer.data, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
return Response(file_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
|
||||
class ETLFunctions(GenericAPIView):
|
||||
|
||||
serializer_class = ETLData
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.data
|
||||
if data['operation'] == "create-folder":
|
||||
os.mkdir('../Analysis/'+data['postData'])
|
||||
with open('../Analysis/'+data['postData']+'/'+data['postData']+'-run.log', 'w') as fp:
|
||||
pass
|
||||
fp.close()
|
||||
|
||||
if data['operation'] == "rename-folder":
|
||||
os.rename('../Analysis/'+data['oldTitle'], '../Analysis/'+data['postData'])
|
||||
|
||||
if data['operation'] == "create-partition-file":
|
||||
print(os.getcwd())
|
||||
print(os.listdir(os.getcwd()))
|
||||
partInfo = (data['postData'].split('|'))
|
||||
with open(f'"{partInfo[0]}.part"', "w") as fpp:
|
||||
pass
|
||||
fpp.close()
|
||||
partitionFile = open(f'"{partInfo[0]}.part"', "w+")
|
||||
partitionFile.write(partInfo[1])
|
||||
partitionFile.close()
|
||||
|
||||
if data['operation'] == "move-file":
|
||||
pass
|
||||
|
||||
return Response("Success", status=status.HTTP_200_OK)
|
||||
|
||||
class BioTools(APIView):
|
||||
def get(self, request):
|
||||
params = request.GET.get('function', '')
|
||||
params = params.split(";")
|
||||
output = subprocess.check_output(f'seqmagick extract-ids ../Analysis/"{params[1]}"/"{(params[2])[:-1]}"', shell=True)
|
||||
outgroups = (output.decode("utf-8")).split('\n')
|
||||
outgroups = outgroups[:len(outgroups)-1]
|
||||
return Response({'outgroups': outgroups})
|
||||
|
||||
class CommandRunner(GenericAPIView):
|
||||
|
||||
serializer_class = InterimData
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = serializer.data
|
||||
runLogFile = f'../Analysis/{data["nodeName"]}/{data["nodeName"]}-run.log'
|
||||
|
||||
try:
|
||||
commands = ast.literal_eval(data['finalParameter'])
|
||||
for key, value in commands.items():
|
||||
process = subprocess.Popen(value+f" > {runLogFile}", shell=True)
|
||||
except:
|
||||
process = subprocess.Popen(data['finalParameter']+f" > {runLogFile}", shell=True)
|
||||
return Response("Command successfully sent for execution", status=status.HTTP_200_OK)
|
||||
'''
|
||||
0
backend/backend/__init__.py
Normal file
16
backend/backend/asgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for backend project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
165
backend/backend/settings.py
Normal file
@ -0,0 +1,165 @@
|
||||
"""
|
||||
Django settings for backend project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 5.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-2qks!5e#imys-r@tp2t#%cc3!*apkfu9f-(a7t)bn%sm@@3aq+'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'apimanager'
|
||||
]
|
||||
|
||||
TINYMCE_DEFAULT_CONFIG = {
|
||||
'height': 360,
|
||||
'width': 1120,
|
||||
'cleanup_on_startup': True,
|
||||
'custom_undo_redo_levels': 20,
|
||||
'selector': 'textarea',
|
||||
'theme': 'silver',
|
||||
'plugins': '''
|
||||
textcolor save link image media preview codesample contextmenu
|
||||
table code lists fullscreen insertdatetime nonbreaking
|
||||
contextmenu directionality searchreplace wordcount visualblocks
|
||||
visualchars code fullscreen autolink lists charmap print hr
|
||||
anchor pagebreak spellchecker
|
||||
''',
|
||||
'toolbar1': '''
|
||||
fullscreen preview bold italic underline | fontselect,
|
||||
fontsizeselect | forecolor backcolor | alignleft alignright |
|
||||
aligncenter alignjustify | indent outdent | bullist numlist table |
|
||||
| link image media | codesample |
|
||||
''',
|
||||
'toolbar2': '''
|
||||
visualblocks visualchars |
|
||||
charmap hr pagebreak nonbreaking anchor | code |
|
||||
''',
|
||||
'contextmenu': 'formats | link image',
|
||||
'menubar': True,
|
||||
'statusbar': True,
|
||||
}
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny',
|
||||
]
|
||||
}
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'backend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'templates')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'backend.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost:3000',
|
||||
]
|
||||
52
backend/backend/urls.py
Normal file
@ -0,0 +1,52 @@
|
||||
"""
|
||||
URL configuration for backend project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, re_path
|
||||
from django.conf.urls import include
|
||||
from .views import my_form
|
||||
from apimanager.views import (
|
||||
UserDataUpdateAPIView,
|
||||
UserDataListAPIView,
|
||||
ThemeDataUpdateAPIView,
|
||||
ThemeDataListAPIView,
|
||||
CategoryCreateAPIView,
|
||||
CategoryUpdateAPIView,
|
||||
CategoryDeleteAPIView,
|
||||
CategoryListAPIView,
|
||||
BlogCreateAPIView,
|
||||
BlogUpdateAPIView,
|
||||
BlogRetrieveAPIView,
|
||||
BlogDeleteAPIView,
|
||||
BlogsByCategoryAPIView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('data/shared/user-data/', UserDataListAPIView.as_view(), name='user-data-list-view'),
|
||||
path('data/shared/update/user-data/', UserDataUpdateAPIView.as_view(), name='user-data-update-view'),
|
||||
path('data/shared/theme-config/', ThemeDataListAPIView.as_view(), name='theme-data-list-view'),
|
||||
path('data/shared/update/theme-config/', ThemeDataUpdateAPIView.as_view(), name='theme-data-update-view'),
|
||||
path('data/category/', CategoryListAPIView.as_view(), name='category-list-view'),
|
||||
path('data/category/<slug:category_id>/', BlogsByCategoryAPIView.as_view(), name='blogs-by-category-view'),
|
||||
path('data/category/create/<slug:category_id>/', CategoryCreateAPIView.as_view(), name='category-create-view'),
|
||||
path('data/category/update/<slug:category_id>/', CategoryUpdateAPIView.as_view(), name='category-update-view'),
|
||||
path('data/category/delete/<slug:category_id>/', CategoryDeleteAPIView.as_view(), name='category-delete-view'),
|
||||
path('data/blog/<slug:blog_id>/', BlogRetrieveAPIView.as_view(), name='blog-retrieve-view'),
|
||||
path('data/blog/create/<slug:blog_id>/', BlogCreateAPIView.as_view(), name='blog-create-view'),
|
||||
path('data/blog/update/<slug:blog_id>/', BlogUpdateAPIView.as_view(), name='blog-update-view'),
|
||||
path('data/blog/delete/<slug:blog_id>/', BlogDeleteAPIView.as_view(), name='blog-delete-view'),
|
||||
]
|
||||
4
backend/backend/views.py
Normal file
@ -0,0 +1,4 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
def my_form(request):
|
||||
return render(request, 'main')
|
||||
16
backend/backend/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for backend project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
backend/manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -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."
|
||||
}
|
||||
@ -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."
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
16
backend/static/json/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"
|
||||
}
|
||||
]
|
||||
38
backend/static/json/data/shared/theme-config.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
"footer": {
|
||||
"background": "bg-secondary",
|
||||
"text": "bg-white"
|
||||
}
|
||||
},
|
||||
"lightTheme":{
|
||||
"theme": "Light Mode",
|
||||
"background": "bg-light",
|
||||
"textColor": "text-black",
|
||||
"captionColor": "#605f5f",
|
||||
"fontAwesomeIcon": "faMoon",
|
||||
"borderColor": "black",
|
||||
"categoryNavigator": "dark",
|
||||
"navBar": {
|
||||
"navBarTheme": "navbar-light",
|
||||
"background": "bg-secondary",
|
||||
"buttonColor": "light"
|
||||
},
|
||||
"footer": {
|
||||
"background": "bg-secondary",
|
||||
"text": "bg-white"
|
||||
}
|
||||
}
|
||||
}
|
||||
13
backend/static/json/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": ""
|
||||
}
|
||||
}
|
||||
10
backend/templates/main.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hi</h1>
|
||||
</body>
|
||||
</html>
|
||||
@ -3,6 +3,7 @@
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"proxy": "http://127.0.0.1:8000",
|
||||
"scripts": {
|
||||
"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",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"name": "Blog 1",
|
||||
"description": "A subtitle for Blog 1",
|
||||
"tagLine": "Read blog",
|
||||
"coverImage": "blogs/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||
"coverImage": "blog/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"contentBody": "<p>Lorem ipsum dolor <a href='#'>sit amet</a>, 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.</p>"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
@ -3,7 +3,7 @@
|
||||
"name": "Blog 2",
|
||||
"description": "A subtitle for Blog 2",
|
||||
"tagLine": "Read blog",
|
||||
"coverImage": "blogs/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||
"coverImage": "blog/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579",
|
||||
"contentBody": "<p>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.</p>"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 882 KiB After Width: | Height: | Size: 882 KiB |
4
frontend/public/data/category/520b7982-069e-4a48-9ef3-64507d86a579/category-data.json
Normal file → Executable file
@ -9,7 +9,7 @@
|
||||
"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",
|
||||
"coverImage": "blog/72e4d550-a19b-4b62-bf5a-13f98813d31a/media/blog1.png",
|
||||
"tagLine": "Read more",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||
},
|
||||
@ -17,7 +17,7 @@
|
||||
"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",
|
||||
"coverImage": "blog/b4d9e1a0-4a77-48eb-a04b-06ec23e2b73e/media/blog2.png",
|
||||
"tagLine": "Read more",
|
||||
"parentCategory": "520b7982-069e-4a48-9ef3-64507d86a579"
|
||||
}]
|
||||
|
||||
0
frontend/public/data/category/520b7982-069e-4a48-9ef3-64507d86a579/media/technology.png
Normal file → Executable file
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
0
frontend/public/data/category/b9e0d686-351d-49af-8e3d-b62023f44dbe/category-data.json
Normal file → Executable file
0
frontend/public/data/category/b9e0d686-351d-49af-8e3d-b62023f44dbe/media/game.png
Normal file → Executable file
|
Before Width: | Height: | Size: 818 KiB After Width: | Height: | Size: 818 KiB |
0
frontend/public/data/category/category-metadata.json
Normal file → Executable file
0
frontend/public/data/homepage/media/profile.png
Normal file → Executable file
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 9.7 KiB |
10
frontend/public/data/shared/theme-config.json
Normal file → Executable file
@ -8,8 +8,8 @@
|
||||
"linkTextColor": "text-black",
|
||||
"captionColor": "#8a8a8a",
|
||||
"fontAwesomeIcon": "faSun",
|
||||
"borderColor": "white",
|
||||
"categoryNavigator": "light",
|
||||
"borderColor": "secondary",
|
||||
"buttonColor": "light",
|
||||
"navBar": {
|
||||
"navBarTheme": "navbar-dark",
|
||||
"background": "bg-secondary",
|
||||
@ -17,7 +17,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"background": "bg-secondary",
|
||||
"text": "bg-white"
|
||||
"text": "text-white"
|
||||
}
|
||||
},
|
||||
"lightTheme":{
|
||||
@ -28,8 +28,8 @@
|
||||
"linkTextColor": "text-white",
|
||||
"captionColor": "#605f5f",
|
||||
"fontAwesomeIcon": "faMoon",
|
||||
"borderColor": "black",
|
||||
"categoryNavigator": "dark",
|
||||
"borderColor": "secondary",
|
||||
"buttonColor": "dark",
|
||||
"navBar": {
|
||||
"navBarTheme": "navbar-light",
|
||||
"background": "bg-secondary",
|
||||
|
||||
0
frontend/public/data/shared/user-data.json
Normal file → Executable file
0
frontend/public/vite.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
0
frontend/src/App.css
Normal file → Executable file
2
frontend/src/App.jsx
Normal file → Executable file
@ -53,8 +53,8 @@ function App() {
|
||||
<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}`}>
|
||||
<Notification isOpen={isOpen} message={notificationMessage} />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
||||
<Route path="/categories" element={<CategoryList notificationToggler={notificationToggler} GlobalTheme={globalTheme} ThemeConfig={themeConfig} />} />
|
||||
|
||||
59
frontend/src/AppEditable.jsx
Normal file → Executable file
@ -16,7 +16,7 @@ import Footer from './components/editable/shared/footer';
|
||||
import Notification from './components/editable/shared/notification';
|
||||
|
||||
//Import Services
|
||||
import DataService from './services/data-service'
|
||||
import EditableDataService from './services/editable-data-service'
|
||||
|
||||
function AppEditable() {
|
||||
const [userData, setUserData] = useState(null);
|
||||
@ -25,44 +25,69 @@ function AppEditable() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [notificationMessage, setNotificationMessage] = useState("")
|
||||
|
||||
const notificationToggler = (message) => {
|
||||
const notificationToggler = (message, color) => {
|
||||
setIsOpen(true)
|
||||
setNotificationMessage(message)
|
||||
setNotificationMessage({message: message, color: color})
|
||||
setTimeout(() => {
|
||||
setIsOpen(false)
|
||||
}, 3500)
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
const setInfo = async (path, data) => {
|
||||
try {
|
||||
const response = await EditableDataService.updateData(path, data);
|
||||
setConfigData();
|
||||
return response.status;
|
||||
} catch (error) {
|
||||
return error.response ? error.response.status : 500;
|
||||
}
|
||||
}
|
||||
|
||||
const setConfigData = () => {
|
||||
EditableDataService.getData('/data/shared/user-data/').then( response => {
|
||||
let responseData = response.data[0]
|
||||
setUserData({
|
||||
"name": responseData["name"],
|
||||
"introContent": responseData["intro_content"],
|
||||
"profilePhoto": responseData["profile_photo"]
|
||||
})
|
||||
}
|
||||
)
|
||||
EditableDataService.getData('/data/shared/theme-config/').then( response =>{
|
||||
let responseData = response.data[0]
|
||||
setThemeConfig({
|
||||
"defaultTheme": responseData["default_theme"],
|
||||
"darkTheme": JSON.parse(responseData["dark_theme"]),
|
||||
"lightTheme": JSON.parse(responseData["light_theme"])
|
||||
})
|
||||
setGlobalTheme(responseData["default_theme"])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
setConfigData()
|
||||
},[])
|
||||
|
||||
const themeSwitcher = (theme) => {
|
||||
setGlobalTheme(theme);
|
||||
}
|
||||
|
||||
if (themeConfig)
|
||||
if (themeConfig && userData && globalTheme)
|
||||
return (
|
||||
<div className="app-container">
|
||||
<Router>
|
||||
<Header className="header" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||
<Notification isOpen={isOpen} message={notificationMessage} />
|
||||
<Header className="header" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} notificationToggler={notificationToggler} setInfo={setInfo} />
|
||||
<div className={`p-0 ${themeConfig[globalTheme].background}`}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />} />
|
||||
<Route path="/" element={<Home notificationToggler={notificationToggler} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} setInfo={setInfo} />} />
|
||||
<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} />
|
||||
<Footer notificationToggler={notificationToggler} setInfo={setInfo} className="footer" ThemeSwitcher={themeSwitcher} GlobalTheme={globalTheme} ThemeConfig={themeConfig} UserData={userData} />
|
||||
<Notification style={{width: '20%'}} className="fixed-top" isOpen={isOpen} message={notificationMessage} />
|
||||
</Router>
|
||||
</div>
|
||||
);
|
||||
|
||||
0
frontend/src/assets/react.svg
Normal file → Executable file
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
33
frontend/src/components/editable/blog-list.jsx
Normal file → Executable file
@ -14,9 +14,7 @@ import {
|
||||
CardImg,
|
||||
CardTitle,
|
||||
CardText,
|
||||
CardBody,
|
||||
Button,
|
||||
ButtonGroup
|
||||
CardBody
|
||||
} from 'reactstrap';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
@ -34,8 +32,9 @@ function BlogList(props) {
|
||||
useEffect(() => {
|
||||
DataService.getData(`category/${categoryID}/category-data`).then(response =>{
|
||||
setCategoryData(response.data);
|
||||
console.log(response.data)
|
||||
if (response.data.featuredBlog){
|
||||
DataService.getData(`blogs/${response.data.featuredBlog}/blog-data`).then(response =>
|
||||
DataService.getData(`blog/${response.data.featuredBlog}/blog-data`).then(response =>
|
||||
setFeaturedBlogData(response.data)
|
||||
);
|
||||
}
|
||||
@ -52,36 +51,46 @@ function BlogList(props) {
|
||||
<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%"}}>
|
||||
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{width: "100%", border: "none"}}>
|
||||
<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>
|
||||
{`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}`}>
|
||||
{`All blogs`}
|
||||
{`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}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={featuredBlogData}
|
||||
/>
|
||||
}
|
||||
{
|
||||
categoryData === 'loading' ? <Spinner /> :
|
||||
categoryData.blogMetadata.map((item, index) => (
|
||||
<CardListViewer
|
||||
key={item.id}
|
||||
totalItems={categoryData.blogMetadata.length}
|
||||
cardType={"longCard"}
|
||||
cardType={"smallCard"}
|
||||
resourceType={"blog"}
|
||||
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||
bgColor={ThemeConfig[GlobalTheme].background}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={item}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<ButtonGroup>
|
||||
<Button outline>Save Data</Button>
|
||||
<Button outline>Publish Data</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
135
frontend/src/components/editable/blog.jsx
Normal file → Executable file
@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import parse from 'html-react-parser';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
|
||||
import DataService from '../../services/data-service';
|
||||
import MediaService from '../../services/media-service'
|
||||
@ -7,53 +6,42 @@ import CategoryBar from './shared/category-bar';
|
||||
import EditorComponent from './shared/tiptap';
|
||||
|
||||
import {
|
||||
Container,Row, Col,Spinner, UncontrolledCollapse, Button, ButtonGroup, Card, CardBody
|
||||
Container,Row, Col,Spinner, UncontrolledCollapse, Button, ButtonGroup, Card, CardBody, Input, InputGroup, InputGroupText
|
||||
} from 'reactstrap';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
function Blog(props) {
|
||||
|
||||
const nameField = useRef(null);
|
||||
const descriptionField = useRef(null);
|
||||
const tagLineField = useRef(null);
|
||||
|
||||
const { blogID } = useParams();
|
||||
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
|
||||
const [blogData, setBlogData] = useState([]);
|
||||
const [blogContent, setBlogContent] = 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}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
const setInfo = (event) => {
|
||||
let localEditedBlogData = {...blogData};
|
||||
localEditedBlogData["name"] = nameField.current.value;
|
||||
localEditedBlogData["description"] = descriptionField.current.value;
|
||||
localEditedBlogData["tagLine"] = tagLineField.current.value;
|
||||
localEditedBlogData["contentBody"] = blogContent
|
||||
setBlogData(localEditedBlogData);
|
||||
props.notificationToggler('Data saved!');
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
DataService.getData(`blogs/${blogID}/blog-data`).then(response =>{
|
||||
DataService.getData(`blog/${blogID}/blog-data`).then(response =>{
|
||||
setBlogData(response.data)
|
||||
const parsedContent = parse(response.data.contentBody, { replace });
|
||||
setBlogContent(parsedContent);
|
||||
setBlogContent(response.data.contentBody)
|
||||
}
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (blogData.contentBody){
|
||||
const parsedContent = parse(blogData.contentBody, { replace });
|
||||
setBlogContent(parsedContent);
|
||||
}
|
||||
}, [GlobalTheme])
|
||||
|
||||
if (GlobalTheme && ThemeConfig && blogData) {
|
||||
return (
|
||||
<Container fluid className={`${ThemeConfig[GlobalTheme].background}`}>
|
||||
@ -70,68 +58,24 @@ function Blog(props) {
|
||||
<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>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroupText>
|
||||
Name
|
||||
</InputGroupText>
|
||||
<Input innerRef={nameField} defaultValue={blogData.name} />
|
||||
</InputGroup>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroupText>
|
||||
Description
|
||||
</InputGroupText>
|
||||
<Input innerRef={descriptionField} defaultValue={blogData.description} />
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
Tagline
|
||||
</InputGroupText>
|
||||
<Input innerRef={tagLineField} defaultValue={blogData.tagLine} />
|
||||
</InputGroup>
|
||||
</Col>
|
||||
<Col xs="3" className="d-none d-md-block"></Col>
|
||||
</Row>
|
||||
@ -141,15 +85,14 @@ function Blog(props) {
|
||||
<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}/>
|
||||
<EditorComponent setContent={setBlogContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={blogData.contentBody}/>
|
||||
<ButtonGroup className='mt-4'>
|
||||
<Button outline>Save Data</Button>
|
||||
<Button outline>Publish Data</Button>
|
||||
<Button onClick={(event) => setInfo(event)} color={ThemeConfig[GlobalTheme].buttonColor} outline>Save Data</Button>
|
||||
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Publish Data</Button>
|
||||
</ButtonGroup>
|
||||
</Col>
|
||||
|
||||
|
||||
10
frontend/src/components/editable/category-list.jsx
Normal file → Executable file
@ -37,13 +37,12 @@ function Blogs(props) {
|
||||
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%"}}>
|
||||
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{width: "100%", border: "none"}}>
|
||||
<CardBody>
|
||||
<CardTitle style={{ display: "grid" }} className={`${ThemeConfig[GlobalTheme].textColor} justify-content-center`} tag="h1">
|
||||
{"Categories"}<Button className='mt-2' outline>Add New</Button>
|
||||
{"Categories"}<Button className='mt-2' color={ThemeConfig[GlobalTheme].buttonColor} outline>Add New</Button>
|
||||
</CardTitle>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@ -60,12 +59,13 @@ function Blogs(props) {
|
||||
resourceType={"categories"}
|
||||
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||
bgColor={ThemeConfig[GlobalTheme].background}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={item}
|
||||
/>
|
||||
)) : <Spinner />}
|
||||
<ButtonGroup className='mt-4'>
|
||||
<Button outline>Save Data</Button>
|
||||
<Button outline>Publish Data</Button>
|
||||
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Save Data</Button>
|
||||
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Publish Data</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
32
frontend/src/components/editable/home.jsx
Normal file → Executable file
@ -1,11 +1,33 @@
|
||||
import { Container, Spinner, Input, InputGroup, InputGroupText, Button, ButtonGroup } from 'reactstrap';
|
||||
import {useEffect, useState, useRef} from 'react';
|
||||
import EditorComponent from './shared/tiptap';
|
||||
import MediaService from '../../services/media-service'
|
||||
|
||||
function HomePage(props) {
|
||||
const [introContent, setIntroContent] = useState("")
|
||||
const [saveKeyReady, setSaveKeyReady] = useState(true)
|
||||
const nameField = useRef(null)
|
||||
const UserData = props.UserData ? props.UserData : <Spinner> Loading... </Spinner>
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
|
||||
const setInfo = async () => {
|
||||
let response = await props.setInfo('/data/shared/update/user-data/', {
|
||||
"name": nameField.current.value,
|
||||
"intro_content": introContent,
|
||||
"profile_photo": ""
|
||||
})
|
||||
console.log(response)
|
||||
if (response === 200)
|
||||
props.notificationToggler("Data saved successfully!")
|
||||
if ([500, 404, 403].includes(response))
|
||||
props.notificationToggler("Something failed!", "danger")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIntroContent(UserData.introContent)
|
||||
}, [UserData])
|
||||
|
||||
if (GlobalTheme && ThemeConfig)
|
||||
return (
|
||||
<Container fluid className={`p-0 mt-5 ${ThemeConfig[GlobalTheme].background}`}>
|
||||
@ -17,13 +39,13 @@ function HomePage(props) {
|
||||
<InputGroupText>
|
||||
Name
|
||||
</InputGroupText>
|
||||
<Input defaultValue={UserData.name} />
|
||||
<Input innerRef={nameField} defaultValue={UserData.name} />
|
||||
</InputGroup>
|
||||
<EditorComponent content={UserData.introContent}/>
|
||||
<EditorComponent GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig} content={UserData.introContent} setContent={setIntroContent}/>
|
||||
</>
|
||||
<ButtonGroup className='mt-4'>
|
||||
<Button outline>Save Data</Button>
|
||||
<Button outline>Publish Data</Button>
|
||||
<ButtonGroup className={`mt-4`}>
|
||||
<Button onClick={() => setInfo()} color={ThemeConfig[GlobalTheme].buttonColor} outline>Save Data</Button>
|
||||
<Button color={ThemeConfig[GlobalTheme].buttonColor} outline>Publish Data</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
108
frontend/src/components/editable/shared/card-list-viewer.jsx
Normal file → Executable file
@ -13,50 +13,72 @@ 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>)
|
||||
}
|
||||
if (props.totalItems > 0 && itemObject && Object.keys(itemObject).length !== 0){
|
||||
if (props.resourceType === 'categories')
|
||||
return (
|
||||
<Card color={props.borderColor} outline 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={`mb-3 ${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 (
|
||||
<Card color={props.borderColor} outline 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
|
||||
3
frontend/src/components/editable/shared/category-bar.jsx
Normal file → Executable file
@ -34,10 +34,9 @@ function CategoryBar(props) {
|
||||
<Button
|
||||
key={item.id}
|
||||
className="btn-lg"
|
||||
color={`${ThemeConfig[GlobalTheme].categoryNavigator}`}
|
||||
color={`${ThemeConfig[GlobalTheme].buttonColor}`}
|
||||
outline
|
||||
active={props.currentPage === item.id}
|
||||
|
||||
>
|
||||
<Link className="p-3" to={`/categories/${item.id}`}>
|
||||
{item.name}
|
||||
|
||||
62
frontend/src/components/editable/shared/footer.jsx
Normal file → Executable file
@ -6,18 +6,76 @@ import {
|
||||
Col,
|
||||
Nav,
|
||||
NavLink,
|
||||
Spinner
|
||||
Spinner,
|
||||
Button,
|
||||
ButtonGroup
|
||||
} from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSun, faMoon, faPen, faBrush } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const Footer = (props) => {
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
const UserData = props.UserData;
|
||||
|
||||
const setInfo = async (color, colorArea) => {
|
||||
let localThemeConfig = {...ThemeConfig}
|
||||
localThemeConfig[GlobalTheme].footer[colorArea] = `${color}`
|
||||
let response = await props.setInfo('/data/shared/update/theme-config/', GlobalTheme === "darkTheme" ? {
|
||||
"dark_theme": JSON.stringify(localThemeConfig[GlobalTheme]),
|
||||
}:{
|
||||
"light_theme": JSON.stringify(localThemeConfig[GlobalTheme]),
|
||||
})
|
||||
if (response === 200)
|
||||
props.notificationToggler(`Color set for ${ThemeConfig[GlobalTheme].theme} successfully!`)
|
||||
if ([500, 404, 403].includes(response))
|
||||
props.notificationToggler("Something failed!", "danger")
|
||||
}
|
||||
|
||||
return (
|
||||
<footer className={`footer p-4 text-white ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] : ""}`} id="site-footer">
|
||||
<footer className={`footer p-4 ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] + ' ' + ThemeConfig[GlobalTheme].footer['text'] : ""}`} id="site-footer">
|
||||
<Container className='p-1'>
|
||||
<Row>
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px', marginRight: '15px'}}>
|
||||
<Button disabled color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||
<FontAwesomeIcon icon={faBrush} /> Set color
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={() => setInfo('bg-primary', 'background')}/>
|
||||
<Button
|
||||
color='secondary'
|
||||
onClick={() => setInfo('bg-secondary', 'background')}/>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={() => setInfo('bg-success', 'background')}/>
|
||||
<Button
|
||||
color='danger'
|
||||
onClick={() => setInfo('bg-danger', 'background')}/>
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={() => setInfo('bg-warning', 'background')}/>
|
||||
<Button
|
||||
color='info'
|
||||
onClick={() => setInfo('bg-info', 'background')}/>
|
||||
<Button
|
||||
color='light'
|
||||
onClick={() => setInfo('bg-light', 'background')}/>
|
||||
<Button
|
||||
color='dark'
|
||||
onClick={() => setInfo('bg-dark', 'background')}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||
<Button disabled color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||
<FontAwesomeIcon icon={faBrush} /> Set footer text color
|
||||
</Button>
|
||||
<Button
|
||||
color='dark'
|
||||
onClick={() => setInfo('text-light', 'text')}>White on black</Button>
|
||||
<Button
|
||||
color='light'
|
||||
onClick={() => setInfo('text-black', 'text')}>Black on white</Button>
|
||||
</ButtonGroup>
|
||||
<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>
|
||||
|
||||
61
frontend/src/components/editable/shared/navbar.jsx
Normal file → Executable file
@ -15,7 +15,7 @@ import {
|
||||
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 { faSun, faMoon, faPen, faBrush } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
function Header(props) {
|
||||
@ -27,6 +27,20 @@ function Header(props) {
|
||||
const [collapseClasses, setCollapseClasses] = useState('');
|
||||
const [themeSelected, setThemeSelected] = useState('lightTheme');
|
||||
|
||||
const setInfo = async (color, colorArea) => {
|
||||
let localThemeConfig = {...ThemeConfig}
|
||||
localThemeConfig[GlobalTheme].navBar[colorArea] = `${color}`
|
||||
let response = await props.setInfo('/data/shared/update/theme-config/', GlobalTheme === "darkTheme" ? {
|
||||
"dark_theme": JSON.stringify(localThemeConfig[GlobalTheme]),
|
||||
}:{
|
||||
"light_theme": JSON.stringify(localThemeConfig[GlobalTheme]),
|
||||
})
|
||||
if (response === 200)
|
||||
props.notificationToggler(`Color set for ${ThemeConfig[GlobalTheme].theme} successfully!`)
|
||||
if ([500, 404, 403].includes(response))
|
||||
props.notificationToggler("Something failed!", "danger")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
props.ThemeSwitcher(themeSelected)
|
||||
}, [themeSelected])
|
||||
@ -58,7 +72,7 @@ function Header(props) {
|
||||
</NavbarBrand>
|
||||
<Nav className="ml-lg-auto" navbar>
|
||||
<NavItem>
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px', marginRight: '15px'}}>
|
||||
<Button color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||
<Link to="/categories">
|
||||
<FontAwesomeIcon icon={faPen} /> Blogs
|
||||
@ -81,6 +95,49 @@ function Header(props) {
|
||||
<FontAwesomeIcon icon={faMoon}/> Dark Theme
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px', marginRight: '15px'}}>
|
||||
<Button disabled color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||
<FontAwesomeIcon icon={faBrush} /> Set color
|
||||
</Button>
|
||||
<Button
|
||||
color='primary'
|
||||
onClick={() => setInfo('bg-primary', 'background')}/>
|
||||
<Button
|
||||
color='secondary'
|
||||
onClick={() => setInfo('bg-secondary', 'background')}/>
|
||||
<Button
|
||||
color='success'
|
||||
onClick={() => setInfo('bg-success', 'background')}/>
|
||||
<Button
|
||||
color='danger'
|
||||
onClick={() => setInfo('bg-danger', 'background')}/>
|
||||
<Button
|
||||
color='warning'
|
||||
onClick={() => setInfo('bg-warning', 'background')}/>
|
||||
<Button
|
||||
color='info'
|
||||
onClick={() => setInfo('bg-info', 'background')}/>
|
||||
<Button
|
||||
color='light'
|
||||
onClick={() => setInfo('bg-light', 'background')}/>
|
||||
<Button
|
||||
color='dark'
|
||||
onClick={() => setInfo('bg-dark', 'background')}/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup style={{marginTop: '15px', marginBottom: '15px'}}>
|
||||
<Button disabled color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}>
|
||||
<FontAwesomeIcon icon={faBrush} /> Set button color
|
||||
</Button>
|
||||
<Button
|
||||
color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
|
||||
outline
|
||||
onClick={() => setInfo('light', 'buttonColor')}>White</Button>
|
||||
<Button
|
||||
color={`${ThemeConfig ? ThemeConfig[GlobalTheme].navBar['buttonColor'] : ""}`}
|
||||
outline
|
||||
onClick={() => setInfo('black', 'buttonColor')}>Black</Button>
|
||||
</ButtonGroup>
|
||||
</NavItem>
|
||||
</Nav>
|
||||
</Container>
|
||||
|
||||
2
frontend/src/components/editable/shared/notification.jsx
Normal file → Executable file
@ -7,7 +7,7 @@ function Notification(props) {
|
||||
<Collapse isOpen={props.isOpen} {...props}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Alert>{props.message}</Alert>
|
||||
<Alert color={props.message.color}>{props.message.message}</Alert>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Collapse>
|
||||
|
||||
72
frontend/src/components/editable/shared/tiptap.jsx
Normal file → Executable file
@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import {
|
||||
Button, ButtonGroup, Label, Input } from 'reactstrap';
|
||||
import { Color } from '@tiptap/extension-color'
|
||||
@ -17,15 +17,30 @@ import { faBold, faItalic,
|
||||
faAlignCenter, faAlignRight,
|
||||
faAlignJustify, faHighlighter,
|
||||
faStrikethrough, faCode,
|
||||
faParagraph, faListUl,
|
||||
faListUl, faLink,
|
||||
faListOl, faQuoteLeft,
|
||||
faQuoteRight, faRulerHorizontal,
|
||||
faRotateLeft, faRotateRight,
|
||||
faBars, faLink } from '@fortawesome/free-solid-svg-icons';
|
||||
faRotateLeft, faRotateRight } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
const MenuBar = (props) => {
|
||||
const { editor } = useCurrentEditor()
|
||||
|
||||
useEffect(() => {
|
||||
if (editor){
|
||||
const handleChange = () => {
|
||||
props.setContent(editor.getHTML())
|
||||
}
|
||||
editor.on('update', handleChange)
|
||||
return () => {
|
||||
editor.on('update', handleChange)
|
||||
}
|
||||
}
|
||||
},[editor])
|
||||
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
|
||||
|
||||
const setLink = useCallback(() => {
|
||||
const previousUrl = editor.getAttributes('link').href
|
||||
const url = window.prompt('URL', previousUrl)
|
||||
@ -54,8 +69,9 @@ const MenuBar = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ButtonGroup>
|
||||
<ButtonGroup className='mt-2'>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
||||
outline
|
||||
active={editor.isActive('left')}
|
||||
@ -63,6 +79,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faAlignLeft}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
outline
|
||||
active={editor.isActive('center')}
|
||||
@ -70,6 +87,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faAlignCenter}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
outline
|
||||
active={editor.isActive('right')}
|
||||
@ -77,15 +95,17 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faAlignRight}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
outline
|
||||
active={editor.isActive('justify')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faAlignJustify}/>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||
</ButtonGroup >
|
||||
<ButtonGroup className='mt-2' style={{marginLeft: '10px'}}>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
disabled={
|
||||
!editor.can()
|
||||
@ -100,6 +120,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faBold}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
disabled={
|
||||
!editor.can()
|
||||
@ -114,6 +135,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faItalic}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||
outline
|
||||
active={editor.isActive('underline')}
|
||||
@ -121,6 +143,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faUnderline}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHighlight().run()}
|
||||
outline
|
||||
active={editor.isActive('highlight')}
|
||||
@ -128,6 +151,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faHighlighter}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
disabled={
|
||||
!editor.can()
|
||||
@ -143,6 +167,8 @@ const MenuBar = (props) => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
className='mt-2'
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={setLink}
|
||||
style={{marginLeft: '10px'}}
|
||||
outline
|
||||
@ -151,6 +177,8 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faLink}/>
|
||||
</Button>
|
||||
<Button
|
||||
className='mt-2'
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
style={{marginLeft: '10px'}}
|
||||
outline
|
||||
@ -158,15 +186,17 @@ const MenuBar = (props) => {
|
||||
>
|
||||
<FontAwesomeIcon icon={faCode}/>
|
||||
</Button>
|
||||
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||
<ButtonGroup className='mt-2' style={{marginLeft: '10px'}}>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
outline
|
||||
active={editor.isActive('paragraph')}
|
||||
>
|
||||
p
|
||||
</Button>
|
||||
<Button
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 1 })}
|
||||
@ -174,6 +204,7 @@ const MenuBar = (props) => {
|
||||
h1
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 2 })}
|
||||
@ -181,6 +212,7 @@ const MenuBar = (props) => {
|
||||
h2
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 3 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 3 })}
|
||||
@ -188,6 +220,7 @@ const MenuBar = (props) => {
|
||||
h3
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 4 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 4 })}
|
||||
@ -195,6 +228,7 @@ const MenuBar = (props) => {
|
||||
h4
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 5 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 5 })}
|
||||
@ -202,6 +236,7 @@ const MenuBar = (props) => {
|
||||
h5
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleHeading({ level: 6 }).run()}
|
||||
outline
|
||||
active={editor.isActive('heading', { level: 6 })}
|
||||
@ -209,8 +244,9 @@ const MenuBar = (props) => {
|
||||
h6
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||
<ButtonGroup className='mt-2' style={{marginLeft: '10px'}}>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||
outline
|
||||
active={editor.isActive('bulletList')}
|
||||
@ -218,6 +254,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faListUl}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
outline
|
||||
active={editor.isActive('orderedList')}
|
||||
@ -226,6 +263,8 @@ const MenuBar = (props) => {
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Button
|
||||
className='mt-2'
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||
style={{marginLeft: '10px'}}
|
||||
outline
|
||||
@ -234,14 +273,17 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faQuoteLeft}/> <FontAwesomeIcon icon={faQuoteRight}/>
|
||||
</Button>
|
||||
<Button
|
||||
className='mt-2'
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
||||
outline
|
||||
style={{marginLeft: '10px'}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRulerHorizontal}/>
|
||||
</Button>
|
||||
<ButtonGroup style={{marginLeft: '10px'}}>
|
||||
<ButtonGroup className='mt-2' style={{marginLeft: '10px'}}>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().undo().run()}
|
||||
disabled={
|
||||
!editor.can()
|
||||
@ -254,6 +296,7 @@ const MenuBar = (props) => {
|
||||
<FontAwesomeIcon icon={faRotateLeft}/>
|
||||
</Button>
|
||||
<Button
|
||||
color={ThemeConfig[GlobalTheme].buttonColor}
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={
|
||||
!editor.can()
|
||||
@ -296,8 +339,11 @@ const extensions = [
|
||||
]
|
||||
|
||||
export default (props) => {
|
||||
if (props.content)
|
||||
const GlobalTheme = props.GlobalTheme;
|
||||
const ThemeConfig = props.ThemeConfig;
|
||||
|
||||
if (props.content && GlobalTheme && ThemeConfig)
|
||||
return (
|
||||
<EditorProvider slotBefore={<MenuBar />} extensions={extensions} content={props.content}></EditorProvider>
|
||||
<EditorProvider slotBefore={<MenuBar setContent={props.setContent} GlobalTheme={GlobalTheme} ThemeConfig={ThemeConfig}/>} extensions={extensions} content={props.content}></EditorProvider>
|
||||
)
|
||||
}
|
||||
7
frontend/src/components/viewable/blog-list.jsx
Normal file → Executable file
@ -32,8 +32,9 @@ function BlogList(props) {
|
||||
useEffect(() => {
|
||||
DataService.getData(`category/${categoryID}/category-data`).then(response =>{
|
||||
setCategoryData(response.data);
|
||||
console.log(response.data)
|
||||
if (response.data.featuredBlog){
|
||||
DataService.getData(`blogs/${response.data.featuredBlog}/blog-data`).then(response =>
|
||||
DataService.getData(`blog/${response.data.featuredBlog}/blog-data`).then(response =>
|
||||
setFeaturedBlogData(response.data)
|
||||
);
|
||||
}
|
||||
@ -50,7 +51,7 @@ function BlogList(props) {
|
||||
<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%"}}>
|
||||
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{width: "100%", border: "none"}}>
|
||||
<CardBody>
|
||||
<CardTitle style={{ display: "grid" }} className={`${ThemeConfig[GlobalTheme].textColor} justify-content-center`} tag="h1">
|
||||
{`Blogs in ${categoryData.name}`}
|
||||
@ -71,6 +72,7 @@ function BlogList(props) {
|
||||
resourceType={"blog"}
|
||||
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||
bgColor={ThemeConfig[GlobalTheme].background}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={featuredBlogData}
|
||||
/>
|
||||
}
|
||||
@ -84,6 +86,7 @@ function BlogList(props) {
|
||||
resourceType={"blog"}
|
||||
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||
bgColor={ThemeConfig[GlobalTheme].background}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={item}
|
||||
/>
|
||||
))
|
||||
|
||||
2
frontend/src/components/viewable/blog.jsx
Normal file → Executable file
@ -38,7 +38,7 @@ function Blog(props) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
DataService.getData(`blogs/${blogID}/blog-data`).then(response =>{
|
||||
DataService.getData(`blog/${blogID}/blog-data`).then(response =>{
|
||||
setBlogData(response.data)
|
||||
const parsedContent = parse(response.data.contentBody, { replace });
|
||||
setBlogContent(parsedContent);
|
||||
|
||||
3
frontend/src/components/viewable/category-list.jsx
Normal file → Executable file
@ -38,7 +38,7 @@ function Blogs(props) {
|
||||
<Col className="d-flex flex-column align-items-center">
|
||||
{/* Top Section - Categories */}
|
||||
<div className="w-100">
|
||||
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{"width": "100%"}}>
|
||||
<Card className={`my-2 ${ThemeConfig[GlobalTheme].background}`} style={{width: "100%", border: "none"}}>
|
||||
<CardBody>
|
||||
<CardTitle style={{ display: "grid" }} className={`${ThemeConfig[GlobalTheme].textColor} justify-content-center`} tag="h1">
|
||||
{"Categories"}
|
||||
@ -57,6 +57,7 @@ function Blogs(props) {
|
||||
resourceType={"categories"}
|
||||
textColor={ThemeConfig[GlobalTheme].textColor}
|
||||
bgColor={ThemeConfig[GlobalTheme].background}
|
||||
borderColor={ThemeConfig[GlobalTheme].borderColor}
|
||||
itemObject={item}
|
||||
/>
|
||||
)) : <Spinner />}
|
||||
|
||||
0
frontend/src/components/viewable/home.jsx
Normal file → Executable file
3
frontend/src/components/viewable/shared/card-list-viewer.jsx
Normal file → Executable file
@ -14,10 +14,11 @@ function CardListViewer(props) {
|
||||
|
||||
|
||||
const itemObject = props.itemObject
|
||||
console.log(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%"}}>
|
||||
<Card color={props.borderColor} outline 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}`}>
|
||||
|
||||
3
frontend/src/components/viewable/shared/category-bar.jsx
Normal file → Executable file
@ -34,10 +34,9 @@ function CategoryBar(props) {
|
||||
<Button
|
||||
key={item.id}
|
||||
className="btn-lg"
|
||||
color={`${ThemeConfig[GlobalTheme].categoryNavigator}`}
|
||||
color={`${ThemeConfig[GlobalTheme].buttonColor}`}
|
||||
outline
|
||||
active={props.currentPage === item.id}
|
||||
|
||||
>
|
||||
<Link className="p-3" to={`/categories/${item.id}`}>
|
||||
{item.name}
|
||||
|
||||
2
frontend/src/components/viewable/shared/footer.jsx
Normal file → Executable file
@ -16,7 +16,7 @@ const Footer = (props) => {
|
||||
|
||||
if (UserData)
|
||||
return (
|
||||
<footer className={`footer p-4 text-white ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] : ""}`} id="site-footer">
|
||||
<footer className={`footer p-4 ${ThemeConfig ? ThemeConfig[GlobalTheme].footer['background'] + ' ' + ThemeConfig[GlobalTheme].footer['text'] : ""}`} id="site-footer">
|
||||
<Container className='p-1'>
|
||||
<Row>
|
||||
<Col md="12">
|
||||
|
||||
0
frontend/src/components/viewable/shared/navbar.jsx
Normal file → Executable file
0
frontend/src/components/viewable/shared/notification.jsx
Normal file → Executable file
@ -1,5 +0,0 @@
|
||||
{
|
||||
"ssl": true,
|
||||
"baseUrl": "",
|
||||
"localBackendMode": true
|
||||
}
|
||||
0
frontend/src/index.css
Normal file → Executable file
1
frontend/src/main.jsx
Normal file → Executable file
@ -6,7 +6,6 @@ const ViewComponent = lazy(() =>
|
||||
? import('./AppEditable.jsx')
|
||||
: import('./App.jsx')
|
||||
);
|
||||
console.log(import.meta.env.VITE_APP_VIEW_TYPE)
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
|
||||
5
frontend/src/services/data-service.jsx
Normal file → Executable file
@ -1,10 +1,7 @@
|
||||
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}`)
|
||||
return axios.get(`/data/${endPoint}.json`)
|
||||
}
|
||||
|
||||
export default { getData }
|
||||
21
frontend/src/services/editable-data-service.jsx
Normal file
@ -0,0 +1,21 @@
|
||||
import axios from 'axios';
|
||||
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
|
||||
if (token) {
|
||||
config.headers['X-CSRFToken'] = token.split('=')[1];
|
||||
}
|
||||
return config;
|
||||
}, error => {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const getData = (endPoint) => {
|
||||
return axios.get(endPoint);
|
||||
};
|
||||
|
||||
const updateData = (endPoint, data) => {
|
||||
return axios.patch(endPoint, data);
|
||||
};
|
||||
|
||||
export default { getData, updateData };
|
||||
0
frontend/src/services/media-service.jsx
Normal file → Executable file
@ -4,4 +4,13 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/data': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||