├── README.md ├── backend ├── .gitignore ├── blog │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── serializers.py │ ├── tests.py │ ├── urls.py │ └── views.py ├── blog_lyfe │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py └── requirements.txt └── frontend ├── .env ├── .gitignore ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── components │ ├── Blog.js │ ├── BlogDetail.js │ ├── Category.js │ ├── Home.js │ └── Navbar.js ├── hocs │ └── Layout.js └── index.js └── yarn.lock /README.md: -------------------------------------------------------------------------------- 1 | # Blog Lyfe 2 | This is a project that demonstrates how you could implement a blog on a Django and React application. What is interesting about the project is that it demonstrates how you can implement a WYSIWYG editor on the Django Admin which makes it much easier to create blogs posts. This is an implementation for making blog posts assuming that you just want an admin user to post blogs on the site, this is not an implementation for a multiuser blog application. 3 | 4 | In order to test out this project, follow these steps: 5 | - clone the repository 6 | - in the frontend folder, run: npm install, this will install the required frontend packages 7 | - in the frontend folder, run: npm run build, this will make a build folder and copy it into the backend folder 8 | - in the backend folder, run: python3 -m venv venv 9 | - then activate the virtual environment 10 | - in the backend folder, run: pip install -r requirements.txt 11 | - in backend/blog_lyfe/settings.py, under DATABASES, set the PASSWORD field to your database password -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/django 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=django 3 | 4 | ### Django ### 5 | *.log 6 | *.pot 7 | *.pyc 8 | __pycache__/ 9 | local_settings.py 10 | db.sqlite3 11 | db.sqlite3-journal 12 | photos 13 | media 14 | static 15 | venv 16 | build 17 | 18 | # If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/ 19 | # in your Git repository. Update and uncomment the following line accordingly. 20 | # /staticfiles/ 21 | 22 | ### Django.Python Stack ### 23 | # Byte-compiled / optimized / DLL files 24 | *.py[cod] 25 | *$py.class 26 | 27 | # C extensions 28 | *.so 29 | 30 | # Distribution / packaging 31 | .Python 32 | build/ 33 | develop-eggs/ 34 | dist/ 35 | downloads/ 36 | eggs/ 37 | .eggs/ 38 | lib/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | pip-wheel-metadata/ 45 | share/python-wheels/ 46 | *.egg-info/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | *.py,cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | pytestdebug.log 75 | 76 | # Translations 77 | *.mo 78 | 79 | # Django stuff: 80 | 81 | # Flask stuff: 82 | instance/ 83 | .webassets-cache 84 | 85 | # Scrapy stuff: 86 | .scrapy 87 | 88 | # Sphinx documentation 89 | docs/_build/ 90 | doc/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # Jupyter Notebook 96 | .ipynb_checkpoints 97 | 98 | # IPython 99 | profile_default/ 100 | ipython_config.py 101 | 102 | # pyenv 103 | .python-version 104 | 105 | # pipenv 106 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 107 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 108 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 109 | # install all needed dependencies. 110 | #Pipfile.lock 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | pythonenv* 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # profiling data 154 | .prof 155 | 156 | # End of https://www.toptal.com/developers/gitignore/api/django -------------------------------------------------------------------------------- /backend/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/backend/blog/__init__.py -------------------------------------------------------------------------------- /backend/blog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django_summernote.admin import SummernoteModelAdmin 3 | from .models import BlogPost 4 | 5 | class BlogPostAdmin(SummernoteModelAdmin): 6 | exclude = ('slug', ) 7 | list_display = ('id', 'title', 'category', 'date_created') 8 | list_display_links = ('id', 'title') 9 | search_fields = ('title', ) 10 | list_per_page = 25 11 | summernote_fields = ('content', ) 12 | 13 | admin.site.register(BlogPost, BlogPostAdmin) 14 | -------------------------------------------------------------------------------- /backend/blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = 'blog' 6 | -------------------------------------------------------------------------------- /backend/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.7 on 2020-06-16 16:26 2 | 3 | import datetime 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='BlogPost', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('title', models.CharField(max_length=50)), 20 | ('slug', models.SlugField()), 21 | ('category', models.CharField(choices=[('world', 'World'), ('environment', 'Environment'), ('technology', 'Technology'), ('design', 'Design'), ('culture', 'Culture'), ('business', 'Business'), ('politics', 'Politics'), ('opinion', 'Opinion'), ('science', 'Science'), ('health', 'Health'), ('style', 'Style'), ('travel', 'Travel')], default='world', max_length=50)), 22 | ('thumbnail', models.ImageField(upload_to='photos/%Y/%m/%d/')), 23 | ('excerpt', models.CharField(max_length=150)), 24 | ('month', models.CharField(max_length=3)), 25 | ('day', models.CharField(max_length=2)), 26 | ('content', models.TextField()), 27 | ('featured', models.BooleanField(default=False)), 28 | ('date_created', models.DateTimeField(blank=True, default=datetime.datetime.now)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /backend/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/backend/blog/migrations/__init__.py -------------------------------------------------------------------------------- /backend/blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from datetime import datetime 3 | from django.template.defaultfilters import slugify 4 | 5 | class Categories(models.TextChoices): 6 | WORLD = 'world' 7 | ENVIRONMENT = 'environment' 8 | TECHNOLOGY = 'technology' 9 | DESIGN = 'design' 10 | CULTURE = 'culture' 11 | BUSINESS = 'business' 12 | POLITICS = 'politics' 13 | OPINION = 'opinion' 14 | SCIENCE = 'science' 15 | HEALTH = 'health' 16 | STYLE = 'style' 17 | TRAVEL = 'travel' 18 | 19 | class BlogPost(models.Model): 20 | title = models.CharField(max_length=50) 21 | slug = models.SlugField() 22 | category = models.CharField(max_length=50, choices=Categories.choices, default=Categories.WORLD) 23 | thumbnail = models.ImageField(upload_to='photos/%Y/%m/%d/') 24 | excerpt = models.CharField(max_length=150) 25 | month = models.CharField(max_length=3) 26 | day = models.CharField(max_length=2) 27 | content = models.TextField() 28 | featured = models.BooleanField(default=False) 29 | date_created = models.DateTimeField(default=datetime.now, blank=True) 30 | 31 | def save(self, *args, **kwargs): 32 | original_slug = slugify(self.title) 33 | queryset = BlogPost.objects.all().filter(slug__iexact=original_slug).count() 34 | 35 | count = 1 36 | slug = original_slug 37 | while(queryset): 38 | slug = original_slug + '-' + str(count) 39 | count += 1 40 | queryset = BlogPost.objects.all().filter(slug__iexact=slug).count() 41 | 42 | self.slug = slug 43 | 44 | if self.featured: 45 | try: 46 | temp = BlogPost.objects.get(featured=True) 47 | if self != temp: 48 | temp.featured = False 49 | temp.save() 50 | except BlogPost.DoesNotExist: 51 | pass 52 | 53 | super(BlogPost, self).save(*args, **kwargs) 54 | 55 | def __str__(self): 56 | return self.title 57 | -------------------------------------------------------------------------------- /backend/blog/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import BlogPost 3 | 4 | class BlogPostSerializer(serializers.ModelSerializer): 5 | class Meta: 6 | model = BlogPost 7 | fields = '__all__' 8 | lookup_field = 'slug' -------------------------------------------------------------------------------- /backend/blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/blog/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from .views import BlogPostListView, BlogPostDetailView, BlogPostFeaturedView, BlogPostCategoryView 3 | 4 | urlpatterns = [ 5 | path('', BlogPostListView.as_view()), 6 | path('featured', BlogPostFeaturedView.as_view()), 7 | path('category', BlogPostCategoryView.as_view()), 8 | path('', BlogPostDetailView.as_view()), 9 | ] 10 | -------------------------------------------------------------------------------- /backend/blog/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework.response import Response 2 | from rest_framework import permissions 3 | from rest_framework.views import APIView 4 | from rest_framework.generics import ListAPIView, RetrieveAPIView 5 | from blog.models import BlogPost 6 | from blog.serializers import BlogPostSerializer 7 | 8 | class BlogPostListView(ListAPIView): 9 | queryset = BlogPost.objects.order_by('-date_created') 10 | serializer_class = BlogPostSerializer 11 | lookup_field = 'slug' 12 | permission_classes = (permissions.AllowAny, ) 13 | 14 | class BlogPostDetailView(RetrieveAPIView): 15 | queryset = BlogPost.objects.order_by('-date_created') 16 | serializer_class = BlogPostSerializer 17 | lookup_field = 'slug' 18 | permission_classes = (permissions.AllowAny, ) 19 | 20 | class BlogPostFeaturedView(ListAPIView): 21 | queryset = BlogPost.objects.all().filter(featured=True) 22 | serializer_class = BlogPostSerializer 23 | lookup_field = 'slug' 24 | permission_classes = (permissions.AllowAny, ) 25 | 26 | class BlogPostCategoryView(APIView): 27 | serializer_class = BlogPostSerializer 28 | permission_classes = (permissions.AllowAny, ) 29 | 30 | def post(self, request, format=None): 31 | data = self.request.data 32 | category = data['category'] 33 | queryset = BlogPost.objects.order_by('-date_created').filter(category__iexact=category) 34 | 35 | serializer = BlogPostSerializer(queryset, many=True) 36 | 37 | return Response(serializer.data) 38 | -------------------------------------------------------------------------------- /backend/blog_lyfe/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/backend/blog_lyfe/__init__.py -------------------------------------------------------------------------------- /backend/blog_lyfe/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for blog_lyfe project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '9lzxal=y%^vdxtai$zgvzxxl4=ppp__11o904lv7x*a^r8$o1i' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'rest_framework', 41 | 'corsheaders', 42 | 'django_summernote', 43 | 'blog' 44 | ] 45 | 46 | MIDDLEWARE = [ 47 | 'corsheaders.middleware.CorsMiddleware', 48 | 'django.middleware.security.SecurityMiddleware', 49 | 'django.contrib.sessions.middleware.SessionMiddleware', 50 | 'django.middleware.common.CommonMiddleware', 51 | 'django.middleware.csrf.CsrfViewMiddleware', 52 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 53 | 'django.contrib.messages.middleware.MessageMiddleware', 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 55 | ] 56 | 57 | ROOT_URLCONF = 'blog_lyfe.urls' 58 | 59 | TEMPLATES = [ 60 | { 61 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 62 | 'DIRS': [os.path.join(BASE_DIR), 'build'], 63 | 'APP_DIRS': True, 64 | 'OPTIONS': { 65 | 'context_processors': [ 66 | 'django.template.context_processors.debug', 67 | 'django.template.context_processors.request', 68 | 'django.contrib.auth.context_processors.auth', 69 | 'django.contrib.messages.context_processors.messages', 70 | ], 71 | }, 72 | }, 73 | ] 74 | 75 | WSGI_APPLICATION = 'blog_lyfe.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.postgresql', 84 | 'NAME': 'blog_lyfe', 85 | 'USER': 'postgres', 86 | 'PASSWORD': '[YOUR POSTGRES DATABASE PASSWORD]', 87 | 'HOST': 'localhost' 88 | } 89 | } 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | STATICFILES_DIRS = [ 129 | os.path.join(BASE_DIR, 'build/static') 130 | ] 131 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 132 | 133 | MEDIA_URL = '/media/' 134 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 135 | 136 | REST_FRAMEWORK = { 137 | 'DEFAULT_PERMISSION_CLASSES': [ 138 | 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' 139 | ] 140 | } 141 | 142 | X_FRAME_OPTIONS = 'SAMEORIGIN' 143 | 144 | SUMMERNOTE_THEME = 'bs4' 145 | 146 | CORS_ORIGIN_ALLOW_ALL = True 147 | -------------------------------------------------------------------------------- /backend/blog_lyfe/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include, re_path 3 | from django.views.generic import TemplateView 4 | from django.conf import settings 5 | from django.conf.urls.static import static 6 | 7 | urlpatterns = [ 8 | path('api-auth/', include('rest_framework.urls')), 9 | path('summernote/', include('django_summernote.urls')), 10 | path('api/blog/', include('blog.urls')), 11 | path('admin/', admin.site.urls), 12 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 13 | 14 | urlpatterns += [re_path(r'^.*', TemplateView.as_view(template_name='index.html'))] 15 | -------------------------------------------------------------------------------- /backend/blog_lyfe/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for blog_lyfe project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_lyfe.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'blog_lyfe.settings') 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == '__main__': 21 | main() 22 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.2.8 2 | Django==3.0.7 3 | django-cors-headers==3.4.0 4 | django-summernote==0.8.11.6 5 | djangorestframework==3.11.0 6 | Pillow==7.1.2 7 | psycopg2==2.8.5 8 | psycopg2-binary==2.8.5 9 | pytz==2020.1 10 | sqlparse==0.3.1 11 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API_URL = 'http://localhost:8000' -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.3.2", 8 | "@testing-library/user-event": "^7.1.2", 9 | "axios": "^0.19.2", 10 | "react": "^16.13.1", 11 | "react-dom": "^16.13.1", 12 | "react-router-dom": "^5.2.0", 13 | "react-scripts": "3.4.1" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "rm -rf ../backend/build && react-scripts build && cp -r build ../backend/build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": "react-app" 23 | }, 24 | "browserslist": { 25 | "production": [ 26 | ">0.2%", 27 | "not dead", 28 | "not op_mini all" 29 | ], 30 | "development": [ 31 | "last 1 chrome version", 32 | "last 1 firefox version", 33 | "last 1 safari version" 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | 33 | Blog Lyfe 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedweb/blog_lyfe/84e37aa10d2ae9774d0abf55f2a3d83753b313e3/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import Layout from './hocs/Layout'; 4 | import Home from './components/Home'; 5 | import Blog from './components/Blog'; 6 | import BlogDetail from './components/BlogDetail'; 7 | import Category from './components/Category'; 8 | 9 | const App = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /frontend/src/components/Blog.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const Blog = () => { 6 | const [blogs, setBlogs] = useState([]); 7 | const [featuredBlog, setFeaturedBlog] = useState([]); 8 | 9 | useEffect(() => { 10 | const fetchData = async () => { 11 | try { 12 | const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/blog/featured`); 13 | setFeaturedBlog(res.data[0]); 14 | console.log(res.data) 15 | } 16 | catch (err) { 17 | 18 | } 19 | } 20 | 21 | fetchData(); 22 | }, []); 23 | 24 | useEffect(() => { 25 | const fetchBlogs = async () => { 26 | try { 27 | const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/blog/`); 28 | setBlogs(res.data); 29 | } 30 | catch (err) { 31 | 32 | } 33 | } 34 | 35 | fetchBlogs(); 36 | }, []); 37 | 38 | const capitalizeFirstLetter = (word) => { 39 | if (word) 40 | return word.charAt(0).toUpperCase() + word.slice(1); 41 | return ''; 42 | }; 43 | 44 | const getBlogs = () => { 45 | let list = []; 46 | let result = []; 47 | 48 | blogs.map(blogPost => { 49 | return list.push( 50 |
51 |
52 | {capitalizeFirstLetter(blogPost.category)} 53 |

{blogPost.title}

54 |
{blogPost.month} {blogPost.day}
55 |

{blogPost.excerpt}

56 | Continue reading 57 |
58 |
59 | thumbnail 60 |
61 |
62 | ); 63 | }); 64 | 65 | for (let i = 0; i < list.length; i += 2) { 66 | result.push( 67 |
68 |
69 | {list[i]} 70 |
71 |
72 | {list[i+1] ? list[i+1] : null} 73 |
74 |
75 | ) 76 | } 77 | 78 | return result; 79 | }; 80 | 81 | return ( 82 |
83 |
84 | 98 |
99 | 100 |
101 |
102 |

{featuredBlog.title}

103 |

{featuredBlog.excerpt}

104 |

105 | 106 | Continue reading... 107 | 108 |

109 |
110 |
111 | 112 | {getBlogs()} 113 |
114 | ); 115 | }; 116 | 117 | export default Blog; 118 | -------------------------------------------------------------------------------- /frontend/src/components/BlogDetail.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import axios from 'axios'; 4 | 5 | const BlogDetail = (props) => { 6 | const [blog, setBlog] = useState({}); 7 | 8 | useEffect(() => { 9 | const slug = props.match.params.id; 10 | 11 | const fetchData = async () => { 12 | try { 13 | const res = await axios.get(`${process.env.REACT_APP_API_URL}/api/blog/${slug}`); 14 | setBlog(res.data); 15 | } 16 | catch (err) { 17 | 18 | } 19 | }; 20 | 21 | fetchData(); 22 | }, [props.match.params.id]); 23 | 24 | const createBlog = () => { 25 | return {__html: blog.content} 26 | }; 27 | 28 | const capitalizeFirstLetter = (word) => { 29 | if (word) 30 | return word.charAt(0).toUpperCase() + word.slice(1); 31 | return ''; 32 | }; 33 | 34 | return ( 35 |
36 |

{blog.title}

37 |

Category: {capitalizeFirstLetter(blog.category)}

38 |

{blog.month} {blog.day}

39 |
40 |
41 |

Back to Blogs

42 |
43 | ); 44 | }; 45 | 46 | export default BlogDetail; 47 | -------------------------------------------------------------------------------- /frontend/src/components/Category.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import axios from 'axios'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | const Category = (props) => { 6 | const [blogs, setBlogs] = useState([]); 7 | const [currentCategory, setCurrentCategory] = useState(''); 8 | 9 | useEffect(() => { 10 | const category = props.match.params.id; 11 | setCurrentCategory(capitalizeFirstLetter(category)); 12 | 13 | const config = { 14 | headers: { 15 | 'Content-Type': 'application/json' 16 | } 17 | }; 18 | 19 | const fetchData = async () => { 20 | try { 21 | const res = await axios.post(`${process.env.REACT_APP_API_URL}/api/blog/category`, { category }, config); 22 | setBlogs(res.data); 23 | } 24 | catch (err) { 25 | 26 | } 27 | }; 28 | 29 | fetchData(); 30 | }, [props.match.params.id]); 31 | 32 | const capitalizeFirstLetter = (word) => { 33 | if (word) 34 | return word.charAt(0).toUpperCase() + word.slice(1); 35 | return ''; 36 | }; 37 | 38 | const getCategoryBlogs = () => { 39 | let list = []; 40 | let result = []; 41 | 42 | blogs.map(blogPost => { 43 | return list.push( 44 |
45 |
46 | {capitalizeFirstLetter(blogPost.category)} 47 |

{blogPost.title}

48 |
{blogPost.month} {blogPost.day}
49 |

{blogPost.excerpt}

50 | Continue reading 51 |
52 |
53 | thumbnail 54 |
55 |
56 | ); 57 | }); 58 | 59 | for (let i = 0; i < list.length; i += 2) { 60 | result.push( 61 |
62 |
63 | {list[i]} 64 |
65 |
66 | {list[i+1] ? list[i+1] : null} 67 |
68 |
69 | ) 70 | } 71 | 72 | return result; 73 | }; 74 | 75 | return ( 76 |
77 |

{currentCategory} Category

78 |
79 | 93 |
94 | {getCategoryBlogs()} 95 |
96 | ); 97 | }; 98 | 99 | export default Category; 100 | -------------------------------------------------------------------------------- /frontend/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | const home = () => ( 5 |
6 |
7 |

Welcome to Blog Lyfe!

8 |

We make all kinds of awesome blog about various topics.

9 |
10 |

Click the button below to check out our awesome blog.

11 | Check out our Blog 12 |
13 |
14 | ); 15 | 16 | export default home; 17 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, NavLink } from 'react-router-dom'; 3 | 4 | const navbar = () => ( 5 | 29 | ); 30 | 31 | export default navbar; 32 | -------------------------------------------------------------------------------- /frontend/src/hocs/Layout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from '../components/Navbar'; 3 | 4 | const layout = (props) => ( 5 |
6 | 7 | {props.children} 8 |
9 | ); 10 | 11 | export default layout; 12 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById('root') 10 | ); 11 | 12 | --------------------------------------------------------------------------------